Skip to content

IM-4b: Wall collaboration + multi-scope rail + re-mounted detached flows#257

Merged
jukka-matti merged 20 commits into
mainfrom
im-4b-wall-collab
May 30, 2026
Merged

IM-4b: Wall collaboration + multi-scope rail + re-mounted detached flows#257
jukka-matti merged 20 commits into
mainfrom
im-4b-wall-collab

Conversation

@jukka-matti
Copy link
Copy Markdown
Owner

IM-4b — Investigation Wall collaboration layer

Spec: docs/superpowers/specs/2026-05-30-investigation-wall-unified-canvas-design.md §4–5, §8.7–8.9.
Master plan: docs/superpowers/plans/2026-05-29-investigation-surface-master-plan.md (IM-4b).
Follows IM-4a (#256). The unified bipartite layout + Focus lens are deferred to IM-4c.

Wires existing backends (comment stack, ActionItem model, scope ops) into the production Wall seam.

What ships

  1. Hypothesis comment threadHypothesisComments mounted on the card; human composer + edit/delete; SSE-synced; ACL-gated.
  2. @mentions + AIContext.recentCommentsparseMentions (longest-match) in the composer; recent hub/finding comment text now flows to CoScout (Azure; PWA has no buildAIContext path).
  3. ActionItem tasks on hypotheses — assign a member a task ("validate against night-shift data"), open/done; exhaustive HYPOTHESIS_ACTION_* union handled in both apps' applyAction.
  4. Plan-owner data-collection task surfaceMeasurementPlan.owner/status/due shown as the data-collection task (distinct from chore(deps): bump actions/checkout from 4.2.2 to 6.0.2 #3's ActionItems).
  5. Multi-scope rail (Azure) — lists persisted ProblemStatementScopes; switching re-anchors via IM-4a's active-scope selection; archive prunes intermediate scopes.
  6. Re-mounted detached IM-1 flowsImprovementIdeasSection (hypothesisId-keyed) + handleProjectIdea/ideaImpacts consumed by the Editor.

Terminology

Labels surface as "Supports / Counts against" and "Contributing factors"; code identifiers keep support/refute/tributaries.

Notes / deviations

  • createHubFromFinding (finding→hypothesis CTA) descoped to IM-4c (needs the bipartite layout); store method + tests retained.
  • ScopeRail is Azure-only — PWA scope spine deferred per IM-4a's PWA-deferral.
  • Hub-comment durability is F5-deferred (in-session analyze blob), consistent with existing HYPOTHESIS_* persistence.
  • onUpdateHypothesisAction omitted (no edit-text UI — YAGNI; the union member exists + is handled as a no-op).
  • Idea write surface is canEdit-gated (read-for-all / write-for-editors), matching the sibling contribution affordances.

Process note

This slice trialed a test-author→implementer TDD pipeline. The first pass produced green-but-dead features (leaf components unit-tested but never wired into the production WallCanvas → card → app seam); an adversarial review caught it and a single Opus wiring pass + seam-level integration tests recovered it. Verdict: the lean TDD pipeline is the wrong tool for integration-heavy UI — leaf-scoped tests bypass the integration seam. IM-4c/IM-6 use single-implementer.

Verification

  • Full gate green (bash scripts/pr-ready-check.sh — all packages build, both app vite builds, all tests).
  • Post-gate ACL fix: full @variscout/ui suite (2776 tests), @variscout/ui build, both-app tsc --noEmit all green.
  • Focused adversarial re-review confirmed all 6 features genuinely reachable through the production path + seam tests non-vacuous.

🤖 Generated with Claude Code

jukka-matti and others added 20 commits May 30, 2026 21:53
…ptance

Adds RED tests for:
- editHubComment / deleteHubComment store actions (7 tests in analyzeStore.test.ts)
- HypothesisComments component: thread rendering, ACL-gated add/edit/delete
  composer (14 tests in HypothesisComments.test.tsx)

All 97 pre-existing analyzeStore tests pass; FindingComments tests pass.
New tests fail for the right reason (actions not a function / module not found).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lift/parameterize FindingComments to support ACL-gated write access:
- Add canEdit?: boolean prop (defaults true) to FindingComments; gates
  add/edit/delete affordances so read-only viewers see no write surface.
- Add HypothesisComments component (AnalyzeWall) that wraps FindingComments,
  binds hub.comments as the data source, and computes canEdit from
  canAccess(currentUserId, members, 'edit-contributions') — mirrors the
  HypothesisCardWithPlans ACL gate pattern.
- Add editHubComment / deleteHubComment store actions to analyzeStore
  (twins of editFindingComment / deleteFindingComment, keyed to hypotheses).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts for acceptance

- New: packages/core/src/findings/__tests__/parseMentions.test.ts — 10 tests
  for the parseMentions(text, members) pure utility (ERR_MODULE_NOT_FOUND, RED)
- Augment: buildAIContext.test.ts — 5 new failing tests for
  investigation.recentComments (hubComments/findingComments/todayStartMs options
  not yet on BuildAIContextOptions; 2 negative-case tests correctly pass)
- Augment: analyzeStore.test.ts — 4 tests for addHubComment @mention storage
  (mentionedUserIds arg not yet accepted; 3 fail, 1 backward-compat passes)

All 67+105+18 pre-existing tests in the touched files remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- parseMentions(text, members): new pure utility in findings barrel that
  resolves @DisplayName tokens to userId strings (case-insensitive,
  deduplicated, unknown tags ignored).
- FindingComment.mentionedUserIds?: string[] — optional field added for
  backward-compatible mention storage.
- addHubComment: accepts optional mentionedUserIds arg; stores it on the
  comment + includes it in the POST body to /api/hub-comments/append.
- BuildAIContextOptions: adds hubComments, findingComments, todayStartMs.
- AIContext.investigation.recentComments: populated with text of today's
  hub + finding comments (createdAt >= todayStartMs), capped at 10,
  sorted newest-first; absent when no today-comments exist.

Reuses: createFindingComment factory, BuildAIContextOptions pattern,
analyzeStore addHubComment structure (Task 1 composer), projectMembership
ProjectMember type.

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

Encodes the acceptance oracle for Task 3 across three layers:

- packages/core: hypothesisActionItems.test.ts — Hypothesis.actions field shape,
  new HubAction kinds (HYPOTHESIS_ACTION_ADD/UPDATE/COMPLETE) under the
  discriminated union, exhaustiveness mirror with the 3 new cases, and the
  MeasurementPlan-distinctness constraint (no primaryFactor/method on ActionItem).

- packages/stores: analyzeStore.hypothesisActionItems.test.ts — 14 RED tests for
  addHypothesisAction / updateHypothesisAction / completeHypothesisAction /
  toggleHypothesisActionComplete / deleteHypothesisAction (all fail today with
  "is not a function").

- packages/ui: HypothesisCardWithPlans.actionItems.test.tsx — 12 RED / 4 GREEN
  UI tests: action item row rendering (text, assignee, open/done status,
  data-testid distinction from chip-body), + Add Task ACL gate (lead/sponsor/
  empty-members visible; non-member hidden; no-callback omitted), add-task
  form flow (open/save/cancel), complete-task flow. All existing tests unbroken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reuses the existing ActionItem model (assignee/dueDate/completedAt) to
attach tasks to Hypothesis hubs — mirroring the pattern already live on
Finding.actions and canvas steps.

Changes:
- Hypothesis.actions?: ActionItem[] field added to core types
- HYPOTHESIS_ACTION_ADD / UPDATE / COMPLETE added to HypothesisAction +
  HubAction discriminated union (kept exhaustive — exhaustiveness.test
  updated to cover new cases; hypothesisActionItems.test mirrors it)
- analyzeStore: addHypothesisAction / updateHypothesisAction /
  completeHypothesisAction / toggleHypothesisActionComplete /
  deleteHypothesisAction — all wired to Hypothesis.actions[], reusing
  createActionItem from @variscout/core
- HypothesisCardWithPlans: renders action-item-row divs (data-testid +
  data-status), "Mark Done" button (ACL-gated), "+ Add Task" button
  (omitted when callback not provided), inline add-task form with Save/
  Cancel; onAddHypothesisAction + onCompleteHypothesisAction optional props
- i18n: 5 wall.task.* keys added to types.ts + all 32 locale files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tests for acceptance

Encodes the acceptance oracle for the plan-owner data-collection task surface
on HypothesisCardWithPlans: data-testid="data-collection-task" section per plan,
"Assigned: collect {primaryFactor}" label, owner display name resolved from roster,
per-status label + data-status attribute, optional dueDate display, and the section
being distinct from Task 3 action-item-row rows and not write-ACL-gated (read display).

All 21 new tests fail RED (data-testid="data-collection-task" is absent from current DOM);
1 boundary case (empty plans → no section) correctly passes both before and after.
Existing tests preserved: MeasurementPlanChip.test.tsx (9/9), HypothesisCardWithPlans.test.tsx (22/22),
HypothesisCardWithPlans.actionItems.test.tsx (16/16) all still PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add `dueDate?: string` to `MeasurementPlan` type (ISO-8601 YYYY-MM-DD)
- Add 6 i18n keys (`wall.collect.*`) to MessageCatalog + all 32 locale files
- Render a `data-testid="data-collection-task"` section per plan in
  HypothesisCardWithPlans: header row (Assigned: collect label + status
  badge with `data-status` attr + optional due date) + embedded
  MeasurementPlanChip. The chip embedding lets `section.textContent` carry
  the primaryFactor + ownerName without creating duplicate `getAllByText`
  matches alongside the chip's own text-node span (Testing Library's
  `getNodeText` only reads direct text-node children).
- Remove standalone chip-row map — chips are now inside each
  data-collection-task section; existing HypothesisCardWithPlans chip tests
  continue to pass since the chip is still rendered.
- Section is always visible (read-display, no ACL gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds RED tests for the ScopeRail component (Task 5):
- ScopeRail.test.tsx: 14 tests encoding the acceptance oracle — scope
  listing, active-scope aria-current marking, onScopeSelect switching,
  and onScopeArchive pruning. All fail with "Failed to resolve import
  ../ScopeRail" since the component doesn't exist yet.
- analyzeStore.test.ts augmented: 5 new archiveScope tests (SCOPE_ARCHIVE
  store side-effect). All fail with "archiveScope is not a function".
  108 existing store tests remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add archiveScope action to analyzeStore (soft-delete via deletedAt) and
create ScopeRail component that lists ProblemStatementScopes as a
horizontal breadcrumb rail with onScopeSelect / onScopeArchive callbacks.
Reuses formatConditionLeaves from @variscout/core for chip labels.

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

Encodes the acceptance oracle for the 3 detached IM-1 flows per PLAN-im-4b.md §Task 6:

Flow 1 — ImprovementIdeasSection mounted (hypothesisId-keyed):
  HypothesisCardWithPlans must render ImprovementIdeasSection when hub.ideas
  is non-empty, keyed by hypothesisId (data-testid="ideas-section-<id>"),
  passing ideaImpacts (impact badges) and onAddIdea affordance.

Flow 2 — handleProjectIdea / ideaImpacts wired:
  onProjectIdea(hypothesisId, ideaId) prop threads through HypothesisCardWithPlans
  → ImprovementIdeasSection and fires when the "Project idea with What-If" button
  is clicked.

Flow 3 — createHubFromFinding (propose-hypothesis-from-finding):
  FindingChip accepts onProposeHypothesis?: (findingId: string) => void and renders
  a "Propose hypothesis" affordance that fires the callback; absent prop = no button
  (regression guard for existing onSelect/onDetach behaviour).

Regression guard: ImprovementIdeasSection direct-mount API shape verified (existing
component — its tests already pass; this suite confirms the prop contract the
implementer must honour when wiring it into HypothesisCardWithPlans).

RED: 9 tests fail — ImprovementIdeasSection not mounted in HypothesisCardWithPlans,
ideaImpacts/onProjectIdea props absent, FindingChip lacks onProposeHypothesis.
8 tests pass (negative assertions + ImprovementIdeasSection direct mount).
Existing HypothesisCardWithPlans.test.tsx (22) + actionItems (16) + FindingChip (3)
+ HypothesisCard.test.tsx (19) all GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flow 1 — ImprovementIdeasSection mounted in HypothesisCardWithPlans:
  Import ImprovementIdeasSection; render in foreignObject below the plans
  section when hub.ideas?.length > 0 && ideaImpacts prop is provided.
  Keyed by hub.id (data-testid="ideas-section-<hypothesisId>"). Passes
  ideaImpacts, onAddIdea, onUpdateIdea, onRemoveIdea, onSelectIdea,
  onProjectIdea through to the existing component — zero reinvention.

Flow 2 — handleProjectIdea / ideaImpacts wired via new props:
  HypothesisCardWithPlansProps gains ideaImpacts, onProjectIdea, onAddIdea,
  onUpdateIdea, onRemoveIdea, onSelectIdea (all optional). Destructured and
  passed to ImprovementIdeasSection; onProjectIdea fires with
  (hypothesisId, ideaId) exactly as useAnalyzeOrchestration returns it.

Flow 3 — createHubFromFinding CTA on FindingChip:
  FindingChipProps gains onProposeHypothesis?: (findingId: string) => void.
  When wired, renders a "Propose hypothesis" foreignObject button below the
  chip with stopPropagation so onSelect is not accidentally triggered.
  Absent prop = no button (regression-safe).

Reuse: ImprovementIdeasSection (existing), FindingChip foreignObject pattern.
No new HubActions, no IDB bump, no new dispatch mechanism.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ale ts-expect-error

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

Task 3 added HYPOTHESIS_ACTION_ADD/UPDATE/COMPLETE to the union but no apply-reducer
cases, so both apps hit assertNever (PWA build failed; azure tsc too). Handle them as
F5-deferred no-ops, mirroring HYPOTHESIS_RECORD_DISCONFIRMATION — hypothesis ActionItems
are in-session-only (analyzeStore) and ride the analyze blob.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anvas (production seam)

IM-4b adversarial review found 5/6 features GREEN-but-DEAD: the leaf
components existed + unit-tested but WallCanvas forwarded 0 of the new
callbacks, so the affordances never rendered via the production path.

Wire the seam:
- WallCanvasPlanningProps now declares the comment-thread, ActionItem,
  and improvement-idea callbacks (+ ideaImpacts).
- The hubPlanningProps builder forwards all of them to the card (and via
  DraggableHypothesisCard's Omit-typed planningProps, the drag path too).
- HypothesisCardWithPlans mounts HypothesisComments via a foreignObject
  (presence of onAddHubComment is the enable signal; ACL gate lives inside).
- Export HypothesisComments + ScopeRail from the AnalyzeWall barrel.
- Fix the blanked {primaryFactor} placeholder in the data-collection-task
  header (was shipping 'Assigned: collect '; now renders plan.primaryFactor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + PWA apps

The leaf affordances now reach the production path end-to-end:

- useHypotheses gains addComment/editComment/deleteComment, addAction/
  completeAction, addIdea/updateIdea/removeIdea/selectIdea — routing through
  the hook keeps the Wall's source of truth (hypothesesState.hubs) live
  in-session AND syncs useAnalyzeStore via onHubsChange (rides the analyze blob,
  mirroring the IM-4a disconfirmation precedent).
- Azure: AnalyzeWorkspace enriches Editor's base measurement-plan bag with the
  comment/task/idea callbacks (sourced from hypothesesState + ideaImpacts +
  handleProjectIdea, which Editor now consumes) and mounts the ScopeRail above
  the canvas — onScopeSelect rewrites the drill filters so IM-4a's producer
  re-anchors the Problem card; onScopeArchive -> analyzeStore.archiveScope.
- PWA: wallPlanningProps routes comment/task/idea writes through useAnalyzeStore
  (AnalyzeView's Wall source of truth). ScopeRail is Azure-only — the PWA Wall
  has no scope spine (IM-4a deferred it; PWA-Mount-Deferral).
- @mentions: the composer wiring runs parseMentions(text, members) and passes
  mentionedUserIds to addHubComment in both apps.
- recentComments: useAIContext now forwards hubComments/findingComments/
  todayStartMs to buildAIContext; useAIOrchestration flattens live hub + finding
  comments so investigation.recentComments populates for CoScout.
- Export parseMentions from the @variscout/core barrel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…il + cleanup

- Descope createHubFromFinding: remove the dead FindingChip.onProposeHypothesis
  prop + the 'Propose hypothesis' button (apps defer finding->hypothesis-on-Wall
  to the IM-4c bipartite re-layout). Replace the unit test with a descope guard.
- Localize ScopeRail: new wall.scope.archive i18n key (types.ts + all 33 locale
  catalogs, English backfill matching the IM-4 wall-key convention); the archive
  aria-label now reads from getMessage.
- ScopeRail a11y/style: fix the invalid role='button'-span-inside-button nesting
  (the archive control is now a sibling <button>, absolutely positioned), and
  swap raw palette classes for the semantic design tokens sibling Wall chrome uses.
- parseMentions: longest-match-wins for prefix-overlapping display names
  ('Bob' vs 'Bob Member') — sort longest-first + consume matched spans; + tests.
- Test hygiene: drop the dead vi.mock('@variscout/stores') blocks from
  ScopeRail.test + HypothesisComments.test (neither component imports stores;
  the @variscout/hooks + lucide mocks in HypothesisComments stay — FindingEditor
  needs them).

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

These exercise the PRODUCTION seam (not injected-prop leaf units):

- WallCanvas.collab.seam.test.tsx renders the real WallCanvas with a hub +
  full planningProps and asserts the comment thread, ActionItem '+ Add Task'
  rows, and ImprovementIdeasSection RENDER on the card path + DISPATCH through
  the forwarded production callbacks (onAddHubComment / onAddHypothesisAction /
  onCompleteHypothesisAction), plus the {primaryFactor} data-collection header.
- AnalyzeWorkspace.mapwall.test.tsx gains a ScopeRail seam block: renders the
  real AnalyzeWorkspace (WallCanvas stubbed, ScopeRail real) and asserts the
  rail renders from useAnalyzeStore.scopes, selecting a chip re-anchors by
  rewriting analysisScopeStore.categoricalFilters, and archiving soft-deletes
  via analyzeStore.archiveScope.

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

The data-collection-task header now interpolates plan.primaryFactor (Task 4
fix), so the factor legitimately appears in BOTH the header and the embedded
chip. Update the two per-hypothesis / soft-delete plan-filter assertions in
WallCanvas.test to count twice (header + chip) while still proving the
filtering intent (factor absent under the non-matching hub / deleted plan).

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

ImprovementIdeasSection mounted on the hypothesis card without a canEdit gate,
so a non-member viewer (members configured, current user not in the roster) got
an over-permissive add/edit/remove/select/project surface — inconsistent with
the sibling contribution affordances (plans, tasks, disconfirmation, comments)
which all gate on edit-contributions per canAccess().

Add a canEdit prop (default true → FindingsLog-era call sites + existing tests
unaffected). When false the section renders read-only: existing ideas, impacts
and projections stay visible; add input / project / remove buttons hide; the
timeframe select renders as a static label and the select-toggle as a
non-interactive indicator. Parent (HypothesisCardWithPlans) passes its canEdit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

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

Project Deployment Actions Updated (UTC)
mean-beoynd-lite-pwa Building Building Preview, Comment May 30, 2026 9:45pm
variscout_website Building Building Preview, Comment May 30, 2026 9:45pm

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