Skip to content

feat(wedge): PR-WV1-3 — membership lifecycle + ActionItem CRUD (3a) + MeasurementPlan + Wall (3b)#186

Merged
jukka-matti merged 28 commits into
mainfrom
feat/wedge-pr-wv1-3-measurement-plans
May 16, 2026
Merged

feat(wedge): PR-WV1-3 — membership lifecycle + ActionItem CRUD (3a) + MeasurementPlan + Wall (3b)#186
jukka-matti merged 28 commits into
mainfrom
feat/wedge-pr-wv1-3-measurement-plans

Conversation

@jukka-matti
Copy link
Copy Markdown
Owner

@jukka-matti jukka-matti commented May 16, 2026

Summary

PR-WV1-3 of the wedge V1 implementation. Two phases on one branch: cleanup (3a) shipped first, MeasurementPlan + Wall integration (3b) layered on top. Single squash-merge.


Phase 3a — Invitation lifecycle + ActionItem CRUD (commits be2b508500f7a272)

Invitation lifecycle (closes PR-WV1-1 deferred item a)

  • MembershipAction extended with INVITATION_ACCEPT (composite — synthesizes a ProjectMember from the Invitation via reduceProjectMembers) + INVITATION_REVOKE.
  • useProjectMembershipStore.acceptInvite re-wired: finds the target IP via useImprovementProjectStore.projectsByHub, applies reduceProjectMembers with INVITATION_ACCEPT, dispatches via upsertProject, AND filters from pendingInvites[]. Composite + atomic.
  • Pre-existing barrel gap closed: reduceProjectMembers + MembershipAction + ProjectMemberPatch now re-exported from @variscout/core/projectMembership.

ActionItem CRUD (closes PR-WV1-2 Task 2 deferred)

  • ACTION_ITEM_UPDATE + ACTION_ITEM_REMOVE added to ActionItemAction. New reduceActionItems reducer covers all 3 kinds. REMOVE is soft-delete (sets deletedAt: removedAt).
  • ActionItemPatch omits id | createdAt | deletedAt | parentImprovementProjectId per feedback_action_patch_omit_lifecycle.
  • ImprovementProjectMetadata.actions?: ActionItem[] field added.
  • PWA + Azure <ImprovementView> consumers replaced their console.warn stubs with real dispatches through useImprovementProjectStore.upsertProject(...) + reduceActionItems. Pure buildApplyAction helper extracted.
  • Both apps' persistence applyAction.ts exhaustive switches updated.

PendingInvitesBanner on Home

  • New <PendingInvitesBanner> at packages/ui/src/components/Home/. Renders null when zero invites.
  • Mounted in 3 places: PWA HomeScreen.tsx empty welcome state + PWA App.tsx Home view + Azure Editor.tsx.

Phase 3b — MeasurementPlan sub-entity + Wall integration (commits f6f40507e8e165e8)

