Skip to content

feat(strategypro): reverse-engineer flow — Outcomes pilot (submodule bump + tests)#3651

Open
gloriafolaron wants to merge 41 commits into
masterfrom
feat/logic-model-reverse
Open

feat(strategypro): reverse-engineer flow — Outcomes pilot (submodule bump + tests)#3651
gloriafolaron wants to merge 41 commits into
masterfrom
feat/logic-model-reverse

Conversation

@gloriafolaron

Copy link
Copy Markdown
Contributor

Summary

Core companion to Leantime/plugins#74 — bumps the StrategyPro submodule pointer to the plugin's feat/logic-model-reverse HEAD and lands unit tests for the reverse flow's read half.

What's in the PR

  • Submodule pointer bump for app/Plugins.
  • tests/Unit/app/Plugins/StrategyPro/Services/WorkAdapterTest.php — 7 tests, 20 assertions covering the four highest-regression-risk behaviors on WorkAdapter:
    • Empty strategy → empty completeness.
    • All goals hidden → "already reflected" completeness.
    • Metric-based classification splits with-target from no-target.
    • Percent goal with endValue=0 is treated as no-target (avoids "0% of 0" nonsense).
    • Hidden goals are removed from candidates but counted honestly.
    • Sparse-completeness message has no unfilled sprintf placeholders.
    • Scope shape passes through cleanly.

The plugin PR carries the actual services, HxControllers, and templates. See there for the full spec + verification checklist.

Merge order

  • Plugin PR merges to plugins:main.
  • This PR's submodule pointer bumps forward to the final plugin merge commit before the core PR merges.
  • No core code changes here — pointer bump + tests only.

Test plan

  • ./vendor/bin/phpunit tests/Unit/app/Plugins/StrategyPro/Services/WorkAdapterTest.php — 7 tests pass.
  • Pint + PHPStan clean on the test file.
  • Manual QA of the reverse flow per the plugin PR checklist.

🤖 Generated with Claude Code

