Opportunity Prioritization (Assessment IR)¶
The assessment package (github.com/grokify/prism-roadmap/assessment) is an evidence-backed, rubric-driven prioritization record: a judge answers bounded Y/N rubric questions with cited evidence, and deterministic Go code — never the judge — turns those answers into a MoSCoW tier, a RICE score, a portfolio classification, and a final rank.
Not the same type as the Opportunity Assessment canvas
This package's OpportunityAssessment is unrelated to canvas.OpportunityAssessment, the SVPG 10-question canvas — they're different Go types in different packages that happen to share a common English name. The canvas is a single-shot, human-authored go/no-go worksheet. This package is a regenerable, per-cycle, evidence-backed judgment record that feeds a ranked portfolio and generated reports.
Design principle: the judge never invents a number¶
Every score, tier, or category in this package is resolved from evidence-backed answers by deterministic code — an LLM/judge only ever produces bounded Y/N answers with citations (ThresholdAnswer, DimensionAnswer). This is enforced structurally, not by convention: a Satisfied: true answer with no EvidenceIDs is treated as unsupported and ignored by every resolver in the package.
Pipeline¶
graph LR
EV[Evidence] --> LA[Ladder / MoSCoW / RICE]
EV --> DIM["Dimensions (Kano, MIH, PDIM, …)"]
LA --> OA[OpportunityAssessment]
DIM --> OA
OKR[OKR Contributions] --> OA
CAP[Capability References] --> OA
OA --> RANK[RankingPolicy]
RANK --> RD[ReportDataset]
RD --> RPT["OpportunityReport / PortfolioReview"] Evidence¶
Evidence wraps structured-evaluation's claims.Claim rather than reinventing claim/verdict semantics, adding capture metadata (source system, excerpt, capture time, sensitivity):
import "github.com/grokify/prism-roadmap/assessment"
ev := assessment.NewEvidence("EV-042", "Deployment inventory shows 72% of accounts on the affected code path").
WithSource("https://wiki.example.com/deploy-inventory", assessment.EvidenceSystemWiki, claims.SourceInternal, claims.ReliabilityHigh).
WithExcerpt("72% of active accounts are on v3, the affected code path").
WithCapturedAt(time.Now()).
WithSensitivity(assessment.SensitivityInternal)
Sensitivity(SensitivityPublic/Internal/Restricted) gates whetherRenderableExcerpt()returns the quoted text or tells a renderer to cite the evidence ID with an access note instead. The zero value is not renderable — evidence with no sensitivity set is treated conservatively.IsStale(now, window)flags evidence older thanDefaultValidityWindow(system)(90 days for analytics dashboards, 180 for docs/code/tickets, never for signed contracts).EvidenceIndexanswers "which assessments/questions cite this evidence" over a flat[]EvidenceRef— a query helper, not a store; the persistence layer (omniroadmap) owns storage.
Ladder: the shared threshold primitive¶
Ladder is a top-down, evidence-required threshold classifier shared by MoSCoW, RICE Impact, and RICE Confidence: an ordered list of ThresholdLevels, each with citable criteria. Evaluate scans top-down and returns the first level whose answer is both Satisfied and evidence-backed — the same discipline used everywhere in this package, so a "yes" with no citation never counts.
MoSCoW and RICE: resolved, not assigned¶
The existing prioritization package's MoSCoWPriority/ImpactLevel/ConfidenceLevel types are reused, not redefined — this package only changes how a value gets assigned to them:
prioritization package | assessment package | |
|---|---|---|
| Input | Direct self-reported assignment on OpportunitySpec/rmi.RoadmapItem | Ladder threshold answers, each requiring cited evidence |
| Output | Same MoSCoWPriority/ImpactLevel/ConfidenceLevel types | Same MoSCoWPriority/ImpactLevel/ConfidenceLevel types |
| Use case | Quick triage, PM judgment call | Defensible, auditable prioritization for a portfolio review |
tier := assessment.ResolveMoSCoWPriority(a.MoSCoWAnswers) // prioritization.MoSCoWPriority
result := assessment.ComputeRICE(assessment.RICEAssessment{
Reach: assessment.Reach{Fraction: 0.72, Population: "active paying accounts", EvidenceIDs: []string{"EV-042"}},
ImpactAnswers: impactAnswers,
ConfidenceAnswers: confidenceAnswers,
Effort: assessment.EffortEstimate{Expected: 12.0, Gate: assessment.EstimabilityGate{ /* ... */ }},
})
// result.Computable is false — with result.Reason explaining why — rather than
// a fabricated score, whenever evidence, an unresolved ladder level, or a
// failed EstimabilityGate makes the score untrustworthy.
EffortEstimate.Gate (EstimabilityGate) is a five-check estimability gate — scope, implementation, dependencies, testing, and deployment identified — that must pass before an effort estimate is trusted for RICE, so an under-specified plan comes back as "needs more detail" instead of a fabricated Person-Days number.
MoSCoW's "Won't/Not Now" tier and RICE's low end are the ladder's floor, not a rung with its own criteria — they're what you get when nothing else is satisfied with evidence, not something a judge tests for directly.
COMPASS-RICE: cross-profile-comparable RICE¶
The ladder-based RICE above uses one Reach scale ("the fraction of the relevant customer population affected") for every opportunity, which breaks down once a portfolio mixes customer features, platform investments, risk mitigations, and cost optimizations — their raw metrics simply aren't comparable on one 0..1 fraction. compass-rice solves this: six investment-thesis profiles (Customer, Platform, Market Expansion, Operational Efficiency, Supportability, Risk), each with a domain-specific evidence model whose deterministic Normalizer produces the same canonical rice.Normalized shape — Reach as a 0–100 band, standard Impact/Confidence multipliers, Effort in person-days — so Score() is comparable across profiles.
Two different Reach scales — never mix them
compass-rice's Normalized.Reach is a 0–100 band; the ladder RICE above uses a 0..1 fraction. ToRankInput never blends the two for one opportunity — see below.
CompassAssessment (assessment/compass.go) is the evidence-to-score record: a profile-typed evidence document (the source of truth), the Normalized result compass-rice's Normalizer derived from it, the judge's rubric reasoning and verified claims, and whether a flagged assessment has cleared human review.
ResolveCompassRICE is the single gate: nil is uncomputable ("no COMPASS assessment recorded"), an assessment still flagged NeedsHumanReview stays uncomputable even though Normalized already holds a valid score, and a validation failure surfaces its own reason — never a silently-scored fallback.
Profile selection is two-phase, per compass-rice's own "exactly one profile generates the canonical RICE score" rule (PRD D5): an LLM judge proposes a ProfileAssignment (the primary investment thesis, with rationale and optional secondary theses recorded as context only — never scored), and a PM confirms or rejects it before its ProfileID is trusted for scoring:
proposed := assessment.ProposeProfileAssignment("OS-042", "customer/b2b/v1", "primarily a retention play", "judge-session-9")
confirmed := proposed.Confirm("pm@example.com", time.Now()) // or .Reject(...) to send back for re-proposal
ProfileAssignment is spec-scoped and survives assessment cycles, like RankOverride — a re-assessment doesn't require re-selecting the profile. ToRankInput prefers Compass over the legacy RICE field whenever both are present on an OpportunityAssessment; a consumer enforcing the two-phase gate end-to-end (assignment confirmed and its ProfileID matching the assessment's) is the consumer's own responsibility — see omniroadmap's compile-time gating for a worked example.
Portfolio dimensions¶
DimensionDefinition is a versioned, referenceable portfolio dimension — either DimensionKindCategory (mutually exclusive, 0..1 selection) or DimensionKindTags (multi-select). Assessments reference a dimension by ID+version (DimensionAssignment), so a definition changing later never retroactively reinterprets a past assignment. Dimensions are descriptive only — they never enter RankingPolicy.Rank.
Six dimensions ship built in:
- Kano (
KanoDimension(),ResolveKano) — Must-be, Performance, Attractive, Indifferent, Reverse. Resolved by a bespoke pattern-matcher over eight cross-cutting characterization questions (KanoAnswers), not the generic per-option resolver — Kano's categories aren't independent criteria the way a Ladder's levels are. - Market Investment Horizon (
MarketInvestmentHorizonDimension(), dimension version 2.0) — a four-rung ordinal ladder, KTLO < SOM < SAM < TAM Expansion; this project's own framework (not a published external standard) combining KTLO with TAM/SAM/SOM. The options are incremental rings — an initiative is classified by the furthest-out horizon it opens — andMIHRollupcollapses SOM/SAM (and the legacy combinedsam_somid) to the 3-way KTLO / SAM+SOM / TAM presentation grain. - Market Position (
MarketPositionDimension()) — the BCG growth-share quadrants (Star, Cash Cow, Question Mark, Dog) plus Enabler for a horizontal platform line whose value is cross-line leverage. Classifies a product line, not an opportunity — the assignment is made once per line and inherited by its initiatives. - Run/Grow/Transform (
RunGrowTransformDimension()) — Gartner's executive IT investment classification by business outcome: operate today's business, grow it, or create tomorrow's. - SRE Work Classification (
SREWorkDimension()) — Google SRE's Software Engineering / Systems Engineering / Toil / Overhead, per the SRE Book's "Eliminating Toil" chapter;SREWorkRollupcollapses the engineering pair to the headline Engineering / Toil / Overhead grain. - Product Development Investment Mix (
ProductDevelopmentInvestmentMixDimension()) — Innovate / Improve / Automate / Maintain / Toil, this project's own investment-mix framework; see the next section.
Every dimension except Kano resolves through the generic DimensionDefinition.ResolveCategory — each option carries its own independent judge criterion, and more than one satisfied criterion is reported as Ambiguous for review rather than silently resolved to one.
A custom, organization-defined dimension (e.g. a "2026 Strategic Priority" category) uses the exact same DimensionDefinition shape — no schema change needed to add one.
The investment mix: PDIM, RGT, and SRE Work¶
Product Development Investment Mix (PDIM) classifies the mix of work types within the product portfolio — not an allocation across products. "Product development" names the joint function (product management, engineering, design, docs), keeping the framework neutral between the product and engineering teams that present it together. The key distinction between its categories is whether work changes the outcome (Innovate, Improve) or the cost of producing the same outcome (Automate, and negatively, Toil).
Conceptually Maintenance = non-toil maintenance + Toil — not all maintenance is toil: a major database migration is deliberate sustaining engineering, while manually rotating certificates every month is toil. Toil is nonetheless a first-class category at the storage grain because exposing and reducing it is the point; PDIMRollup collapses Toil into Maintain for the cleaner 4-bucket executive view once toil is small.
PDIM is the assessment grain for the three investment frameworks: an opportunity judged once at PDIM resolution projects deterministically onto the other two views, keeping the executive roll-up and the engineering-health view mutually consistent.
PDIMToSREWorkis definitional — PDIM's Toil is Google's toil definition (generalized beyond production services), and every other category is enduring-value engineering work, so Toil → Toil and everything else → Engineering.PDIMToRGTis a documented convention — Innovate → Transform, Improve → Grow, Automate/Maintain/Toil → Run. RGT classifies by business outcome while PDIM classifies by work intent, so the axes can genuinely diverge; when they do, an explicit nativeRunGrowTransformDimension(orSREWorkDimension) assignment on the same assessment takes precedence over the projection.
ToilReduction makes the framework's causal loop — toil identified → automation investment → toil eliminated → capacity reclaimed → reinvested in Improve/Innovate — measurable, treating an Automate initiative as a capital-style investment:
tr := assessment.ToilReduction{
ToilSource: "manual certificate rotation",
BaselineHoursPerMonth: 40,
TargetHoursPerMonth: 2,
EvidenceIDs: []string{"EV-042"}, // evidence behind the baseline measurement
}
reclaimed := tr.HoursReclaimedPerMonth() // 38 hours/month
months, err := tr.PaybackPeriodMonths(160) // ≈4.2 months to repay 160 hours of automation effort
OKR and capability links¶
OKRContribution links an opportunity to an objective (and optionally a specific key result) from the goals/okr package, with an evidence-backed ContributionStrength (high/medium/low). CapabilityReference is a type alias for prism-core's CapabilityRef (enables/improves/dependsOn), letting an opportunity reference a prism-capability capability without a hard dependency on its full domain model.
Neither is a RankingPolicy input — OKR alignment answers "what are we trying to achieve," not "which investment is best toward it"; folding it into the ranking formula would double-count against RICE/MoSCoW. What they do enable: a Person-Day investment rollup by objective or capability (see Report Contracts), and — once opportunities ship — comparing predicted outcome against actual.
OpportunityAssessment¶
OpportunityAssessment ties everything above into one per-cycle judgment record:
type OpportunityAssessment struct {
ID string
Opportunity OpportunityRef // references a canvas.OpportunitySpec by ID
Title string
Judge *rubric.JudgeMetadata // structured-evaluation judge provenance
Cycle AssessmentCycle
MoSCoWAnswers []ThresholdAnswer
RICE *RICEAssessment // legacy ladder RICE
Compass *CompassAssessment // COMPASS-RICE — ToRankInput prefers this when set
Dimensions []DimensionAssignment
Contributions []OKRContribution
Capabilities []CapabilityReference
}
Resolved values (MoSCoW(), ComputeRICE(*a.RICE)) are always computed on demand from the raw answers — never stored redundantly, so a resolved value can never drift out of sync with the evidence behind it.
An assessment is never edited in place. A correction is a new cycle:
first := assessment.NewOpportunityAssessment("OA-018", ref, "Self-service SSO", assessedAt)
// ... first cycle's judge run fills in MoSCoWAnswers, RICE, Dimensions ...
next := first.NextCycle("OA-019", laterTime) // carries Opportunity/Title forward, sets SupersedesID
first.MarkSuperseded() // caller persists both records
HasEvidence(), HasRubricAnswers(), and EvidenceReferences() walk every answer/citation on an assessment — used to decide whether a report's evidence/rubric appendices have anything to show, and to build an EvidenceIndex across a whole assessment corpus.
Ranking¶
RankingPolicy.Rank is the deterministic ranking algorithm: MoSCoW tier first, RICE score descending within tier. The portfolio dimensions (Kano, Market Investment Horizon, Market Position, RGT, SRE Work, PDIM) and OKR/capability links never enter it — they describe portfolio composition and inform a human tie-break, never automatic rank.
inputs := []assessment.RankInput{a1.ToRankInput(), a2.ToRankInput(), a3.ToRankInput()}
ranked := assessment.DefaultRankingPolicy().Rank(inputs) // MoSCoW tier, RICE desc, ±5% tie band
Opportunities that resolve to MoSCoWWontHave/unspecified, or whose RICE isn't computable, are excluded with a reason (ExclusionWont/ExclusionRICEUncomputable) rather than silently sorted to the bottom with a fabricated score — every input the policy is given comes back out, ranked or excluded, never dropped.
RankOverride records an explicit governance decision to move an opportunity away from its CalculatedRank — a new, auditable record, never a quiet reweighting of the RICE/MoSCoW inputs to manufacture a desired number:
final := assessment.ApplyOverrides(ranked, []assessment.RankOverride{
{AssessmentID: "OA-018", FinalRank: 2, Rationale: "contractual SLA commitment", ApprovedBy: "vp-product"},
})
collisions := assessment.RankCollisions(final) // FinalRank values shared by more than one opportunity — reconcile before presenting
OpportunityRank (RankedOpportunity + FinalRank + optional Override) is what a report or portfolio review shows: calculated rank next to final rank, so a reviewer can always see when and why they diverge.
JSON Schema¶
OpportunityAssessment, Evidence, DimensionDefinition, and OpportunityRank are all available as generated JSON Schema via github.com/grokify/prism-roadmap/schema (schema.OpportunityAssessmentSchema(), etc.), matching this repo's existing PRD schema generation pattern.
Next Steps¶
- Report Contracts — turning a ranked portfolio into a report
- Feature Prioritization — the underlying RICE/Kano/MoSCoW types this package resolves into
- Opportunity Assessment (SVPG canvas) — the unrelated, same-named canvas type