Data model

  • New MeasurementPlan entity: hypothesisId, factor, method: 'sensor' | 'manual-count' | 'gemba-walk' | 'expert-assessment' | 'other', sampleSize, owner: ProjectMember['id'], status: 'planned' | 'in-progress' | 'complete' | 'skipped', linkedFindingIds?, msaRequired?. Extends EntityBase.
  • New @variscout/core/measurementPlan sub-path (paired package.json#exports + tsconfig.json#paths).
  • MeasurementPlanAction 4-variant union (ADD / UPDATE / REMOVE / LINK_FINDING) + reduceMeasurementPlans exhaustive reducer. MeasurementPlanPatch = Partial<Omit<MeasurementPlan, 'id' | 'createdAt' | 'deletedAt' | 'hypothesisId'>> per feedback_action_patch_omit_lifecycle.
  • Hypothesis.measurementPlanIds?: string[] added (parallel to findingIds; string[] not MeasurementPlan['id'][] to avoid circular import).
  • HubAction union extended with MeasurementPlanAction. MeasurementPlanReadAPI interface added to HubRepository (get + listByHypothesis).

Persistence

  • Azure Dexie schema v11 → v12; PWA Dexie schema v4 → v5. New measurementPlans table indexed id, hypothesisId, status, deletedAt.
  • Both apps' applyAction.ts extended with 4 MEASUREMENT_PLAN_* cases (before INVESTIGATION_* group). Soft-delete via deletedAt per feedback_strict_assert_over_silent_migration.
  • Both apps' repositories: measurementPlans.get + measurementPlans.listByHypothesis(hypothesisId) filtering deletedAt === null.

UI components (all DOM, mounted via foreignObject)

  • <MeasurementPlanChip> — DOM row with status indicator amber⏳/green✓/gray✗.
  • <AddPlanForm> — inline expansion form; owner picker filters role !== 'sponsor' per spec §"Permissions". Defaults: method='sensor', sampleSize=30, status='planned'.
  • <LinkFindingPicker> — modal-style multi-select; filters hypothesis.findingIds.includes(f.id) && !plan.linkedFindingIds?.includes(f.id). (Finding has no hypothesisId field — spec's example filter adapted.)
  • <HypothesisCardWithPlans> — wraps <HypothesisCard> and renders the plans extension via foreignObject. Strategy B chosen (wrapper, not extend) because HypothesisCard already at 13 props. Local React useState for addPlanFormOpen + linkFindingForPlanId — matches the BrushToFindingFlow pattern. ACL: canEdit = members.length === 0 || (currentUserId !== null && canAccess(currentUserId, members, 'edit-approach')).

Wall + app wiring

  • <WallCanvas> gained optional planningProps: WallCanvasPlanningProps bag (plans, members, currentUserId, 3 callbacks). <DraggableHypothesisCard> conditionally renders HypothesisCardWithPlans vs HypothesisCard.
  • PWA App.tsx + Azure Editor.tsx load plans via useEffect from the new repository accessor, build wallPlanningProps useMemo with MEASUREMENT_PLAN_ADD (id stamped via generateDeterministicId() — never Math.random) + per-id MEASUREMENT_PLAN_LINK_FINDING dispatch + optimistic state updates.
  • onEditPlan = console.warn pass-through (edit UI deferred to V2).

Architectural decision pinned

Task 5 discovery confirmed the spec's §3 "no separate modal, sidebar, or panel" — <HypothesisCard> already uses foreignObject for DOM-in-SVG content (mini-chart, TagChip rows, OneStepAwayBadge); <BrushToFindingFlow> is the inline-confirmation precedent. The earlier "HypothesisDetailPanel" naming in the plan was a transitional artifact. Decision logged in docs/investigations.md 2026-05-16 entry.


V1 limitations carried forward

  • Invitations remain transient (localStorage). Cross-device durable persistence → PR-WV1-5.
  • Per-user useProjectMembershipStore persistence key → PR-WV1-5.
  • <MeasurementPlanChip> edit form: V1 pass-through (console.warn).
  • PWA currentUserId hardcoded 'analyst@local' until real auth wiring lands.

Test plan

  • @variscout/core — 3455/3455 green
  • @variscout/stores — 281/281 green
  • @variscout/pwa mapwall + applyAction targeted — 98/98 green
  • @variscout/azure-app mapwall + applyAction targeted — 123/123 green
  • @variscout/ui InvestigationWall (30 files) — 206/206 green
  • pnpm --filter @variscout/ui build — green (caught and fixed test fixture type drift before push per feedback_ui_build_before_merge)
  • pnpm --filter @variscout/pwa build — green
  • pnpm --filter @variscout/azure-app build — green
  • --chrome browser walk: open Wall → add a Plan on a Hypothesis → mark complete → link to a Finding

Canonical artifacts

  • Design spec: docs/superpowers/specs/2026-05-16-pr-wv1-3-measurement-plans-design.md
  • Plan 3a: docs/superpowers/plans/2026-05-16-pr-wv1-3a-membership-cleanup.md
  • Plan 3b: docs/superpowers/plans/2026-05-16-pr-wv1-3b-measurement-plans-wall.md
  • Wall-surface decision: docs/investigations.md 2026-05-16 entry
  • Master plan: docs/superpowers/plans/2026-05-16-wedge-implementation.md (PR-WV1-3 row)
  • Decision-log: 2 amendments at top of docs/decision-log.md

Next

PR-WV1-4 (canvas response paths 5 → 3 + persona-routing deletion) fans out off main after this lands.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mean-beoynd-lite-pwa Ready Ready Preview, Comment May 16, 2026 8:19pm
variscout_website Ready Ready Preview, Comment May 16, 2026 8:19pm

@jukka-matti jukka-matti changed the title feat(wedge): PR-WV1-3a membership lifecycle + ActionItem CRUD feat(wedge): PR-WV1-3 — membership lifecycle + ActionItem CRUD (3a) + MeasurementPlan + Wall (3b) May 16, 2026
@jukka-matti jukka-matti force-pushed the feat/wedge-pr-wv1-2-improve-workspace branch from eb28ad6 to a6bed5c Compare May 16, 2026 20:06
@jukka-matti jukka-matti changed the base branch from feat/wedge-pr-wv1-2-improve-workspace to main May 16, 2026 20:07
jukka-matti and others added 20 commits May 16, 2026 23:07
…tion

INVITATION_ACCEPT synthesizes a ProjectMember from the invitation's fields
(userId, displayName, role, invitedAt) plus acceptedAt as createdAt, using
generateDeterministicId for the new member id. INVITATION_REVOKE is a no-op
at the members[] level — invitation status transitions are store-layer concern.
…target IP

acceptInvite is now a composite action: it looks up the Invitation, dispatches
INVITATION_ACCEPT through reduceProjectMembers (which synthesizes a ProjectMember
via generateDeterministicId), and calls upsertProject on useImprovementProjectStore
to patch the target IP — all before filtering the invite from pendingInvites.
revokeInvite remains filter-only. Also exports reduceProjectMembers + MembershipAction
from @variscout/core/projectMembership index (they were defined but not re-exported).
PWA: reads pendingInvites/acceptInvite/revokeInvite from
useProjectMembershipStore (selectors) and renders the banner at the top
of HomeScreen — visible to users who haven't loaded data yet.

Azure: same selector pattern; banner mounts in the layout chrome between
InvestigationMetadataPanel and the main tab-content div (visible across
all active views, renders null when no invites).

Tests: new HomeScreen.test.tsx (2 assertions); Azure Editor.test.tsx
extended with 2 banner-integration assertions + ui mock switched to
importOriginal to expose real PendingInvitesBanner.
…Home view

Users with loaded data on the Home tab now see pending invitations;
previously only the empty-state HomeScreen path surfaced the banner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ceActionItems

- Add optional `actions?: ActionItem[]` field to ImprovementProjectMetadata
  (read-write via reduceActionItems; optional preserves existing fixtures)
- Export `reduceActionItems` and `ActionItemPatch` as values from
  @variscout/core/actions barrel so app layers can import without
  reaching into the internal module path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…re.upsertProject

Replace 3 console.warn stubs in PWA ImprovementView and Azure Editor with
real ACTION_ITEM_ADD / UPDATE / REMOVE dispatch:

- Extract `buildApplyAction(activeIP, upsertProject)` pure helper in
  ImprovementView.tsx (exported for unit testing)
- PWA ImprovementView: adds useImprovementProjectStore selector, reads
  actions from activeIP.metadata.actions, calls applyAction on each callback
- PWA App.tsx: drops the now-unnecessary `actions={[]}` prop
- Azure Editor.tsx: inline applyAction pattern mirrors PWA, currentUserId
  from currentUser?.email
- Tests: ImprovementView.applyAction.test.ts covers null-guard, ADD, UPDATE,
  REMOVE without full component rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pplyAction

Both Dexie persistence layers (PWA + Azure) were missing ACTION_ITEM_UPDATE
and ACTION_ITEM_REMOVE cases, causing assertNever() to throw at tsc build
time when the new ActionItemAction kinds (added in Task 2) hit the default
branch. Fixes the build-breaking TS error at applyAction.ts:589.

Also fixes the vitest Mock<> type annotation in ImprovementView.applyAction
.test.ts — vi.fn() typed as ReturnType<typeof vi.fn> failed tsc strict check;
switched to `Mock<(project: ImprovementProject) => void>` from vitest import.
Creates @variscout/core/measurementPlan with MeasurementPlan,
MeasurementMethod, MeasurementPlanStatus types per wedge spec §3.6.3.
Paired package.json#exports + tsconfig.json#paths per CLAUDE.md invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tPlanReadAPI

- Extend HubAction union with MeasurementPlanAction (4 variants)
- Add MeasurementPlanReadAPI interface to HubRepository.ts
- Add measurementPlans field to HubRepository interface
- Export MeasurementPlanReadAPI from persistence barrel
…th apps

- Azure schema v12: add measurementPlans table (id, hypothesisId, status, deletedAt)
- PWA schema v5: add measurementPlans table (same indexes)
- Add MEASUREMENT_PLAN_{ADD,UPDATE,REMOVE,LINK_FINDING} cases to both applyAction.ts
- Add measurementPlans.{get,listByHypothesis} to AzureHubRepository + PwaHubRepository
- TDD: 4 new tests per app (ADD/UPDATE/REMOVE/LINK_FINDING) + exhaustiveness coverage
Pure DOM components (no SVG) for MeasurementPlan display and creation inside
HypothesisCard — TDD with 13 tests (7 chip + 6 form), barrel-exported from
InvestigationWall index. Task 8 will mount via foreignObject.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jukka-matti and others added 8 commits May 16, 2026 23:07
DOM component that filters findings by hypothesis ownership and
not-yet-linked status, then emits onConfirm(ids[]) for Task 8 to
dispatch MEASUREMENT_PLAN_LINK_FINDING. 6 tests green.
…oreignObject

Strategy B wrapper: HypothesisCardWithPlans renders HypothesisCard unchanged
plus a foreignObject extension zone below it for MeasurementPlanChip rows,
+ Add Plan button (ACL-gated), AddPlanForm inline expansion, and
LinkFindingPicker overlay. Callbacks bubble up to parent for dispatch.
18 TDD tests, open-access escape for empty-members V1 scenario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ch (PWA + Azure)

Adds WallCanvasPlanningProps bag to WallCanvas (1 optional prop vs 7 individual), updates
DraggableHypothesisCard to conditionally render HypothesisCardWithPlans, threads planningProps
through InvestigationView (PWA) and InvestigationWorkspace (Azure), and wires MEASUREMENT_PLAN_ADD
+ MEASUREMENT_PLAN_LINK_FINDING dispatch with optimistic state updates in App.tsx and Editor.tsx.
Plans loaded reactively via useEffect from Dexie repository per hypothesis. IDs stamped with
generateDeterministicId(). 56 tests green across ui, pwa, azure-app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ith actual types

Removed excess fields (type/hypothesisId/validationStatus/tags/updatedAt/
investigationId) that Task 8/9a test fixtures incorrectly included; added the
required Finding shape (context, evidenceType, status, comments,
statusChangedAt, investigationId). Caught by pnpm --filter @variscout/ui build
(tsc) per feedback_ui_build_before_merge — tests passed at runtime but the
build's stricter object-literal narrowing rejected excess properties.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tegration)

Pins the Wall-surface inline-only decision and documents the V1 limitations
(no edit form, currentUserId hardcoded for PWA, etc.) so future PRs know
which items still owe forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PendingInvitesBanner project name

- MeasurementPlanChip: add role="button" + aria-label when canEdit=true (ARIA spec permits role="button" on div; no nested-button violation since button is a child element, not a nested button element). Drop both when canEdit=false. TDD: 2 new tests.
- AddPlanForm: filter soft-deleted members (deletedAt !== null) from the eligible owners list alongside the existing sponsor-role filter. TDD: 1 new test.
- PendingInvitesBanner: add optional resolveProjectName prop; render resolved name with UUID fallback. Wire in all 3 mount sites (PWA App.tsx active-IP path, PWA HomeScreen no-data path, Azure Editor.tsx). TDD: 2 new tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…error rollback + plan-load deps key

PWA + Azure parallel changes:
- Fix 1 (PWA only): wrap wallActiveIPMembers in useMemo to stabilize reference and avoid downstream wallPlanningProps useMemo invalidation on every render.
- Fix 4: onLinkFinding optimistic update deduplicates linkedFindingIds — fast double-tap no longer produces phantom duplicate rows. Reducer already deduped; UI now matches.
- Fix 5: plan-loading useEffect deps keyed on hypothesisIds.join('|') instead of the array reference. Each hypotheses store mutation was creating a new array reference → N serial Dexie reads per mutation. Also switched serial for-loop to Promise.all(flat) for cleaner parallel fetch.
- Fix 6: dispatch calls no longer void-ed. onAddPlan rolls back optimistic state on rejection (filter out stamped id). onLinkFinding captures snapshot before update and restores it on rejection. No test — error path; rollback hardening per code review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…2 deferrals

- docs/investigations.md: add 5 entries surfaced by PR-WV1-3 architecture review:
  1. [RESOLVED] HubAction-vs-Project-metadata-patch dispatch rule — own-Dexie-table entities use HubRepository.dispatch; metadata-bag fields use upsertProject. Dead-code ACTION_ITEM_UPDATE/REMOVE in applyAction.ts documented as intentional.
  2. [LOGGED] V2 ACL gap: pendingInvites recipientUserId not filtered per recipient — dormant in V1 single-user PWA; PR-WV1-5 auth-wiring closes it.
  3. [LOGGED] 3 test gaps: MEASUREMENT_PLAN_REMOVE E2E, Dexie upgrade smoke, acceptInvite-missing-project guard.
  4. [LOGGED] PWA_SINGLE_USER_ID: 'analyst@local' hardcoded in 3 places; consolidate to packages/core/src/identity/pwaSingleUser.ts in PR-WV1-5.
- packages/ui/CLAUDE.md: add "Test fixtures" section — require factory functions (createFinding, createHypothesis) over bare typed literals; document that tsc catches drift vitest misses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant