feat(strategypro): reverse-engineer flow — Outcomes pilot (submodule bump + tests)#3651
feat(strategypro): reverse-engineer flow — Outcomes pilot (submodule bump + tests)#3651gloriafolaron wants to merge 41 commits into
Conversation
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 → 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>
| class WorkAdapterTest extends TestCase | ||
| { | ||
| public function test_empty_strategy_returns_empty_completeness(): void |
| 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>
…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>
| class WorkAdapterTest extends TestCase | ||
| { | ||
| public function test_empty_strategy_returns_empty_completeness(): void | ||
| { |
| 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>
… 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>
| class WorkAdapterTest extends TestCase | ||
| { | ||
| public function test_empty_strategy_returns_empty_completeness(): void |
| class ReverseStageTest extends TestCase | ||
| { | ||
| public function test_impact_can_never_be_seeded(): void |
| class WorkAdapterTest extends TestCase | ||
| { |
| 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
| 30518, | ||
| 30519, | ||
| 30520, | ||
| 30521, | ||
| ]; |
| 'acceptanceCriteria' => $values['acceptanceCriteria'] ?? '', | ||
| 'outcomeImpact' => $values['outcomeImpact'] ?? '', | ||
| 'editFrom' => $values['editFrom'] ?? '', |
| // Filter to open tickets only — DONE work doesn't need capacity. | ||
| $openTickets = array_values(array_filter($tickets, fn ($t) => ! $this->isDone($t))); | ||
|
|
| $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), | ||
| ]; |
| /** | ||
| * 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>
| // Filter to open tickets only — DONE work doesn't need capacity. | ||
| $openTickets = array_values(array_filter($tickets, fn ($t) => ! $this->isDone($t))); | ||
|
|
| 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; | ||
| } |
| 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'; |
|
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 2. Change requests:
3. Acceptance checklist (before merge):
4. CI status: Advisory review only — not an approval, and I am not marking this ready or merging. |
|
Maintainer triage — CI follow-up (advisory · merge authority stays with @marcelfolaron / @broskees) · status: blocked (CI + merge-order) · priority: P2 · next action: fix failing Read the check-runs on head
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
Recommend @gloriafolaron pull the 2 annotations from the Advisory review only — not an approval, and I am not marking this ready or merging. |
Summary
Core companion to Leantime/plugins#74 — bumps the StrategyPro submodule pointer to the plugin's
feat/logic-model-reverseHEAD and lands unit tests for the reverse flow's read half.What's in the PR
app/Plugins.tests/Unit/app/Plugins/StrategyPro/Services/WorkAdapterTest.php— 7 tests, 20 assertions covering the four highest-regression-risk behaviors onWorkAdapter:endValue=0is treated as no-target (avoids "0% of 0" nonsense).The plugin PR carries the actual services, HxControllers, and templates. See there for the full spec + verification checklist.
Merge order
plugins:main.Test plan
./vendor/bin/phpunit tests/Unit/app/Plugins/StrategyPro/Services/WorkAdapterTest.php— 7 tests pass.🤖 Generated with Claude Code