marcelfolaron and others added 30 commits July 13, 2026 13:35
Board-packet style reporting built for cross-level rollups (#reporting):

- New ReportEngine service/repository in Reports: session-free, bulk,
  project-id-set based queries powering project/plan/strategy reports
  (milestone buckets w/ history-derived completion dates, goal rollups,
  status updates, needs-attention, due-date slippage, effort, deltas)
- ReportPeriod value object: quarter presets in user TZ, custom ranges,
  prior-period math, shareable query strings
- New /reports/project status report screen (HTMX period swap,
  print-optimized); legacy /reports/show stays as Delivery Metrics tab
- zp_tickets.outcomeImpact: retrospective milestone outcome narrative,
  captured inline on the report and editable in the milestone dialog
- zp_goal_history + write hooks + daily snapshot: goal values become
  chartable over time (previously overwritten in place)
- Shared report partials (reports::partials.*) + <x-global::periodpicker>
  consumed by the PgmPro/StrategyPro report screens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design pass on the report family (feedback: milestones were buried in
table cells and the boxedHighlight tiles / dense tables were hard to parse):

- New reports.css component styles: quiet card-based stat tiles (muted
  label, semibold value, directional deltas), disciplined tables (muted
  headers, tabular right-aligned numbers, row separators), card-based
  milestone rows, calmer needs-attention treatment (left accent border
  instead of a red-tinted box)
- Plan report is milestone-first: completed (with outcomes + task
  drill-down), in-flight, and coming-up milestone sections across all
  child projects lead the page; the project matrix shrinks to a compact
  per-project summary (status, progress, next milestone, goals, hours)
- Strategy report gains a 'Milestones & outcomes this period' list
  (outcome statements, no tasks)
- Numbers trimmed via Number::format (18 not 18.00); goal metrics read
  '18 of 40 graduates'; empty linked-milestone column drops out
- Stat tile deltas are semantic: direction x whether up is good

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ects, shared blade primitives

Introduces app/Core/Resources — a small contract layer that lets a
plugin provide Resources data (people allocations, budget lines,
dependencies at the program level) to the rest of the app without core
knowing which plugin is providing.

Follows the WorkStructure precedent verbatim: core owns the contract
+ registry + typed value objects; the plugin (PgmPro, in the companion
plugin PR) owns the concrete implementation, storage, and UI.

What's in this PR
─────────────────

app/Core/Resources/
  Contracts/ResourcesGateway
    Interface with two methods (getForProjects, getForProgram) that
    every consumer of Resources data goes through. Returns typed
    ResourceSummary objects, not raw arrays.
  Models/ResourceSummary + PersonAllocation + BudgetLine + Dependency
    Immutable value objects. ResourceSummary::empty() distinguishes
    "plugin registered but nothing authored" from "no plugin
    registered" (which is null from the registry).
  Services/ResourcesRegistry
    Singleton. First plugin to register wins; a second registration
    from a different class is logged and refused — silently
    overwriting would produce a data-source ambiguity nobody notices
    until reports disagree.
  ResourcesServiceProvider
    Binds the registry as a singleton. Nothing seeded — an install
    without a Resources plugin gets a registry that resolves to null,
    which every consumer handles as the honest "not installed" state.

app/Views/Templates/components/ (4 shared Blade primitives)
  avatar-stack   — overlapping avatar row
  collapsible    — animated collapsing section
  metric-cell    — label + value + optional trend
  proportion-bar — segmented capacity/budget bar
  These were phase-0 resource-adjacent but are genuinely reusable
  (the reporting design will consume them for tables/tiles too).
  Tailwind v4 tw: prefix converted to master's v3 tw-.

Wired into app/Core/Configuration/laravelConfig.php next to the
WorkStructure provider (same layer of the stack).

Unit tests
──────────
tests/Unit/app/Core/Resources/
  ResourcesRegistryTest    — null-when-empty, first-wins, idempotent
                             re-register (4 tests)
  ResourceSummaryTest      — utilization edge cases (zero-divide,
                             over-allocation), isEmpty semantics
                             (7 tests)
Full unit suite: 681 tests, 1700 assertions, all passing. PHPStan
clean. Pint clean.

Companion PR (plugin repo) implements the ResourcesGateway in PgmPro
and includes the Resource Allocation tab UI, ties into StrategyPro
via a WorkStructure lm_inputs → people|budget|dependency mapping,
and prepares the reporting seam for the ReportEngine integration
that lands after core PR #3643.

Architectural rationale
───────────────────────
Why not put the whole thing in the plugin
  Resources are a concept the ReportEngine (core) needs to know about
  to render a Resources section. Baking a hardcoded reference to
  PgmPro into core would be wrong. The gateway/registry pattern
  lets core define the concept and any plugin implement it, matching
  how WorkStructure does the same for structures and how
  ContentTemplates does the same for template libraries.

Why not events/filters instead of a formal contract
  Contract + typed value objects give consumers (the ReportEngine,
  future UI callers) a real IDE-navigable, PHPStan-checkable surface.
  Filters would work but push the shape into runtime string keys
  that break silently when the plugin changes.

Why "first plugin wins" instead of a chain
  There is exactly one Resources provider per install in v1
  (PgmPro). If a second plugin ever wants to provide, that's a real
  product decision (which numbers do reports believe?) that
  shouldn't be silently resolved by registration order.

Reporting integration
─────────────────────
Not in this PR. The seam is a one-liner in
ReportEngine::buildReport() (from feature/report-screens, PR #3643):

    $resources = $this->resourcesRegistry->resolve()?->getForProjects($projectIds);
    return [..., 'resources' => $resources, ...];

Lands as a follow-up commit on that branch (or as a separate PR after
it merges) so this PR stays reviewable independently of the reporting
work.

Doc followup outside this repo
──────────────────────────────
The stakeholder reporting requirements doc §4 Data-origin map should
be updated to describe the Resources source as layered:
  - primary: structured via ResourcesGateway (this PR + companion)
  - fallback: narrative from Logic Model Inputs (existing behavior)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Corresponds to plugins@8af22f23 (feat: layer V2 plan-vs-actual overlay
onto Resource Allocation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ResourcesRegistry docblock: honest "same-class replaces / cross-class refused"
  wording, matching the code (was "no-op" which contradicted the test).
- BudgetLine::$spent non-nullable — providers commit to a value (0.0 when
  no actuals tracked). Removes the "unknown vs zero" ambiguity that leaked
  into totalSpent + budgetUtilization().
- Delete unused speculative primitives: avatar-stack, collapsible,
  metric-cell, proportion-bar. None had consumers; collapsible also
  depended on Alpine (not loaded in this codebase).
- ResourcesRegistryTest: assert Log::warning() fires on cross-class
  refusal (contract, not a silent behavior).
- ResourceSummaryTest: exercise the previously-dead makeDependency() via
  a "deps-only isEmpty" case; drop the now-non-null spent hint.

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

Corresponds to plugins@3e67a12e — honest empty/stub states across all
three sections and the userId === 0 no-actuals V2 treatment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Corresponds to plugins@4b556372.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Corresponds to plugins@d45c0345 — the "backward" seeding direction
(people + budget from child projects) landing across service, HxController,
UI, JSON-RPC, and MCP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ResourcesGateway docblock: drop the class-name reference to a
  ReportEngine that doesn't exist on master. Rewords the consumer
  example generically until the report engine lands.
- Rename test test_reregistering_same_provider_class_is_idempotent
  → test_reregistering_same_provider_class_replaces_instance. The
  behavior is deliberately last-write-wins (state changes), not
  idempotent in the strict sense. The old name misdescribed what was
  under test.
- Test file header rewritten to match: same-class REPLACES; different
  class refused.

PR description separately updated (out-of-band) to match the final
BudgetLine.spent non-null contract — the "unknown actuals" ambiguity
is deliberately not encoded at model level.

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

The board-facing stakeholder report: the Logic Model canvas read out as a
4-page swipeable deck. Strategy and program scopes only (per §6.2 —
project keeps existing /reports/project).

New:
- app/Domain/Reports/Templates/partials/stakeholder/deck.blade.php
  The plugin-neutral deck shell. Persistent header with computed status
  verdict + provenance line (§3), global controls, tab bar with 4 tabs
  (Overview / Logic Model / Resources & Coverage / Programs), horizontal
  swipe + arrow-key + arrow-button navigation. Sizes the panel to the
  active page (no dead space on short pages). Layout discipline: every
  grid track minmax(0,1fr), every child min-width:0, so the deck fits
  12–13" laptops with no horizontal scroll (§2).
- Print stylesheet (§7): expands the 4 pages into one flowing snapshot,
  hides tab bar / arrows / period picker / view toggle.

Phase 1 renders page 1 (Overview) with the real KPI band bound to
ReportEngine::buildReport() stats + deltas from the nested $report key
(§8 shape verified against origin/feature/report-screens). Pages 2–4
render placeholder cards for phased implementation:
  - p2 Logic Model — requires LM canvas fetch + card standard + connection
    badges from zp_logicmodel_health (via plugin service; core never reads
    the plugin table directly per §6.7)
  - p3 Resources & Coverage — blocked on §10.d resolution + ResourcesGateway
    wiring + buildCoverageMatrix upgrade
  - p4 Programs & Narrative — program rows + status narrative

Also: 30 stakeholder.* language keys under app/Language/en-US.ini for the
shell copy.

Companion plugin PR: [feat/stakeholder-report on plugins repo] with the
two template shims that include this partial.

Design source: Docs/STAKEHOLDER-REPORT-REQUIREMENTS.md
Mockup:        Docs/board-report-swipe.html

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes to the deck shell, matching the mockup (Docs/board-report-swipe.html):

1. Header sub-line is now semantic — "Strategy report · Q2 2026 · last
   closed period · updated Jul 14" — reading WHY this period is being
   shown, not the raw date range (which repeats in the picker below).

2. Period picker collapsed from Marcel's 4-pill row into ONE button
   showing the current period + range, click opens a dropdown:
     Last quarter    (default — how did we do?)
     This quarter    (how are we doing now?)
     Next quarter    (what's coming?)
     ─────────
     Custom          [From] – [To] [Apply]

   The dropdown is defensively styled against Bootstrap's global
   anchor/input styles (explicit font-size, height, padding on every
   text/input element) so the app CSS doesn't inflate it.
   Outside-click dismisses; jQuery UI datepicker attaches to the two
   inputs when available (same helper Marcel's periodpicker uses).

Also adds paired plugin pointer bump: both StrategyPro and PgmPro Report
controllers now default to ReportPeriod::lastQuarter() when no explicit
period params, so the picker button opens on the honest default state.

Language: 7 new stakeholder.period.* keys.

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

Split the deck's four pages into partials for reviewability. Page 1
(Overview) is now real; pages 2/3/4 land in follow-up phases.

Overview (page-overview.blade.php):
- KPI band: value-first with delta as a colored pill (up = green,
  down = red), the "/6" subscript letter-spaced so the digit doesn't
  smoosh against the slash, all three rows center-aligned.
- Peak-this-period hero: ranks completed-this-period milestones by
  outcomeImpact presence (structural closure > any recent close); shows
  the top pick with owner-override link ("Recommended · change"). If no
  outcomeImpact, quiet "no outcome narrative yet" note.
- Needs Attention: names the actual at-risk goals, overdue milestones,
  and silent projects (was just counts). Grouped by kind, capped at 3
  per group with a "+ N more" fallback.
- Theory of Change narrative: colored per-stage spans stitched from the
  LM canvas. Small ⓘ hover popover exposes the color code (5 stages +
  one-line descriptions).
- Theory-health strip: warns on fragile connections from
  zp_logicmodel_health; calm "all ok" state when nothing flagged.

Page-lm / page-resources / page-programs: stubbed with real data
plumbing where the shape supports it (5-stage board reads items from
$logicModel.coverageMatrix.stages using SHORT stage keys per
Logicmodelcanvas::STAGES + BOX_TO_STAGE; coverage matrix renders
per-stage linkage tallies with unaligned-columns callout for
off-strategy work; program rows + status narrative from programRows
+ programUpdates).

Also:
- Header sub-line no longer duplicates the date range (picker button
  shows it). Reads: "Strategy report · last closed period · updated
  Jul 14, 2026".
- Compact period picker: one button ("Last quarter · Apr 1 – Jun 30,
  2026") that opens a dropdown with the 3 quarter presets + a custom-
  range mini-form. Preset name, not "Q2 2026", because Leantime doesn't
  let companies define fiscal quarters.
- Smart default: /strategyPro/report and /pgmPro/report default to
  ReportPeriod::lastQuarter() when no explicit params — board reports
  care about "how did we do?" more than "how are we doing?".

Language: ~60 new stakeholder.* keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Peak-this-period hero: real recommendation from completed milestones
  with outcomeImpact preferred.
- Needs Attention: names the actual at-risk goals / overdue milestones /
  silent projects (was counts).
- KPI band: value-first with delta pill (up = green, down = red), /X
  subscript letter-spaced. Hover any cell for a drill-down of the items
  behind the count.
- Theory of Change: colored per-stage narrative + info-icon color legend
  popover. Collapsible via chevron; default state is collapsed, expand
  state persists in localStorage.
- Theory Health: rebuilt as a chain of 5 stage chips + 4 colored
  connectors between. Each connector carries a status badge (24px white
  circle, 14px colored icon: fa-circle-check / fa-circle-exclamation /
  fa-triangle-exclamation / fa-circle-question, colored to match the
  connector line). Summary reads positive-first ("3 solid · 1 needs work").
- Fragile-link callout: severity-sorted so critical leads over warning,
  crit gets red tint (warning stays amber). "Assumption unproven" pill
  sits inline as a soft-tinted chip with hover explainer. "Also fragile"
  chip row lists secondary fragile links below.
- Info-icon component generalized (was ToC-only, reused for Theory Health).
- Fixed a double-escape bug: replaced $tpl->escape (htmlentities → turns
  unicode into &rarr; etc.) with plain Blade {{ }} escaping across all
  stakeholder partials. Now unicode names / arrows / accents render clean.

Header + picker:
- Merged the global picker bar into the tab row (saved a row of vertical
  space). Picker now sits with the tab arrows on the right.
- Removed date range duplication from the sub-line (picker button already
  shows it). Sub-line now reads "Strategy report · last closed period ·
  updated Jul 14".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New actionsMenu partial: shared three-dot menu for both plugin templates,
  scoped-styled to override the app's global .pagetitle dropdown CSS (which
  resolves --main-titles-color to white inside the pageheader). Uses the
  Leantime canvas headerEditDropdown pattern for right-anchored positioning.

- KPI drill: click-to-open (was hover-to-open — hover was retaining and
  reading as "stuck"). Single-open behavior; click outside dismisses. Hover
  now just visually focuses the card. Discoverability: "SEE LIST ↓" chip
  visible in the top-right, chevron rotates when open.

- Goals-on-track drill: lists ON-TRACK goals (the count's referent), not
  at-risk ones (those live in Needs Attention).

- Hours logged drill: per-project breakdown, largest first.

- KPI denominators: Completed shows N/M (of total milestones), Overdue
  shows N/M (of open milestones). Matches the /X pattern Goals already used.

- Milestones ambiguity fix: label was "completed this period" (unclear
  what). Now "milestones completed this period" + drill header says
  "Milestones completed" — explicit.

- Plugin pointer bump: paired StrategyPro/PgmPro Controllers +
  Hxcontrollers + Templates commit for the verdict override write path.

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

# Conflicts:
#	app/Plugins
…lastModified

Adds four optional fields to the Dependency value object so a stakeholder
report can surface WHO is chasing each external commitment, WHEN a
decision is expected, WHY it matters, and how fresh the information is.

Fields are nullable and default to null — older resource canvas items
without these fields hydrate cleanly and render without the annotations.

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

Per-project analyzer that joins three independent estimates of the work
ahead with the capacity a ResourceSummary already exposes:

  1. Budgeted hours  — sum(planHours) on open tickets. Explicit but often blank.
  2. Effort hours    — sum(storypoints) * hoursPerPoint on open tickets.
                       Always populated when sizing is used; a t-shirt-size proxy.
  3. Available hours — sum(person allocation to this project) * weeks in the
                       report period.

Emits per-project rows carrying scope, capacity, gap, verdict (critical /
tight / balanced / buffer), a trust signal (budgeted / effort / mixed /
none) that flags when budgeted and effort disagree materially, and three
rebalance recommendations with their math — extend timeline, add people,
or cut scope in points. aggregateByProgram rolls the per-project rows up
to the program level for the strategy report, recomputing every derived
field against the aggregate so a program reads as its own state rather
than the worst child's.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ollup, hours/days toggle

Rewrites the stakeholder report's Resources page (Page 3) into a
resource-centric story:

- Aggregate cards (People / Budget / Dependencies) — the 30-second read
  from ResourceSummary totals.
- Per-program breakdown table with inline expand to reveal the child
  project list. At program scope, the same table renders per-project.
- Capacity vs. Demand cards, one per program (strategy scope) or per
  project (program scope). Each card reads a computed verdict and shows
  scope two ways with a divergence flag, capacity with its formula,
  balance visualization, and rebalance recommendations with math.
- Dedicated Dependencies section rendering each external commitment
  with owner, decision date, notes, and last-updated timestamp. The
  soonest tentative decision is surfaced as an urgency banner.
- Hours / Days display toggle at the top of the page, persisted via
  localStorage. Server always renders hours; JS swaps data-hours
  attributes to a day equivalent when toggled.

Extracts capacity-card.blade.php as a reusable partial so program and
project cards share the same render.

Also drops the tentative-dependencies chip from the residual gaps
section since it duplicates the Dependencies section names.

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

Pulls in two plugin commits:
  - PgmPro Dependency hydration for owner/dueDate/notes/lastModified
  - PgmPro + StrategyPro report controllers wired to CapacityAnalyzer
    and program rollup vars

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

Full rewrite against the design plan mockups. Board audience: understanding-first,
never editing. Structure:

  1. Belief — one grey sentence, "If we deliver on {activities}, we'll produce
     measurable reach — and that reach will {impact}." Capped ~220 chars.
  2. Producing (Outputs) — evidence, loud: verdict + templated read + rows +
     Show-the-breakdown.
  3. Achieving (Outcomes) — same treatment.
  4. What it's for (Impact) — purpose, honestly long-horizon.
  5. Where it's at risk — one weakest health badge, single sentence.

Interpretation pattern — the through-line:
  - Verdict badge derived from the READ (not a second aggregation).
  - Templated read branches on contributor count (1 / 2 / 3+); never
    freeform. Materiality inverts correctly — 5% share = "low concern",
    77% = "the driver, not a footnote".
  - Exception line cites the specific item it's off on ("behind on 300
    referrals to primary care") — the bridge that reconciles read + rows.
  - Never two lines about the same entity.
  - Single-contributor sentence scoped to its stage ("on the outcomes here")
    so a program appearing in both cards reads as scope, not contradiction.
  - Never names a non-entity — unresolved-project goals are filtered from
    contributors and surfaced as a muted data-quality note.

Data safety rules (learned expensively):
  - Never splice a computed value into an authored label. Description text
    is verbatim — no prefix, no regex.
  - Goal record is the sole source of current/target when a goal is linked.
  - Unit is a known field on the goal; inferred once per item from the
    label (percent/currency/number). Unit-mismatched goals are dropped
    from the aggregate and logged, never silently mixed.
  - Guard rails: target <= 0 or ratio > 5x → render no ratio, log warning.
    No percentage over 100 ever reaches a funder.
  - Percent metrics never render "of Y planned" (percent-of-percent reads
    as nonsense).

Row status vocabulary is one map, one casing: on track / in progress / at
risk / behind. Row status class is tied to the goal RAG (same source as
badge) so rows and badge cannot contradict each other on the same card.

Provenance in a tooltip ("Set by {author} · {date}") on every row status
— report gets exported and printed, instructional text has no place.

Chrome: white cards, stage color as 3px left border + eyebrow + icon
accent only. No background washes, no cream. One system across the page.

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

Two small deck improvements:
  - Active page persists via localStorage under lt.stakeholderReport.activePage,
    so a refresh (or returning to the report) lands on the last-viewed page
    rather than always Overview. Applies to tab click, arrow keys, and swipe.
  - Verdict dot map adds an "inprogress" entry (#3F72B0) — matches the new
    In progress verdict state the strategy/program report templates emit.

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

Pulls in two plugin commits:
  - StrategyPro enrichment of linkedGoals (unit, project/program metadata, provenance)
  - StrategyPro + PgmPro header verdict follows the read's materiality call

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old bar drew a single "demand" fill sized to the reference demand,
with an available marker sitting on top. Visually quieter than the words
around it — the verdict pill said "191h short" but the bar looked
uniformly filled.

New geometry lands the shortfall at the same visual weight as the verdict:
  - Supply segment (0 → available)   in green    — "what you've got"
  - Deficit segment (available → needed) in the verdict color (amber for
    tight, red for critical) — "what you're missing"
  - Marker stays AT the available position with the CAPACITY label above

Deficit segment carries a tippy tooltip ("Shortfall: 191h past available
capacity") on hover.

Closes v1 non-negotiable #9.

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

Page 4 (Impact Journey) — new page replacing the Programs & Narrative
stub. Three lens views (Vision / Progress / Impact), tense-driven, one
persistent bar per metric that morphs on lens change (fill width +
label crossfade, 700ms cubic-bezier easing, 80ms stagger between
metrics, prefers-reduced-motion respected). Progress and Impact lenses
unlock as the journey earns them (any snapshot → Progress; any hit
target → Impact). Bookends both authored + present Day 1: STARTED
(the world today) → the measurable arc → DIFFERENT (the world when
delivered). Growth states per metric: baseline / before-after / full
arc.

Authored-meaning fields on Logic Model canvas items — two nullable
text columns (why_this_matters, starting_picture) via migration
30522 + dbVersion bump. Fields wired through the Canvas editor with
stage-gated visibility (Impact gets both; Outcome/Output get
why_this_matters only). Impact items deliberately have no suggest
control — the friction is the feature. Outputs/Outcomes get a
client-side Suggest button that drafts from the item's own text;
silence when the source is too thin to draft honestly.

Report reads authored text only — no synthesis, no fallback chain
that mines conclusion for meaning (conclusion narrows to "as measured
by" methodology only). Page 4 renders meaning-first when
why_this_matters is authored, falling back to today's label-first
behavior when empty (no regression). Bookend empty states name the
specific canvas field to author.

Blueprints patch allowlist extended with the two new columns.
CanvasItemsApplier passes through why_this_matters / starting_picture
from YAML templates. Deck tab renamed Programs → Impact Journey with
a compass icon.

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

The one place on Page 1 where a human says WHY and what they're doing
about it. Everything else on the page is computed; this is the closest
thing to a decision the system holds.

Renders authored zp_comment notes (module='project') scoped to the
period, portfolio-level first (strategy scope) or the program's own
thread (program scope), then per-program newest-first. Cap 4 total,
oldest drop off. Each note: bold entity name — verbatim text — muted
date. Notes are trimmed of stray HTML from rich-text editors but
never truncated mid-sentence and never summarized.

Silent when zero notes exist — no apology, no prompt. Missing status
notes aren't a gap in the artifact; they just mean nobody wrote one.

Placement: full-width strip under the p1-topband (peak hero + needs-
attention), before the ToC narrative. Same reading order as the
existing right-column pattern.

Deck passes the new strategyUpdates + programUpdates + programRows
through to page-overview.

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

Page 2 gets a small block above the drift note that lists milestones
completed this period that never got linked to an outcome — reads
authored task-stats where present, otherwise renders the completion
itself. Nothing generated; the row's job is to point at unlinked work
so it can be tied back to the model.

Page 3 gets a Coverage table above Capacity that answers the question
Capacity can't reach: does each focus area actually have people or
money behind it? Per Output/Outcome item, walks goal links → owning
projects, then scopes ResourceSummary to those projectIds. Verdicts
(covered/thin/gap) never assert against unauthored resources — the
ambiguity line spells the two meanings out when a gap exists.

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

Snapshots (Step 1)
- getGoalValueAt(id, asOf): last snapshot on or before the date.
- getGoalValuesAtSeries(ids, dates): the three-year-arc query.
- goals:backfillHistory CSV command; imported rows are indistinguishable
  from nightly captures.
- Write path, hook, and reports:goalValueSnapshot scheduler untouched.
  The existing sparse writes are correct — the read makes "missing days
  don't matter" transparent.

Coverage table (Step 2) — cut
- ResourcesGateway attributes only at project/program scope, not to
  strategy goals. Walking the tree would badly overstate. With one
  dimension left, the verdict just restated the count under a header
  asking a question the table couldn't answer. Removed.

Vision lens (Step 3)
- Three beats, not a table: authored `starting_picture` → measurable
  arc as one prose statement (authored labels, cap 3 producing + 2
  achieving) → authored `why_this_matters`. No tracks, no bars, no
  "will be measured".
- On Vision → Progress/Impact, the arc block hides and metric rows
  stagger in (~80ms). prefers-reduced-motion → instant.

Small ones (Step 4)
- Removed the "link to an outcome" CTA — nothing consumed
  ?linkMilestone. A dead link belongs anywhere but the honesty page.
- StrategyPro submodule bumped to clean the community-health seed so
  `conclusion` holds methodology only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The button hinted at a "coming soon" PDF exporter. No exporter exists;
the CTA promised something the app can't deliver. Same class as the
dead link on Page 2 — a placeholder button belongs anywhere but the
funder-facing artifact.

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

Previously the strategy report ran per-person over-allocation checks in
Gaps, then reported "Resources look aligned" whenever no individual
exceeded their own capacity — even when Capacity vs Demand above was
flagging programs as tight. Two aggregations, one page, contradicting
reads for the same audience.

At strategy scope the report now:
- drops the per-person over-allocation and idle-capacity checks (wrong
  altitude for a portfolio audience — those belong on the program
  report where a manager can act),
- escalates each tight/critical program from CapacityAnalyzer into a
  matching gap row so the two blocks agree,
- rescopes the empty-state detail to name what's actually being checked
  ("No program tight on capacity …") instead of the broad claim.

Per-person checks still run at program scope, where a manager IS the
right audience for individual hour math.

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

Bumps the StrategyPro submodule to the plugins#74 Phase 2 HEAD and adds
9 contract tests for the ReverseStage enum — 44 assertions covering the
load-bearing invariants:

- Impact can never be seeded (canBeSeeded=false at the enum level).
- Every other stage IS seedable.
- Highest-confidence stages (Inputs, Outcomes) propose selected.
- Medium/low (Outputs, Activities) propose unselected.
- Only Activities collapses by default.
- Canvas order is Inputs → Impact (locks review + rail order).
- Box values match Logicmodelcanvas convention (lm_inputs, ...).
- Every stage has a real guide prompt (not a fallback).
- Impact's guide prompt matches the exact spec §6 rev.2 phrasing.

Companion plugin PR: Leantime/plugins#74.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 21:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +37 to +39
class WorkAdapterTest extends TestCase
{
public function test_empty_strategy_returns_empty_completeness(): void
Comment on lines +21 to +23
class ReverseStageTest extends TestCase
{
public function test_impact_can_never_be_seeded(): void
… bump)

Bumps the plugins submodule to include:

- Guide-mode authoring in the reverse flow — Inputs (and any empty-
  source stage) now has a real textarea instead of a Skip-only shrug.
  Authored items commit as canvas items with NO MapsTo.
- "Pull in new work" ⋮ menu item on populated Logic Model canvases.
  Fires the same wizard, filtered to only new/unlinked candidates. Toast
  fires when nothing new. Closes the incremental-loop gap.
- Findability banner uses post-filter counts so re-runs don't over-
  promise ("3 new goals since your last pull," not "5 new goals").

Companion plugin commit: Leantime/plugins@8ee5749c on feat/logic-model-
reverse (PR #74).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 22:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

$this->assertSame(1, $out['outcomes']['hiddenCount']);
}

public function test_sparse_level_when_one_or_two_candidates(): void
…opy pattern + modal fixes

Companion commit: Leantime/plugins@3b94a770 on feat/logic-model-reverse
(PR #74). Rolls up user-testing feedback into the reverse flow:

- Activities offers both altitudes (Programs + Projects direct).
- New copy pattern (name the data → ask the purpose → label the result)
  applied to the Activities H1 + body.
- Inline authoring in Seed mode (mixed seed+authored per stage).
- Helper hint for projects living elsewhere in Leantime.
- Modal scrolls internally so long steps don't hide the Continue button.
- Groups auto-expand when they carry a selection + reveal-count badge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 23:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +37 to +40
class WorkAdapterTest extends TestCase
{
public function test_empty_strategy_returns_empty_completeness(): void
{
Comment on lines +21 to +24
class ReverseStageTest extends TestCase
{
public function test_impact_can_never_be_seeded(): void
{
…ctivities

Companion commit: Leantime/plugins@416ad433 on feat/logic-model-reverse
(PR #74). Closes the canvas-has-it-work-side-doesn't gap: authoring an
activity in the wizard now creates the real zp_projects row of the
correct type (program|project) under the strategy AND writes the canvas
item AND records MapsTo from the new project to it. All in one
transaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 00:14
… co-testing

Merges PR #72 (Resource Allocation revival + tab + sidebar) into the
reverse flow branch so both surfaces can be walked in one session. The
reverse flow's Inputs stage already reads zp_canvas type='resource'
directly — this merge lets the sidebar entry that AUTHORS those be
reachable in the same build for testing.

No functional coupling change — WorkAdapter still uses the one call
site (readResourcesInScope) it always did; when PgmPro merges to main,
that body swaps to the Gateway with zero other changes.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +37 to +39
class WorkAdapterTest extends TestCase
{
public function test_empty_strategy_returns_empty_completeness(): void
Comment on lines +21 to +23
class ReverseStageTest extends TestCase
{
public function test_impact_can_never_be_seeded(): void
Copilot AI review requested due to automatic review settings July 16, 2026 00:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +37 to +38
class WorkAdapterTest extends TestCase
{
Comment on lines +21 to +22
class ReverseStageTest extends TestCase
{
Brings the Core\Resources\* contract surface (ResourcesGateway
interface, ResourcesRegistry, value objects — ResourceSummary,
PersonAllocation, BudgetLine, Dependency) into this branch so the
PgmPro plugin's ResourceStructureService (merged in on the plugins
side) can resolve its interface at runtime.

Same intent as the plugins-side merge — lets both open PRs be tested
end-to-end in one build without coupling their diffs on GitHub. When
these merge to master in their own order, they'll come together
cleanly.

# Conflicts:
#	app/Plugins
Copilot AI review requested due to automatic review settings July 16, 2026 02:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated 5 comments.

Comment on lines 92 to 96
30518,
30519,
30520,
30521,
];
Comment on lines 2274 to 2276
'acceptanceCriteria' => $values['acceptanceCriteria'] ?? '',
'outcomeImpact' => $values['outcomeImpact'] ?? '',
'editFrom' => $values['editFrom'] ?? '',
Comment on lines +108 to +110
// Filter to open tickets only — DONE work doesn't need capacity.
$openTickets = array_values(array_filter($tickets, fn ($t) => ! $this->isDone($t)));

Comment on lines +111 to +123
$ts = strtotime($rawDate);
if ($ts === false) {
$errors[] = "line {$lineNo}: unparseable dateRecorded '{$rawDate}'";

continue;
}

$rows[] = [
'itemId' => $itemId,
'value' => (float) $rawValue,
'userId' => ($idxUser !== false && isset($record[$idxUser]) && $record[$idxUser] !== '') ? (int) $record[$idxUser] : null,
'dateRecorded' => date('Y-m-d H:i:s', $ts),
];
Comment on lines +17 to +24
/**
* Shared, period-aware reporting engine feeding the project, plan and strategy report screens.
*
* All methods operate on explicit project-id sets so higher levels (plan = child projects,
* strategy = descendant projects, later company = all projects) compose the same building
* blocks. Ids are filtered to projects the requesting user may view before any query runs,
* and the repository re-applies the SQL access predicate — defense in depth.
*/
…ed + dead-button fixes

Companion commit: Leantime/plugins@d9982115 on feat/logic-model-reverse
(PR #74). Fixes the three Resource Allocation tab issues Gloria caught
during testing:
- Empty first-open with dead Add buttons → auto-seed from child projects
  on open + banner explaining what happened
- Auto-fill button 500 → missing view template created
- Four dead Add* buttons → disabled with 'coming soon' badge (CRUD
  modals become a follow-up)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated 6 comments.

Comment on lines +108 to +110
// Filter to open tickets only — DONE work doesn't need capacity.
$openTickets = array_values(array_filter($tickets, fn ($t) => ! $this->isDone($t)));

Comment on lines +350 to +356
private function isDone(array $t): bool
{
// Numeric statuses vary per project; treat -1 and 0 as closed/archived by convention.
$status = (int) ($t['status'] ?? 0);

return $status <= 0;
}
Comment on lines 92 to 96
30518,
30519,
30520,
30521,
];
@@ -0,0 +1,334 @@
<?php

namespace Tests\Unit\App\Domain\Reports\Services;
@@ -0,0 +1,175 @@
<?php

namespace Tests\Unit\App\Domain\Reports\Models;
public string $appVersion = '3.9.8';

public string $dbVersion = '3.5.20';
public string $dbVersion = '3.5.22';
@marcelfolaron

Copy link
Copy Markdown
Collaborator

Maintainer triage (advisory — merge authority stays with @marcelfolaron / @broskees) · first pass · status: ready-for-review (blocked by merge-order) · priority: P2 · next action: hold for plugin #74 + verify submodule pointer · owner: @marcelfolaron / @broskees

1. Intent: Core companion to plugins #74 — bumps the app/Plugins submodule pointer to the StrategyPro feat/logic-model-reverse HEAD and lands WorkAdapterTest (7 tests, 20 assertions) covering the reverse flow's read half. No core code changes — pointer bump + tests only.

2. Change requests:

  1. ⛔ CR Documentation #1 — submodule pointer coupling (same class of blocker as feat(core): Resources contract surface — gateway, registry, value objects, shared blade primitives #3645). This PR moves app/Plugins to the current head of the unmerged plugin PR Show all tickets choose milestone out of screen #74. If feat(strategypro): reverse-engineer flow — Outcomes pilot (submodule bump + tests) #3651 merges before Show all tickets choose milestone out of screen #74 lands on plugins:main, master's submodule references a commit that isn't on the plugin default branch — a broken checkout for anyone pulling recursively. Per the standing convention: land the plugin PR first, then do the pointer bump as the explicit last step. This is the single merge-blocker.
  2. ⚠ CR Gitignore #2 — test placement vs. tested code. WorkAdapterTest lives in core but exercises WorkAdapter, which ships in the plugin (Show all tickets choose milestone out of screen #74). Confirm the test can actually resolve the plugin class in core's CI (submodule checked out during the test run) — otherwise these 7 tests silently no-op or error in the core pipeline. If core CI doesn't init submodules, this coverage isn't real where it's running.
  3. ✅ Test content looks right on paper — the 7 behaviors (empty→empty, all-hidden→"already reflected", metric target split, endValue=0→no-target, hidden-but-counted, no unfilled sprintf placeholders, scope pass-through) map cleanly to the §11 rules in the plugin PR. Verify green in the suite.

3. Acceptance checklist (before merge):

  1. Plugin Show all tickets choose milestone out of screen #74 merged to plugins:main first; this PR's pointer bumps to the final Show all tickets choose milestone out of screen #74 merge commit (CR Documentation #1).
  2. Core CI initializes the submodule so WorkAdapterTest runs against the real WorkAdapter (CR Gitignore #2) — confirm 7/7 green, not skipped.
  3. CI green: Pint + PHPStan level 5 + full unit suite on the current head.

4. CI status: ⚠️ Confirm check-runs green in the UI; a head with zero/failing checks must not merge. Merge order: #74#3651 (pointer bump last).

Advisory review only — not an approval, and I am not marking this ready or merging.

@marcelfolaron

Copy link
Copy Markdown
Collaborator

Maintainer triage — CI follow-up (advisory · merge authority stays with @marcelfolaron / @broskees) · status: blocked (CI + merge-order) · priority: P2 · next action: fix failing unit job before merge

Read the check-runs on head 2cce2b3 directly (12 total). Status:

  • unit — FAILED (completed, 2 annotations). This is the exact risk called out as CR Gitignore #2 in the first pass: WorkAdapterTest lives in core but exercises WorkAdapter, which ships in the unmerged plugin PR #74. The core unit job either can't resolve the plugin class (submodule not initialized to a commit that contains WorkAdapter) or the 7 tests genuinely fail. This must be green before merge — a failing unit suite is a hard blocker.
  • CodeQL — success (no new alerts).
  • copilot-pull-request-reviewer — success.

Confirmed merge-order state: plugins #74 is still open / unmerged as of this pass. Per CR #1, this pointer-bump PR must not merge until #74 lands on plugins:main, then re-bump to the final #74 merge commit. So there are now two independent gates:

  1. plugins Show all tickets choose milestone out of screen #74 merged first; pointer re-bumped to its final merge commit.
  2. unit job green on the re-bumped head (submodule initialized so the 7 tests run against the real WorkAdapter, not skipped/errored) — currently failing.

Recommend @gloriafolaron pull the 2 annotations from the unit run to confirm whether it's a submodule-init gap in core CI or a real test failure; the former is a CI-config fix, the latter is a code fix. Either way #3651 stays not-mergeable until it's green.

Advisory review only — not an approval, and I am not marking this ready or merging.

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.

3 participants