Portfolio environment analysis
One environment backs every task. Each task gives you a scope (in its
prompt.txt) and an exact output shape (in its payloads/answer_template.json).
Your job: pull the data over HTTP, apply the shared rules below, and emit a single
JSON object matching that template. Output JSON only — no prose.
reference.md holds the full data model and decision tables.
portfolio_toolkit.py implements every rule; prefer it over re-deriving the math.
Workflow
- Read the task, not your memory of past tasks. From the task
prompt.txt
extract the scope parameters (scope_id, quarter, teams, product areas,
categories, as-of date, recent-window days, release id — whichever apply).
From payloads/answer_template.json extract the exact keys, ordering rules,
rounding, and enum values the answer must use. Different tasks in the same
family use different key names and different subsets of fields — follow the
template in front of you.
- Get access: read
environment_access.md for the Base URL and
X-Env-Token. GET endpoints are open; only POST /api/query needs the token.
- Fetch the collections you need (usually just
GET /api/work-items plus
one or two others). portfolio_toolkit.fetch_all() grabs them all.
- Classify & filter using the shared conventions (below).
- Compute with the matching task-family entry point.
- Format the result into the template's exact keys, order, and rounding.
Copy nothing from any example answer — every value is derived from the live
data for the scope you were given.
Fast path:
import portfolio_toolkit as pt
data = pt.fetch_all() # reads ./environment_access.md
res = pt.portfolio_mix(data, scope_id=SCOPE, teams=TEAMS,
product_areas=AREAS, quarter=QUARTER)
# map res -> the keys/order this task's answer_template.json requires
Shared conventions (apply to every family)
- Authoritative fields only. Use
status, work_type, labels, title,
team, product_area, owner, severity, created_at, closed_at,
due_at, duplicate_of, release_id, milestone_id. Never trust
mirror_status or legacy_category — they are stale mirror/export fields kept
to mislead. (The portfolio template's ignored_mirror_status_and_legacy_category
flag is always true.)
- Complete/done =
status ∈ {Closed, Done, Deployed, Verified}. Everything
else (Backlog, In Progress, Review, Reopened) is not complete.
- Duplicate =
status == "Duplicate" or duplicate_of set → excluded
from primary work; duplicate_of names its canonical id.
- Cancelled =
status == "Cancelled" → excluded from primary work.
- Category classification: gather category signals from
work_type + labels
title, keep the highest-precedence one, precedence
Security > Reliability > TechDebt > NewFeature (NewFeature = fallback). Full
work_type map and keyword lists are in reference.md / classify().
Family A — portfolio-mix review
Scope: scope_id, quarter, teams, product_area(s). Target mix = the
mix_targets row whose scope_id equals the task's scope_id (percentages are
stored as fractions 0–1 → ×100).
- In scope = team in scope, product_area in scope,
closed_at inside the
quarter (Q4 = Oct 1–Dec 31, etc.).
- Split in-scope records: duplicates and cancelled are excluded and reported
(as
excluded_duplicate_ids / excluded_cancelled_ids, or combined as
excluded_distractor_ids — whichever the template names). Remaining records
with a done status are the included mix.
- Classify each included item; counts are item counts, not story points.
actual_pct = count / total_included × 100 (1 dp); gap_pct = actual − target
(1 dp). Under-invested / deficit = categories with negative gap, ordered
most-negative first. largest_deficit_category = the most-negative gap.
- Follow-up / recommendation:
- Any negative gaps →
REBALANCE_CAPACITY, primary = largest negative gap,
secondary = next negative (or null), rationale LARGEST_NEGATIVE_GAP.
- No negative gaps →
MAINTAIN_CURRENT_MIX, rationale NO_NEGATIVE_GAPS.
- When a template wants
owner_team, use the team owning the most included
work in the deficit category (ties alphabetical).
- Ordering:
included_work_item_ids (and excluded lists) by closed_at
ascending then id ascending; category tables in the fixed order
NewFeature, TechDebt, Reliability, Security; team/area lists per the template's
stated order (some say alphabetical, some give an explicit order).
Entry point: portfolio_mix(data, scope_id, teams, product_areas, quarter) →
included ids, counts, percentages, per-category table, under-invested list,
deficit category, follow-up action, owner team, and the excluded id lists.
Family B — SLA aging review
Scope: teams, categories (Reliability/Security), as_of date, recent closed
window_days.
- Primary population = team in scope,
classify() ∈ categories, not
cancelled, created_at ≤ as_of, and either still open or closed within
[as_of − window_days, as_of]. Duplicates split off into
duplicate_clusters keyed by their duplicate_of; the rest are primary.
- Overdue: closed →
closed_at > due_at; open → due_at < as_of (strict).
- Aging buckets on age
= (closed_at or as_of) − created_at:
0-3, 4-7, 8-14, 15-30, 31+ (inclusive), computed over all primary items.
- breach_rate = overdue primary ÷ primary, 3 decimals.
- team_overdue_counts (teams alphabetical); overdue_counts_by_severity
(S1–S4); top_hotspot = (team, owner) pair with most overdue, owner
UNASSIGNED if missing; missing_owner_ids = primary with no owner.
- escalation_queue_ids = overdue primary in priority order: severity ascending
(S1 first), then days-overdue
(closed_at or as_of) − due_at descending, then
id ascending.
- Ordering: id lists lexicographic unless a field defines its own order
(escalation queue); clusters sorted by
primary_id, duplicate_ids sorted.
Entry point: sla_aging(data, teams, categories, as_of, window_days) → primary
ids, overdue ids, aging buckets, team counts, severity counts, hotspot,
escalation queue, missing-owner ids, clusters, breach rate.
Family C — release-readiness assessment
Scope: a release_id. Build only from authoritative status/blocker/dependency
data (ignore mirror fields).
- milestone_completion (sorted by
milestone_id asc): per milestone, primary
= release items with that milestone_id excluding duplicates/cancelled;
complete_primary uses the done set; completion_pct 1 dp.
- readiness_score = total complete primary ÷ total primary across milestones,
3 dp.
- blocker_cause_counts: unresolved high-impact blockers only
(
severity ∈ {High, Critical}, resolved_at null), keyed by exact cause.
- gating_work_item_ids: non-complete primary release items that have an
unresolved high-impact blocker (sorted, unique).
- critical_dependency_chains: from each gating item follow
dependencies
edges to any non-complete dependency; each path
[gating_id, …, non_complete_dep_id]; sort lexicographically. Empty when every
gating item's dependencies are already complete.
- ship_decision:
NO_SHIP if any gating items, critical chains, or unresolved
Critical blocker; SHIP only when readiness is 1.0 with no unresolved
high-impact blockers and nothing gating; else SHIP_WITH_WATCH.
Entry point: release_readiness(data, release_id) returns exactly these fields.
Output discipline
- Emit one JSON object matching the task's
answer_template.json — its keys,
nesting, enums, ordering, and rounding. Nothing extra, no prose.
- Respect each template's rounding (mix/gaps 1 dp as percentage points; breach and
readiness 3 dp) and its stated sort orders.
- Recompute everything from the live environment for the given scope. Do not
copy ids, counts, percentages, or scope values from any example — they belong to
other scopes and will be wrong here.
1---2name: portfolio-env-analysis-43description: Answer read-only analysis questions against the shared engineering-portfolio environment (work items, mix targets, SLA policy, releases, milestones, blockers, dependencies). Covers three task families — portfolio-mix reviews, SLA aging/breach reviews, and release-readiness assessments — each returning a single JSON object that must match the task's own answer_template.json. Use whenever a task points at that environment and asks for a portfolio mix vs a target, an SLA/overdue/escalation readout, or a ship decision.4---56# Portfolio environment analysis78One environment backs every task. Each task gives you a **scope** (in its9`prompt.txt`) and an exact **output shape** (in its `payloads/answer_template.json`).10Your job: pull the data over HTTP, apply the shared rules below, and emit a single11JSON object matching that template. Output JSON only — no prose.1213`reference.md` holds the full data model and decision tables.14`portfolio_toolkit.py` implements every rule; prefer it over re-deriving the math.1516## Workflow17181. **Read the task**, not your memory of past tasks. From the task `prompt.txt`19 extract the scope parameters (scope_id, quarter, teams, product areas,20 categories, as-of date, recent-window days, release id — whichever apply).21 From `payloads/answer_template.json` extract the exact keys, ordering rules,22 rounding, and enum values the answer must use. Different tasks in the same23 family use different key names and different subsets of fields — follow the24 template in front of you.252. **Get access**: read `environment_access.md` for the Base URL and26 `X-Env-Token`. GET endpoints are open; only `POST /api/query` needs the token.273. **Fetch** the collections you need (usually just `GET /api/work-items` plus28 one or two others). `portfolio_toolkit.fetch_all()` grabs them all.294. **Classify & filter** using the shared conventions (below).305. **Compute** with the matching task-family entry point.316. **Format** the result into the template's exact keys, order, and rounding.32 Copy nothing from any example answer — every value is derived from the live33 data for the scope you were given.3435Fast path:3637```python38import portfolio_toolkit as pt39data = pt.fetch_all() # reads ./environment_access.md40res = pt.portfolio_mix(data, scope_id=SCOPE, teams=TEAMS,41 product_areas=AREAS, quarter=QUARTER)42# map res -> the keys/order this task's answer_template.json requires43```4445## Shared conventions (apply to every family)4647- **Authoritative fields only.** Use `status`, `work_type`, `labels`, `title`,48 `team`, `product_area`, `owner`, `severity`, `created_at`, `closed_at`,49 `due_at`, `duplicate_of`, `release_id`, `milestone_id`. **Never** trust50 `mirror_status` or `legacy_category` — they are stale mirror/export fields kept51 to mislead. (The portfolio template's `ignored_mirror_status_and_legacy_category`52 flag is always `true`.)53- **Complete/done** = `status ∈ {Closed, Done, Deployed, Verified}`. Everything54 else (`Backlog`, `In Progress`, `Review`, `Reopened`) is not complete.55- **Duplicate** = `status == "Duplicate"` **or** `duplicate_of` set → excluded56 from primary work; `duplicate_of` names its canonical id.57- **Cancelled** = `status == "Cancelled"` → excluded from primary work.58- **Category classification**: gather category signals from `work_type` + `labels`59 + `title`, keep the highest-precedence one, precedence60 `Security > Reliability > TechDebt > NewFeature` (NewFeature = fallback). Full61 work_type map and keyword lists are in `reference.md` / `classify()`.6263## Family A — portfolio-mix review6465Scope: `scope_id`, `quarter`, `teams`, `product_area(s)`. Target mix = the66`mix_targets` row whose `scope_id` equals the task's scope_id (percentages are67stored as fractions 0–1 → ×100).6869- **In scope** = team in scope, product_area in scope, `closed_at` inside the70 quarter (Q4 = Oct 1–Dec 31, etc.).71- Split in-scope records: duplicates and cancelled are **excluded** and reported72 (as `excluded_duplicate_ids` / `excluded_cancelled_ids`, or combined as73 `excluded_distractor_ids` — whichever the template names). Remaining records74 with a done status are the **included mix**.75- Classify each included item; counts are **item counts, not story points**.76- `actual_pct = count / total_included × 100` (1 dp); `gap_pct = actual − target`77 (1 dp). **Under-invested / deficit** = categories with negative gap, ordered78 most-negative first. **largest_deficit_category** = the most-negative gap.79- Follow-up / recommendation:80 - Any negative gaps → `REBALANCE_CAPACITY`, primary = largest negative gap,81 secondary = next negative (or null), rationale `LARGEST_NEGATIVE_GAP`.82 - No negative gaps → `MAINTAIN_CURRENT_MIX`, rationale `NO_NEGATIVE_GAPS`.83 - When a template wants `owner_team`, use the team owning the most included84 work in the deficit category (ties alphabetical).85- **Ordering**: `included_work_item_ids` (and excluded lists) by `closed_at`86 ascending then id ascending; category tables in the fixed order87 NewFeature, TechDebt, Reliability, Security; team/area lists per the template's88 stated order (some say alphabetical, some give an explicit order).8990Entry point: `portfolio_mix(data, scope_id, teams, product_areas, quarter)` →91included ids, counts, percentages, per-category table, under-invested list,92deficit category, follow-up action, owner team, and the excluded id lists.9394## Family B — SLA aging review9596Scope: `teams`, `categories` (Reliability/Security), `as_of` date, recent closed97`window_days`.9899- **Primary population** = team in scope, `classify() ∈ categories`, not100 cancelled, `created_at ≤ as_of`, and **either still open or closed within101 `[as_of − window_days, as_of]`**. Duplicates split off into102 `duplicate_clusters` keyed by their `duplicate_of`; the rest are primary.103- **Overdue**: closed → `closed_at > due_at`; open → `due_at < as_of` (strict).104- **Aging buckets** on age `= (closed_at or as_of) − created_at`:105 `0-3, 4-7, 8-14, 15-30, 31+` (inclusive), computed over all primary items.106- **breach_rate** = overdue primary ÷ primary, **3 decimals**.107- **team_overdue_counts** (teams alphabetical); **overdue_counts_by_severity**108 (S1–S4); **top_hotspot** = (team, owner) pair with most overdue, owner109 `UNASSIGNED` if missing; **missing_owner_ids** = primary with no owner.110- **escalation_queue_ids** = overdue primary in priority order: severity ascending111 (S1 first), then days-overdue `(closed_at or as_of) − due_at` descending, then112 id ascending.113- **Ordering**: id lists lexicographic unless a field defines its own order114 (escalation queue); clusters sorted by `primary_id`, `duplicate_ids` sorted.115116Entry point: `sla_aging(data, teams, categories, as_of, window_days)` → primary117ids, overdue ids, aging buckets, team counts, severity counts, hotspot,118escalation queue, missing-owner ids, clusters, breach rate.119120## Family C — release-readiness assessment121122Scope: a `release_id`. Build only from authoritative status/blocker/dependency123data (ignore mirror fields).124125- **milestone_completion** (sorted by `milestone_id` asc): per milestone, primary126 = release items with that milestone_id excluding duplicates/cancelled;127 `complete_primary` uses the done set; `completion_pct` 1 dp.128- **readiness_score** = total complete primary ÷ total primary across milestones,129 3 dp.130- **blocker_cause_counts**: unresolved high-impact blockers only131 (`severity ∈ {High, Critical}`, `resolved_at` null), keyed by exact `cause`.132- **gating_work_item_ids**: non-complete primary release items that have an133 unresolved high-impact blocker (sorted, unique).134- **critical_dependency_chains**: from each gating item follow `dependencies`135 edges to any non-complete dependency; each path136 `[gating_id, …, non_complete_dep_id]`; sort lexicographically. Empty when every137 gating item's dependencies are already complete.138- **ship_decision**: `NO_SHIP` if any gating items, critical chains, or unresolved139 Critical blocker; `SHIP` only when readiness is 1.0 with no unresolved140 high-impact blockers and nothing gating; else `SHIP_WITH_WATCH`.141142Entry point: `release_readiness(data, release_id)` returns exactly these fields.143144## Output discipline145146- Emit **one JSON object** matching the task's `answer_template.json` — its keys,147 nesting, enums, ordering, and rounding. Nothing extra, no prose.148- Respect each template's rounding (mix/gaps 1 dp as percentage points; breach and149 readiness 3 dp) and its stated sort orders.150- Recompute everything from the live environment for the given scope. Do **not**151 copy ids, counts, percentages, or scope values from any example — they belong to152 other scopes and will be wrong here.