From 0e831c96a005688c52d3a15ce78a846767efca09 Mon Sep 17 00:00:00 2001 From: Edwin Lim Date: Wed, 28 Oct 2026 17:45:50 -0700 Subject: [PATCH] feat(traits): write the run's framework through its project trait The session holds the run's written traits in `projectTraits`, each with its source. Every framework write records the framework trait as well as `integration`: the session store's `setFrameworkConfig` on a live session, and `applyFramework` on the drafts the `ciPreRun` hooks of posthog-integration, feature-flags, error-tracking and replay-vision edit. Detection records `static`; the intro screen's framework picker and `--integration` record `user`. The readers of `integration` are unchanged. `getProjectTraits` reads the written framework trait, with its source, over what detection supports. Every later event carries `framework_source`. Before each agent run, the runner compares the framework trait with `integration` and logs `framework trait mismatch` when a write went around the trait. Detection runs before login and flags are evaluated after it, so the change has no flag; the values the run uses are the same as before. Co-Authored-By: Claude Opus 5.5 --- .../__tests__/error-tracking.test.ts | 3 + src/programs/error-tracking/index.ts | 4 +- .../__tests__/feature-flags.test.ts | 5 ++ src/programs/feature-flags/index.ts | 4 +- src/programs/posthog-integration/index.ts | 4 +- src/programs/program-contract/index.ts | 7 +- .../program-contract/program-session.ts | 3 + src/programs/program-contract/program-step.ts | 2 + .../program-contract/project-traits.ts | 5 +- src/programs/program-runner/run-program.ts | 19 ++++- src/programs/replay-vision/index.ts | 4 +- src/programs/types.ts | 1 + .../project-traits/__tests__/traits.test.ts | 72 ++++++++++++++++- src/services/project-traits/index.ts | 4 + src/services/project-traits/traits.ts | 78 +++++++++++++++++-- .../session/__tests__/framework-trait.test.ts | 50 ++++++++++++ src/services/session/session-store.ts | 17 +++- src/services/session/wizard-session.ts | 4 + .../screens/PostHogIntegrationIntroScreen.tsx | 3 +- src/tui/store.ts | 10 ++- 20 files changed, 273 insertions(+), 26 deletions(-) create mode 100644 src/services/session/__tests__/framework-trait.test.ts diff --git a/src/programs/error-tracking/__tests__/error-tracking.test.ts b/src/programs/error-tracking/__tests__/error-tracking.test.ts index 9134f22a9..03604424b 100644 --- a/src/programs/error-tracking/__tests__/error-tracking.test.ts +++ b/src/programs/error-tracking/__tests__/error-tracking.test.ts @@ -139,6 +139,7 @@ describe('error-tracking ciPreRun', () => { const session = { installDir: '/tmp/error-tracking-ci', frameworkContext: {}, + projectTraits: {}, } as unknown as WizardSession; const stopped = errorTracking.ciPreRun?.(session, testCiRunnerContext()); @@ -157,6 +158,7 @@ describe('error-tracking ciPreRun', () => { const session = { installDir: '/tmp/error-tracking-ci', frameworkContext: {}, + projectTraits: {}, } as unknown as WizardSession; const runner = testCiRunnerContext(); @@ -181,6 +183,7 @@ describe('error-tracking posthog-cli pre-install', () => { const session = { installDir: '/tmp/error-tracking-ci', frameworkContext: {}, + projectTraits: {}, } as unknown as WizardSession; await errorTracking.ciPreRun?.(session, runner); diff --git a/src/programs/error-tracking/index.ts b/src/programs/error-tracking/index.ts index e9c144f51..37881b02d 100644 --- a/src/programs/error-tracking/index.ts +++ b/src/programs/error-tracking/index.ts @@ -7,6 +7,7 @@ import { import { detectFramework } from '@services/detection'; import { scopeInstallDirToProject } from '@services/detection'; import { FRAMEWORK_REGISTRY } from '@services/frameworks'; +import { applyFramework } from '@services/project-traits'; import type { ProgramRun } from '@programs/program-contract'; import { ERROR_TRACKING_UNSUPPORTED, @@ -196,8 +197,7 @@ export const config: ProgramConfig = { if (ERROR_TRACKING_UNSUPPORTED.has(integration)) { abortUnsupportedPlatform(integration); } - session.integration = integration; - analytics.setTag('integration', integration); + applyFramework(session, integration); session.frameworkConfig = FRAMEWORK_REGISTRY[integration]; session.skillId = integration; diff --git a/src/programs/feature-flags/__tests__/feature-flags.test.ts b/src/programs/feature-flags/__tests__/feature-flags.test.ts index bef73d989..09058f9fc 100644 --- a/src/programs/feature-flags/__tests__/feature-flags.test.ts +++ b/src/programs/feature-flags/__tests__/feature-flags.test.ts @@ -36,12 +36,17 @@ describe('feature-flags program', () => { installDir: '/tmp/feature-flags-ci', frameworkContext: {}, integration: null, + projectTraits: {}, skillId: null, } as unknown as WizardSession; await featureFlagsConfig.ciPreRun?.(session, testCiRunnerContext()); expect(session.integration).toBe(Integration.nextjs); + expect(session.projectTraits.framework).toEqual({ + value: Integration.nextjs, + source: 'static', + }); expect(session.skillId).toBe(Integration.nextjs); }); }); diff --git a/src/programs/feature-flags/index.ts b/src/programs/feature-flags/index.ts index 7afce882a..da40e603c 100644 --- a/src/programs/feature-flags/index.ts +++ b/src/programs/feature-flags/index.ts @@ -13,6 +13,7 @@ import { gatherFrameworkContext } from '@services/detection'; import { noteDetectedFramework } from '@services/detection'; import { scopeInstallDirToProject } from '@services/detection'; import { FRAMEWORK_REGISTRY } from '@services/frameworks'; +import { applyFramework } from '@services/project-traits'; import { ProgramAbort } from '@programs/program-contract'; import type { ProgramConfig } from '@programs/program-contract'; import type { ProgramSession } from '@programs/program-contract'; @@ -89,8 +90,7 @@ export const config: ProgramConfig = { if (FEATURE_FLAGS_UNSUPPORTED.has(integration)) { abortUnsupportedPlatform(integration); } - session.integration = integration; - analytics.setTag('integration', integration); + applyFramework(session, integration); const frameworkConfig = FRAMEWORK_REGISTRY[integration]; session.frameworkConfig = frameworkConfig; diff --git a/src/programs/posthog-integration/index.ts b/src/programs/posthog-integration/index.ts index 87f5dad88..53bc75a88 100644 --- a/src/programs/posthog-integration/index.ts +++ b/src/programs/posthog-integration/index.ts @@ -22,6 +22,7 @@ import type { RunnerContext, } from '@programs/program-contract'; import { FRAMEWORK_REGISTRY } from '@services/frameworks'; +import { applyFramework } from '@services/project-traits'; import { abortNoFrameworkDetected } from '../shared/abort-no-framework'; import { SETUP_REPORT_FILE, @@ -296,8 +297,7 @@ export const config: ProgramConfig = { if (!integration) { abortNoFrameworkDetected(); } - session.integration = integration; - analytics.setTag('integration', integration); + applyFramework(session, integration); const frameworkConfig = FRAMEWORK_REGISTRY[integration]; session.frameworkConfig = frameworkConfig; diff --git a/src/programs/program-contract/index.ts b/src/programs/program-contract/index.ts index 8aa74f6ab..b086ccd51 100644 --- a/src/programs/program-contract/index.ts +++ b/src/programs/program-contract/index.ts @@ -9,7 +9,12 @@ export { export { ProgramAbort } from './program-abort'; export type { ProgramRun } from './program-run'; export type { ProgramSession, ScanReport } from './program-session'; -export type { ProjectTraits, Trait, TraitName } from './project-traits'; +export type { + ProjectTraits, + Trait, + TraitName, + TraitSource, +} from './project-traits'; export type { DecisionQuestionKey, ProgramConfig, diff --git a/src/programs/program-contract/program-session.ts b/src/programs/program-contract/program-session.ts index 3e7c4736a..eac3aa597 100644 --- a/src/programs/program-contract/program-session.ts +++ b/src/programs/program-contract/program-session.ts @@ -8,6 +8,7 @@ import type { ApiUser, Credentials } from '@shared/api'; import type { DiscoveredFeature } from '@shared/discovered-feature'; import type { ScanConsent } from '@shared/run-state'; import type { FrameworkConfig } from './framework-config'; +import type { ProjectTraits } from './project-traits'; import type { OutroData } from '@agent/types'; export interface ProgramSession { @@ -64,6 +65,8 @@ export interface ProgramSession { /** The scan the user agreed to share. Set once per run; the task stream sends it. */ codebaseScanReport: ScanReport | null; integration: Integration | null; + /** The run's written traits, each with its source; `integration` mirrors the framework. Read them through `@services/project-traits`. */ + projectTraits: ProjectTraits; frameworkContext: Record; typescript: boolean; diff --git a/src/programs/program-contract/program-step.ts b/src/programs/program-contract/program-step.ts index 6cc9934ae..486167826 100644 --- a/src/programs/program-contract/program-step.ts +++ b/src/programs/program-contract/program-step.ts @@ -5,6 +5,7 @@ import type { ProgramRun } from './program-run'; import type { Integration } from '@shared/constants'; import type { FrameworkConfig } from './framework-config'; import type { CiRunnerContext, RunnerContext } from './runner-context'; +import type { TraitSource } from './project-traits'; /** A question a program may ask the Decisions service; the catalog in `@services/detection/decisions` has exactly these keys. */ export type DecisionQuestionKey = @@ -40,6 +41,7 @@ export interface ProgramReadyContext { readonly setFrameworkConfig: ( integration: Integration, config: FrameworkConfig, + source?: TraitSource, ) => void; readonly setDetectedFramework: (label: string) => void; readonly setSkillId: (skillId: string | null) => void; diff --git a/src/programs/program-contract/project-traits.ts b/src/programs/program-contract/project-traits.ts index fbdcab472..cc305c450 100644 --- a/src/programs/program-contract/project-traits.ts +++ b/src/programs/program-contract/project-traits.ts @@ -2,10 +2,13 @@ export type TraitName = 'framework' | 'callsLlmApi'; +/** Where a trait's value came from: static detection, or the person (the intro picker or `--integration`). */ +export type TraitSource = 'static' | 'user'; + /** A predicate trait's value is `yes` or `no`; the framework's is its integration id. */ export type Trait = { value: string; - source: 'static'; + source: TraitSource; }; export type ProjectTraits = Partial>; diff --git a/src/programs/program-runner/run-program.ts b/src/programs/program-runner/run-program.ts index 04cd5e3d0..619753883 100644 --- a/src/programs/program-runner/run-program.ts +++ b/src/programs/program-runner/run-program.ts @@ -44,7 +44,11 @@ import { type ResolvedProgramCredentials, } from './credentials'; import { reportCodebaseScan } from '@services/codebase-scan'; -import { getProjectTraits, traitsToSkillTags } from '@services/project-traits'; +import { + frameworkTraitMismatch, + getProjectTraits, + traitsToSkillTags, +} from '@services/project-traits'; import { IS_PRODUCTION_BUILD } from '@env'; import type { QuestionKey } from '@services/detection/decisions'; import { logIn } from './login'; @@ -398,6 +402,19 @@ async function invokeProgram( planned.own ? undefined : planned.stepId, ); const current = session(); + // Every framework write records the trait too; a disagreement means one went around it. + const mismatch = frameworkTraitMismatch(current); + if (mismatch) { + logToFile( + `[traits] framework trait ${ + mismatch.trait ?? 'none' + } disagrees with integration ${mismatch.integration ?? 'none'}`, + ); + analytics.wizardCapture('framework trait mismatch', { + ...mismatch, + program_id: planned.config.id, + }); + } const result = await runAgent( { programId: planned.config.id, diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index 58323c1f2..26d4a07a1 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -12,6 +12,7 @@ import { noteDetectedFramework } from '@services/detection'; import { scopeInstallDirToProject } from '@services/detection'; import type { CiRunnerContext } from '@programs/program-contract'; import { FRAMEWORK_REGISTRY } from '@services/frameworks'; +import { applyFramework } from '@services/project-traits'; import { createSkillProgram } from '../shared/skill-program'; import { detectPostHogIntegration } from '@services/detection'; import type { @@ -168,8 +169,7 @@ export const config: ProgramConfig = { if (!REPLAY_VISION_SUPPORTED.has(integration)) { abortUnsupportedPlatform(integration); } - session.integration = integration; - analytics.setTag('integration', integration); + applyFramework(session, integration); const frameworkConfig = FRAMEWORK_REGISTRY[integration]; session.frameworkConfig = frameworkConfig; diff --git a/src/programs/types.ts b/src/programs/types.ts index a85bddfb5..87ecd5ef1 100644 --- a/src/programs/types.ts +++ b/src/programs/types.ts @@ -12,6 +12,7 @@ export type { } from '@programs/program-contract'; export type { ProgramRun } from '@programs/program-contract'; export type { ProgramSession } from '@programs/program-contract'; +export type { TraitSource } from '@programs/program-contract'; export type { CiRunnerContext, RunnerContext, diff --git a/src/services/project-traits/__tests__/traits.test.ts b/src/services/project-traits/__tests__/traits.test.ts index 070adf29d..26dd2316d 100644 --- a/src/services/project-traits/__tests__/traits.test.ts +++ b/src/services/project-traits/__tests__/traits.test.ts @@ -1,11 +1,18 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { DiscoveredFeature } from '@shared/discovered-feature'; import { Integration } from '@shared/constants'; +import { analytics } from '@utils/analytics'; import { + applyFramework, + frameworkTraitMismatch, getProjectTraits, traitsFromDetectors, traitsToSkillTags, + withFramework, } from '@services/project-traits/traits'; +import type { ProjectTraits } from '@programs/program-contract'; + +vi.mock('@utils/analytics', () => ({ analytics: { setTag: vi.fn() } })); describe('traitsFromDetectors', () => { it('reads the framework from detection', () => { @@ -37,14 +44,75 @@ describe('traitsFromDetectors', () => { }).callsLlmApi, ).toBeUndefined(); }); +}); - it('gives every program in a run the same traits', () => { +describe('getProjectTraits', () => { + it('reads detection when no framework trait was written', () => { const session = { integration: Integration.django, discoveredFeatures: [DiscoveredFeature.LLM], + projectTraits: {}, }; expect(getProjectTraits(session)).toEqual(traitsFromDetectors(session)); }); + + it('keeps the written framework trait and its source', () => { + expect( + getProjectTraits({ + integration: Integration.flask, + discoveredFeatures: [], + projectTraits: { framework: { value: 'flask', source: 'user' } }, + }).framework, + ).toEqual({ value: 'flask', source: 'user' }); + }); +}); + +describe('framework writes', () => { + it('replaces the framework trait, or removes it for no framework', () => { + const traits: ProjectTraits = { + framework: { value: 'nextjs', source: 'static' }, + callsLlmApi: { value: 'yes', source: 'static' }, + }; + expect(withFramework(traits, Integration.nuxt, 'user')).toEqual({ + framework: { value: 'nuxt', source: 'user' }, + callsLlmApi: { value: 'yes', source: 'static' }, + }); + expect(withFramework(traits, null, 'static')).toEqual({ + callsLlmApi: { value: 'yes', source: 'static' }, + }); + }); + + it('writes integration, the trait and both tags on a draft', () => { + const draft = { + integration: null as Integration | null, + projectTraits: {} as ProjectTraits, + }; + applyFramework(draft, Integration.rails); + expect(draft).toEqual({ + integration: 'rails', + projectTraits: { framework: { value: 'rails', source: 'static' } }, + }); + expect(analytics.setTag).toHaveBeenCalledWith('integration', 'rails'); + expect(analytics.setTag).toHaveBeenCalledWith('framework_source', 'static'); + }); + + it('finds a write that went around the trait', () => { + expect( + frameworkTraitMismatch({ integration: null, projectTraits: {} }), + ).toBeUndefined(); + expect( + frameworkTraitMismatch({ + integration: Integration.nextjs, + projectTraits: { framework: { value: 'nextjs', source: 'static' } }, + }), + ).toBeUndefined(); + expect( + frameworkTraitMismatch({ + integration: Integration.nextjs, + projectTraits: {}, + }), + ).toEqual({ trait: null, integration: 'nextjs' }); + }); }); describe('traitsToSkillTags', () => { diff --git a/src/services/project-traits/index.ts b/src/services/project-traits/index.ts index 420d7519b..799315638 100644 --- a/src/services/project-traits/index.ts +++ b/src/services/project-traits/index.ts @@ -1,6 +1,10 @@ export { + applyFramework, + frameworkTraitMismatch, getProjectTraits, + tagFramework, traitsFromDetectors, traitsToSkillTags, + withFramework, type DetectionResults, } from './traits'; diff --git a/src/services/project-traits/traits.ts b/src/services/project-traits/traits.ts index 09a2e2055..261025568 100644 --- a/src/services/project-traits/traits.ts +++ b/src/services/project-traits/traits.ts @@ -1,14 +1,20 @@ /** * The run's project traits: what detection read from the user's codebase, as * skill criteria and flow conditions see it. Traits describe the code, never - * the person running the wizard. Static detection is the only source, and - * context-mill never sees a trait name, only the tags `traitsToSkillTags` maps - * values to. + * the person running the wizard. Static detection and the person's own pick + * are the sources, and context-mill never sees a trait name, only the tags + * `traitsToSkillTags` maps values to. */ +import { analytics } from '@utils/analytics'; +import type { Integration } from '@shared/constants'; import { DiscoveredFeature } from '@shared/discovered-feature'; import { frameworkTag, type TraitTags } from '@shared/skill-resolution'; -import type { ProgramSession, ProjectTraits } from '@programs/program-contract'; +import type { + ProgramSession, + ProjectTraits, + TraitSource, +} from '@programs/program-contract'; /** What static detection stored on the session that a trait reads. */ export type DetectionResults = Pick< @@ -16,6 +22,9 @@ export type DetectionResults = Pick< 'integration' | 'discoveredFeatures' >; +/** The session fields a framework write touches. */ +type FrameworkFields = Pick; + /** The traits static detection supports. A trait no detector settles is absent. */ export function traitsFromDetectors(scan: DetectionResults): ProjectTraits { const traits: ProjectTraits = {}; @@ -29,9 +38,64 @@ export function traitsFromDetectors(scan: DetectionResults): ProjectTraits { return traits; } -/** The run's traits. Every program in a run reads the same ones. */ -export function getProjectTraits(session: DetectionResults): ProjectTraits { - return traitsFromDetectors(session); +/** The run's traits: the written framework trait, with its source, over what detection supports. Every program in a run reads the same ones. */ +export function getProjectTraits( + session: DetectionResults & Pick, +): ProjectTraits { + const detected = traitsFromDetectors(session); + const framework = session.projectTraits.framework; + return framework ? { ...detected, framework } : detected; +} + +/** The traits with the framework replaced, or removed when there is none. */ +export function withFramework( + traits: ProjectTraits, + integration: Integration | null, + source: TraitSource, +): ProjectTraits { + const rest: ProjectTraits = { ...traits }; + delete rest.framework; + return integration + ? { ...rest, framework: { value: integration, source } } + : rest; +} + +/** Tags every later event with the run's framework and where it came from. */ +export function tagFramework( + integration: Integration, + source: TraitSource, +): void { + analytics.setTag('integration', integration); + analytics.setTag('framework_source', source); +} + +/** + * Writes the run's framework onto a session draft: the trait with its source, + * `integration` as every reader sees it, and the analytics tags. The session + * store's `setFrameworkConfig` makes the same write on a live session. + */ +export function applyFramework( + session: FrameworkFields, + integration: Integration, + source: TraitSource = 'static', +): void { + session.integration = integration; + session.projectTraits = withFramework( + session.projectTraits, + integration, + source, + ); + tagFramework(integration, source); +} + +/** Where the framework trait and `integration` disagree, a write went around the trait path; undefined when they agree. */ +export function frameworkTraitMismatch( + session: FrameworkFields, +): { trait: string | null; integration: string | null } | undefined { + const trait = session.projectTraits.framework?.value ?? null; + return trait === session.integration + ? undefined + : { trait, integration: session.integration }; } /** Each trait value as the tags skill criteria require or prefer. Only the framework has a skill tag today. */ diff --git a/src/services/session/__tests__/framework-trait.test.ts b/src/services/session/__tests__/framework-trait.test.ts new file mode 100644 index 000000000..dfdb7f48b --- /dev/null +++ b/src/services/session/__tests__/framework-trait.test.ts @@ -0,0 +1,50 @@ +/** Every framework write on a session records the framework trait with its source. */ +import { Integration } from '@shared/constants'; +import { analytics } from '@utils/analytics'; +import { frameworkTraitMismatch } from '../../project-traits/traits'; +import { SessionStore } from '../session-store'; +import { buildSession } from '../wizard-session'; + +vi.mock('@utils/analytics', () => ({ + analytics: { setTag: vi.fn() }, +})); + +describe('framework trait', () => { + it('records `--integration` as the person’s pick', () => { + expect(buildSession({}).projectTraits).toEqual({}); + expect( + buildSession({ integration: Integration.django }).projectTraits, + ).toEqual({ framework: { value: 'django', source: 'user' } }); + }); + + it('records a detected framework as static, and tags its source', () => { + const store = new SessionStore(buildSession({})); + store.setFrameworkConfig(Integration.nextjs, null); + expect(store.session.integration).toBe('nextjs'); + expect(store.session.projectTraits.framework).toEqual({ + value: 'nextjs', + source: 'static', + }); + expect(analytics.setTag).toHaveBeenCalledWith('framework_source', 'static'); + expect(frameworkTraitMismatch(store.session)).toBeUndefined(); + }); + + it('records a picked framework as the person’s, replacing detection’s', () => { + const store = new SessionStore(buildSession({})); + store.setFrameworkConfig(Integration.nextjs, null); + store.setFrameworkConfig(Integration.nuxt, null, 'user'); + expect(store.session.projectTraits.framework).toEqual({ + value: 'nuxt', + source: 'user', + }); + }); + + it('clears the trait with the framework', () => { + const store = new SessionStore( + buildSession({ integration: Integration.flask }), + ); + store.setFrameworkConfig(null, null); + expect(store.session.projectTraits).toEqual({}); + expect(frameworkTraitMismatch(store.session)).toBeUndefined(); + }); +}); diff --git a/src/services/session/session-store.ts b/src/services/session/session-store.ts index cd5f66322..422139511 100644 --- a/src/services/session/session-store.ts +++ b/src/services/session/session-store.ts @@ -30,7 +30,11 @@ import { TaskStatus, isTaskStatus } from '@shared/task-status'; import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; import { reportWarehouseSourcesDetected } from '../detection/integration'; -import type { ProgramReadyContext } from '@programs/program-contract'; +import { tagFramework, withFramework } from '../project-traits/traits'; +import type { + ProgramReadyContext, + TraitSource, +} from '@programs/program-contract'; import type { WizardSession } from './wizard-session'; import type { ScanReport } from './task-stream/types'; @@ -262,14 +266,20 @@ export class SessionStore { this.set('apiUser', user); } + /** The one write of the run's framework on a live session: the trait with its source, and `integration` as every reader sees it. */ setFrameworkConfig( integration: WizardSession['integration'], config: WizardSession['frameworkConfig'], + source: TraitSource = 'static', ): void { this.$session.setKey('integration', integration); + this.$session.setKey( + 'projectTraits', + withFramework(this.session.projectTraits, integration, source), + ); this.$session.setKey('frameworkConfig', config); this.$session.setKey('unsupportedVersion', null); - if (integration) analytics.setTag('integration', integration); + if (integration) tagFramework(integration, source); this.emitChange(); } @@ -546,10 +556,11 @@ export class SessionStore { return read(); }, setFrameworkContext: (k, v) => this.setFrameworkContext(k, v), - setFrameworkConfig: (i, c) => + setFrameworkConfig: (i, c, s) => this.setFrameworkConfig( i as WizardSession['integration'], c as WizardSession['frameworkConfig'], + s, ), setDetectedFramework: (l) => this.setDetectedFramework(l), setPosthogSdkDetected: (d) => this.setPosthogSdkDetected(d), diff --git a/src/services/session/wizard-session.ts b/src/services/session/wizard-session.ts index 67e1558fe..1975b0b17 100644 --- a/src/services/session/wizard-session.ts +++ b/src/services/session/wizard-session.ts @@ -136,6 +136,10 @@ export function buildSession(args: SessionArgs): WizardSession { warehouseSourcesReported: false, codebaseScanReport: null, integration: args.integration ?? null, + // `--integration` is the person's choice; detection that later finds a framework overwrites it, as `integration`. + projectTraits: args.integration + ? { framework: { value: args.integration, source: 'user' } } + : {}, frameworkContext: {}, typescript: false, detectedFrameworkLabel: null, diff --git a/src/tui/programs/posthog-integration/screens/PostHogIntegrationIntroScreen.tsx b/src/tui/programs/posthog-integration/screens/PostHogIntegrationIntroScreen.tsx index b99123fca..c4cf12dcf 100644 --- a/src/tui/programs/posthog-integration/screens/PostHogIntegrationIntroScreen.tsx +++ b/src/tui/programs/posthog-integration/screens/PostHogIntegrationIntroScreen.tsx @@ -75,7 +75,8 @@ const FrameworkPicker = ({ const integration = Array.isArray(value) ? value[0] : value; void import('@programs').then(({ FRAMEWORK_REGISTRY }) => { const config = FRAMEWORK_REGISTRY[integration]; - store.setFrameworkConfig(integration, config); + // The person picked it, so the framework trait records `user`. + store.setFrameworkConfig(integration, config, 'user'); store.setDetectedFramework(config.metadata.name); onComplete?.(); }); diff --git a/src/tui/store.ts b/src/tui/store.ts index a6f240878..c10059229 100644 --- a/src/tui/store.ts +++ b/src/tui/store.ts @@ -52,7 +52,12 @@ import { scanCodebaseFor, SessionStore, } from '@programs'; -import type { PlannedEvent, TaskItem, WizardSession } from '@programs/types'; +import type { + PlannedEvent, + TaskItem, + TraitSource, + WizardSession, +} from '@programs/types'; import { addTokenUsage, EMPTY_TOKEN_USAGE, @@ -553,8 +558,9 @@ export class WizardStore implements TuiView { setFrameworkConfig( integration: WizardSession['integration'], config: WizardSession['frameworkConfig'], + source?: TraitSource, ): void { - this.sessions.setFrameworkConfig(integration, config); + this.sessions.setFrameworkConfig(integration, config, source); } setDetectionComplete(): void {