Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/azure/src/persistence/__tests__/applyAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,11 +679,15 @@ function makeMeasurementPlan(id: string, hypothesisId: string): MeasurementPlan
createdAt: NOW,
deletedAt: null,
hypothesisId,
factor: 'X',
outcome: 'Y',
primaryFactor: 'X',
neededFactors: [],
method: 'sensor',
sampleSize: 10,
owner: 'pm-1',
status: 'planned',
scope: [],
processLocation: '',
};
}

Expand Down
6 changes: 5 additions & 1 deletion apps/pwa/src/persistence/__tests__/applyAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,11 +639,15 @@ function makeMeasurementPlan(id: string, hypothesisId: string): MeasurementPlan
createdAt: NOW,
deletedAt: null,
hypothesisId,
factor: 'X',
outcome: 'Y',
primaryFactor: 'X',
neededFactors: [],
method: 'sensor',
sampleSize: 10,
owner: 'pm-1',
status: 'planned',
scope: [],
processLocation: '',
};
}

Expand Down
2 changes: 1 addition & 1 deletion docs/03-features/specifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Both investigation starting points converge on the Wall:
| **Data-first (exploratory)** | Paste → explore in Analyze → notice patterns → create Findings → group into Hypotheses on the Wall. |
| **Hypothesis-first (deductive)** | Open Wall → create Hypothesis → add Measurement Plans (what evidence is needed) → collect out-of-product → re-paste → Findings link to Plans → Hypothesis status progresses. |

Measurement Plan fields (V1): factor, method, sample size, owner, status, hypothesis link, optional linked-findings, optional MSA-required flag (informational only). Formal MSA / Gage R&R workflow + statistical sample-size calculator defer to V2.
Measurement Plan fields (V1 DCP shape — IM-2): `primaryFactor`, `outcome`, `neededFactors[]` (dataset column names — IM-3 join contract), `scope: ConditionLeaf[]` (WHERE snapshot), `processLocation` (ProcessMap step id), `method`, `sampleSize`, `owner`, `status`, `hypothesisId` (required + immutable), optional `opDef`/`msaNote` notes (informational — not gates). `msaRequired` flag removed; formal MSA / Gage R&R workflow defers to V2. Full field reference: [Measurement Plan DCP](workflows/measurement-plan-dcp.md).

---

Expand Down
100 changes: 100 additions & 0 deletions docs/03-features/workflows/measurement-plan-dcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
tier: living
purpose: design
title: Measurement Plan (DCP Shape)
audience: human
category: workflow
status: active
last-reviewed: 2026-05-30
related: [analyze-wall, adr-085, adr-087, hypothesis, process-maps]
layer: L3
kind: ui
serves:
- docs/03-features/specifications.md
- docs/03-features/workflows/analyze-wall.md
---

# Measurement Plan (DCP Shape)

A Measurement Plan is a sub-entity of a Hypothesis on the Investigation Wall. It specifies
**what evidence to collect, how to collect it, under what conditions, and at which process step** —
the DCP (Detection Control Plan) pattern adapted for investigation.

Each Hypothesis can have zero or more Measurement Plans. Plans are created in-line via the
`AddPlanForm` component, rendered as `MeasurementPlanChip` rows below each Hypothesis card on the Wall.

## Field reference (spec §7.1)

| Field | Type | Required | Notes |
| ------------------ | ----------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hypothesisId` | `Hypothesis['id']` | **Required + immutable** | Exclusively links the plan to one Hypothesis. Cannot be patched (excluded from `MeasurementPlanPatch` via `Omit<>`). See decision-log 2026-05-30. |
| `outcome` | `string` | Required | The Y being studied on this plan. Pre-filled from the project/hypothesis outcome at the call site when available; otherwise user-entered. |
| `primaryFactor` | `string` | Required | The primary X being measured. Renamed from `factor` (IM-2). |
| `neededFactors` | `string[]` | Required (may be `[]`) | **Values are dataset column names** (not display labels). Stratifiers or covariates to collect alongside `primaryFactor`. IM-3's column-overlap matcher joins on these names — preserving this contract is critical. |
| `sampleSize` | `number` | Required | Minimum planned sample count. |
| `method` | `MeasurementMethod` | Required | One of: `sensor`, `manual-count`, `gemba-walk`, `expert-assessment`, `other`. |
| `owner` | `ProjectMember['id']` | Required | Entity id of the responsible member (Lead or Member; Sponsors are excluded from the owner picker). |
| `status` | `MeasurementPlanStatus` | Required | Lifecycle state: `planned` → `in-progress` → `complete` or `skipped`. |
| `scope` | `ConditionLeaf[]` | Required (may be `[]`) | A **snapshot copy** of the active WHERE drill-chip conditions at plan-creation time (ADR-085). Derived from `analysisScopeStore.categoricalFilters` via `buildConditionFromCategoricalFilters`. This is **not** a reference to the `ProblemStatementScope` entity — it is a captured point-in-time WHERE clause. Empty when no drill chips are active. |
| `processLocation` | `string` | Required (may be `''`) | ProcessMap node id (`step-${slug}-${seq}`) identifying the process step this plan targets. Resolved against canonical ProcessMap node ids (ADR-087). Empty string `''` is allowed for mapless projects — ADR-087 tolerates orphaned/empty `stepId` values pre-launch. |
| `opDef` | `string?` | Optional | Free-text operational-definition note — **informational only, not a maturity gate**. |
| `msaNote` | `string?` | Optional | Free-text MSA / Gage R&R comment — **informational only, not a gate**. Replaces the removed `msaRequired: boolean` flag (IM-2). Formal MSA / Gage R&R workflow defers to V2. |
| `linkedFindingIds` | `Finding['id'][]?` | Optional | Finding ids linked to this plan via `MEASUREMENT_PLAN_LINK_FINDING`. |

## DCP contract notes

- **`neededFactors[]` = dataset column names.** This is a hard contract: IM-3's
column-overlap matcher (`packages/core/src/...`) joins `neededFactors` against
the dataset's column keys. Do not store display labels here.

- **`processLocation` join.** ProcessMap node ids are canonical (`step-${slug}-${seq}`,
minted by `canvasStore`). `processLocation` is an intentionally non-strict FK (ADR-087):
orphaned or empty values are tolerated pre-launch so the form can be submitted without
a configured processMap.

- **`scope` is a snapshot, not a live filter.** The `ConditionLeaf[]` is captured once
at plan-creation time from `analysisScopeStore.categoricalFilters`. After creation the
plan's `scope` does not update when the drill chips change. This is intentional — the
scope documents the WHERE at the time the measurement commitment was made.

- **`opDef` and `msaNote` are optional notes.** VariScout V1 treats these as informational
context that improves communication across the team. They are **not** maturity gates,
not required for plan status progression, and not validated against any schema beyond
`string`.

## Persistence

Plans are stored in the `measurementPlans` Dexie table (both PWA and Azure apps), indexed
on `id`, `hypothesisId`, `status`, and `deletedAt`. The new DCP fields (`outcome`,
`neededFactors`, `scope`, `processLocation`, `opDef`, `msaNote`) are non-indexed object
fields — Dexie stores them freely without an IDB version bump. No migration is required
for existing rows (wedge = no existing production rows).

The reducer (`reduceMeasurementPlans` in `packages/core/src/measurementPlan/actions.ts`) is
a pure spread-merge — it requires no changes when new fields are added to `MeasurementPlan`
(the `MeasurementPlanPatch` type auto-widens via `Partial<Omit<...>>`).

## UI threading

```
WallCanvas (planningProps bag)
└─ derives stepOptions from processMap via deriveProcessSteps()
└─ forwards defaultScope? + defaultOutcome? from planningProps
└─ HypothesisCardWithPlans
└─ AddPlanForm
scope = defaultScope ?? []
processLocation = selected stepOption.id ('' if none / no options)
outcome = user input || defaultOutcome || ''
```

Call sites that cannot cheaply source `defaultScope` or `defaultOutcome` pass `undefined`;
the form defaults to `[]` / `''` respectively.

## Related decisions

- [ADR-085](../../07-decisions/adr-085-drop-question-problem-statement-scope.md) — `ConditionLeaf`
scope capture (WHERE snapshot).
- [ADR-087](../../07-decisions/adr-087-process-step-model-reconciliation.md) — `processLocation`
non-strict join; `deriveProcessSteps` as the single read source.
- [Decision log 2026-05-30](../../decision-log.md) — `hypothesisId` stays required + immutable;
`neededFactors[]` = dataset column names contract pinned.
2 changes: 2 additions & 0 deletions docs/decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ When in doubt: capture, don't invent. Record the decision; link to its source ar

Decisions we keep relitigating. Each entry: short statement, rationale, closing artifact, date pinned.

- **2026-05-30 — IM-2: `MeasurementPlan.hypothesisId` stays required + immutable; `neededFactors[]` = dataset column names contract.** `decision`: Spec §11 open-Q #2 resolved. `hypothesisId` is kept required on `MeasurementPlan` (no "bare condition" plan without a Hypothesis); it is excluded from `MeasurementPlanPatch` via `Omit<>` (type-level enforcement). No plan-creation path exists without a Hypothesis, so the weaker "optional at creation, set later" design adds complexity for zero V1 benefit. `neededFactors: string[]` values are **dataset column names** (not display labels) — IM-3's column-overlap matcher joins on these keys. Changing to display labels would silently break the IM-3 join. Related: [measurement-plan-dcp L3 doc](03-features/workflows/measurement-plan-dcp.md), spec §7.1, ADR-085, ADR-087. _Pinned 2026-05-30._

- **2026-05-30 — IM-1 shipped (PR #249); CoScout `legacy.ts` retirement (complete ADR-068) scheduled as a Wave-1 PR, must precede IM-3.** `decision`: IM-1 (drop `Question` entity + `ProblemStatementScope` first-class) merged as an atomic cascade (core/stores/hooks/ui/apps/data/i18n + residual cleanup + 6-package test layer), preceded by Bucket-2 pre-existing-fix PR #245 for a green baseline. Its adversarial review surfaced that `packages/core/src/ai/prompts/coScout/legacy.ts` (1647 lines, the pre-ADR-068 monolith) is a **stalled deprecation**: `buildCoScoutSystemPrompt`/`buildCoScoutMessages` have ZERO production callers (live path is `assembleCoScoutPrompt` via `tools/registry.ts`), but the dead code is kept alive by backward-compat tests (`promptTemplates.test.ts`/`promptSafety.test.ts`), forcing dual-maintenance + duplicated tool defs (IM-1 paid the tax: stale Question prompt copy + a registry/legacy split that turned a 3-line tool removal into a multi-file reconciliation). **Decision:** complete the ADR-068 migration as a dedicated Wave-1 PR (disjoint `coScout/` subtree — no conflict with IM-2/5/7/0b-2): relocate the still-live utils (`formatKnowledgeContext` ×7 callers; audit `buildCoScoutInput`/`buildCoScoutTools`) into the modular tree, move any genuine prompt-safety contract coverage onto `assembleCoScoutPrompt`, then delete the dead monolith + its backward-compat tests. **Must land before IM-3** (auto-link is CoScout-adjacent). Why: a stalled deprecation (dead code pinned by its own tests + a duplicated source of truth) costs more than completing the migration, and doing it before the CoScout-touching wave PRs avoids re-paying the tax. Build-state + tracked deferrals: [[investigation-surface-build]]; IM-1 follow-ups in [`ephemeral/investigations.md`](ephemeral/investigations.md). Related: [adr-068](07-decisions/adr-068-coscout-cognitive-redesign.md), [[investigation-model-design]]. _Pinned 2026-05-30._

- **2026-05-29 — Investigation-model design graduated → spec + 5 ADRs + master plan; four review calls locked.** `new spec` + `new ADR`: promoted the 2026-05-29 Clusters A/B/C investigation-model design from a decision-log candidate to [`superpowers/specs/2026-05-29-investigation-surface-design.md`](superpowers/specs/2026-05-29-investigation-surface-design.md) + ADRs [085](07-decisions/adr-085-drop-question-problem-statement-scope.md)–[089](07-decisions/adr-089-retire-mode-lens-user-axis.md) + [master plan](superpowers/plans/2026-05-29-investigation-surface-master-plan.md) (IM-0a…IM-7). Grounded against source via 8 grounding agents (refined the design: three step homes not two; `ProblemCondition`/`ScopeFilter` name-collisions → new `ProblemStatementScope`; the What-If chain already exists; 8f LOD ≠ factor-family coarsening; no `SuspectedCause` type — `stores/CLAUDE.md` stale; `isPaidTier` fully deleted). Initial commit `04854150`; investigations.md entry marked `[PROMOTED 2026-05-29]`.
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/measurementPlan/__tests__/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ const basePlan: MeasurementPlan = {
createdAt: 100,
deletedAt: null,
hypothesisId: 'h-1',
factor: 'Nozzle temperature',
outcome: 'Fill Weight',
primaryFactor: 'Nozzle temperature',
neededFactors: [],
method: 'sensor',
sampleSize: 50,
owner: 'pm-1',
status: 'planned',
scope: [],
processLocation: '',
};

describe('reduceMeasurementPlans — MEASUREMENT_PLAN_ADD', () => {
Expand Down Expand Up @@ -45,7 +49,7 @@ describe('reduceMeasurementPlans — MEASUREMENT_PLAN_UPDATE', () => {
});

it('leaves non-matching plans unchanged', () => {
const otherPlan: MeasurementPlan = { ...basePlan, id: 'mp-2', factor: 'Other' };
const otherPlan: MeasurementPlan = { ...basePlan, id: 'mp-2', primaryFactor: 'Other' };
const next = reduceMeasurementPlans([basePlan, otherPlan], {
kind: 'MEASUREMENT_PLAN_UPDATE',
planId: 'mp-1',
Expand All @@ -65,7 +69,7 @@ describe('reduceMeasurementPlans — MEASUREMENT_PLAN_REMOVE', () => {
expect(next).toHaveLength(1);
expect(next[0].deletedAt).toBe(200);
expect(next[0].id).toBe('mp-1');
expect(next[0].factor).toBe(basePlan.factor);
expect(next[0].primaryFactor).toBe(basePlan.primaryFactor);
});

it('does not mutate input', () => {
Expand Down Expand Up @@ -120,7 +124,8 @@ describe('MeasurementPlanPatch', () => {
const _patch3: MeasurementPlanPatch = { deletedAt: 999 };
// @ts-expect-error hypothesisId in Omit list
const _patch4: MeasurementPlanPatch = { hypothesisId: 'h-other' };
// Allowed: status, factor, sampleSize, owner, method, linkedFindingIds, msaRequired
// Allowed: status, primaryFactor, outcome, neededFactors, sampleSize, owner, method,
// scope, processLocation, opDef, msaNote, linkedFindingIds
const _patch5: MeasurementPlanPatch = { status: 'complete', sampleSize: 100 };
expect(true).toBe(true);
});
Expand Down
Loading