posture-score-plan.md
Thread `posture-score`: plan (2026-11-04)
From posture-score-prompt.md. Everything here stays in the lab: the shared world, part/FrameworkDetail.vue, no layer release and no qdash port until the picks are made.
Built 2026-11-04 through step 3, plus the Compliance listing; see CYCLE.md Key Changes. Mark's changes to this plan when he said go:
- Q3: only the column's name changes, and its content stays. (That is what got built.)
- Items Qpoint can't address leave the table and stay under Gaps, replacing trap 4's grey row.
- The Compliance listing moved up from "where else" and shows both metrics. Score also shows on untracked frameworks, as Violations did, rather than blank.
- The window counts findings detected in the last 30 days plus any older one still open, since hiding an open finding is worse than a slightly uneven base.
The two metrics
| Compliance Posture | Compliance Score | |
|---|---|---|
| Question | How well are we set up? | How well is the activity holding to that setup? |
| Comes from | Policy Config: modes and exceptions | Findings, against how much activity was checked |
| Time | Now. It changes when someone changes config | A window: the shared when: pill, default 30 days (c128 D5) |
| Can be previewed | Yes, purely from the draft | No |
Each one covers the other's blind spot, so they should always appear together. A policy that is Off raises no findings, so its Score looks spotless while its Posture is red.
Decisions to make first
These are the places the request meets an earlier ruling or leaves room for two readings. Each has a default, and the plan runs on the defaults unless Mark changes them.
| # | Question | Default | Why it matters |
|---|---|---|---|
| Q1 | Does this amend F1? The 2026-10-26 session ruled "evidence, not compliance". The brief's success criteria say "no percentage and no 'compliant' wording", and "a single compliance %" is on its do-not-revive list. | Build with Mark's names in the lab. Record it in CYCLE.md as an amendment: a per-row bar measures configuration and makes no claim of compliance. Take it to Peterson before any qdash port. Hold any single org-wide number until then. | F1 was ruled with Peterson, Rob and Tyler in the room. Lighter alternative: Posture and Score as the column names, since the page already says Compliance. |
| Q2 | What does "Remove header items" mean? | The row above the table goes: the "Items" h2, the Coverage filter (Coverage was its only key) and the "n of n" count. Column sort replaces the filter. | Other readings are the remaining band machines or the title row. The title row carries kind and version, which F3 and F7 require. |
| Q3 | "Rename column items -> name": every framework? | Yes, "Name" on all of them. | Sets aside D8's source term in the header (Control, Criterion, Subcategory). With the h2 gone too, the page's other text has to carry the term. |
| Q4 | "Mapped Policies -> Enforcement Posture": where? | Both the band machine and the table's "Policies governing this item" column. | It sits beside Compliance Posture. They read as detail and summary: the pills are the parts, the bar is the sum. |
Compliance Posture: the model
Formula (per policy, then per row):
- For each person a policy can reach, take the mode they actually get, using
applicability()as Teams does: Enforce = 1, Observe = 0.25, Off = 0. - Average that across people. Loosening exceptions lower it and tightening pilots raise it, with no special case.
- Each live advanced exception (the allow of one resource) takes a flat 10 points off, because the lab can't say how much activity one allow covers.
- A row is the plain average of its mapped policies. Its hover lists them: "3 enforced, 1 observed, Platform team at Observe by exception".
Colours (Mark's rule, with the thresholds made exact): green at exactly 100%, orange below that, red under 25%. An all-Observe row lands exactly at 25%, so it is orange, and red always means part of the row is Off. The number always sits beside the bar, so colour never carries it alone.
Dry run on the lab's story world (the end of runStory(RULED); 19 policies, 14 named subjects; advanced exceptions not yet counted):
| Framework | Green | Orange | Red | No policy | Plain average of rows |
|---|---|---|---|---|---|
| OWASP LLM (tracked) | 1 | 6 | 3 | 0 | 50% |
| OWASP Agentic | 0 | 10 | 0 | 0 | 54% |
| NIST AI RMF | 1 | 13 | 1 | 3 | 54% |
| ISO 27001 | 0 | 1 | 1 | 1 | 43% |
| SOC 2 | 0 | 0 | 0 | 3 | none |
The bars vary, so the rule does tell rows apart. The run turned up four traps:
- Observe-only policies can never reach 100%. 8 of the 19 policies can't enforce (
enforceable: false). Default: these count as full when they are observing, because that is all they can do. Without this, no row mapped to one can ever go green. - Pills and bars will disagree.
standing()reports the strongest setting anywhere, soP-DATA-BOUNDARYandP-FILES(mode Off, one enforcing exception) read "enforced" while reaching 14% of people.P-MCPreads "enforced" at 43%. Default: the pill shows the policy's own mode, and the bar and its hover carry the exceptions. - Procedural rows can go green. A row can be fully configured and still need process Qpoint doesn't provide (one each in OWASP LLM and NIST). Default: it stays green, since Posture measures configuration, but the note moves from the removed Coverage cell to under the bar ("needs process too"). "Mapping never implies Qpoint alone satisfies" has to survive somewhere.
- No policy isn't 0%. Rows with no mapping show a grey "No Qpoint policy", not a red bar, and they stay out of rollups. The Gaps section stays. A row whose policies are all Off is the red one.
Left out for now: enforcement reach (rollout.reach, an Enforce that has reached 8 of 10 devices). The data exists, and it's the next honest refinement if the bar feels too generous.
Non-policy evidence is also unaddressed: the redline's "Evidenced" state and the brief's "strongest evidence shouldn't read as a gap" both still have no home. Peterson's intents may settle it.
Compliance Score: the options
The missing piece is the denominator. The lab world has no count of what was checked: only session summaries, plus daily sessions and tokens in reports-c128.ts. In qdash, rule signals look aggregated ("one row per tool + decision group", "one row per host"), so rule evaluations don't equal calls. session.tool_calls and provider requests do exist. I read the signal comments and API types, not the pipeline, so a real count of checks needs confirming with the backend. In the lab the number is seeded per policy per day, and marked with the existing projected ring.
What every option has to get right:
- Off shows "—", never clean. Nothing was looking.
- Rollups count distinct findings. One policy maps to many rows: OWASP LLM's rows sum to 8 open findings against 6 distinct, NIST's to 23 against 6.
- Same window on both sides. "Open now" over "checks in 30 days" mixes two times. Default: findings detected in the window, with how many are still open.
- Blocked isn't failed. D16's Enforced findings mean the policy worked. Default: they count, and the hover splits blocked from observed. Mark can rule otherwise.
Options, built side by side behind ?v.score= (the mechanism Teams uses for ?v.posture=). The sample figures are illustrative.
| Reads as | Good | Weak | |
|---|---|---|---|
| A. Severity box + base | 2 High · 1 Med / "of 30.4k checks" | Keeps today's Findings box, and the base adds scale | Most ink in the row |
| B. Two numbers | 30.4k checks · 4 findings | Mark's suggestion, and plainest | Loses severity; a Critical reads like a Low |
| C. Clean sessions | 1,204 sessions · 4 with findings | Sessions dedupe across policies and finding.sessionId already exists, so it's likeliest to be real soon | Install-style policies have no session |
| D. Percentage | 99.99% clean + bar | Matches Posture's shape | Findings are rare, so every row reads 99.9-something and nothing separates |
| E. Rate | 1.3 per 10k checks | Compares rows of very different volume | Needs learning; not a number anyone has a feel for |
| F. 0–100 score | 92 | Literally a score | Weights are invented, and it repeats what c128 removed from the board report (its composite score) |
Recommendation going in: A, with C's base if the backend can't give checks. D is worth seeing so it can be ruled out by looking. E is the likely answer for Teams rather than this page.
Build
- Model. Add
postureOf()andscoreOf()besidecoverage()ingovernance-fixture.ts, plus seeded check counts. Addscripts/c123-metrics-check.mtscovering the four traps, the thresholds and distinct rollups. - Framework detail (
part/FrameworkDetail.vue; the record page shares it):- Remove the Coverage machine and column, and the row above the table (Q2).
- Columns: Name · Enforcement Posture · Compliance Posture · Compliance Score, with Findings folded into Score.
- Add the posture bar: lab-local, composed from
DataSegmentBar. The layer'sPolicyCompletenessCellis grape and hard-codes "Controls", so it doesn't fit. - Make Posture and Score sortable.
- Rewrite "What coverage means" as "How these are worked out".
coverage()stays, for Gaps.
- Score options behind
?v.score=, A–F. - Review with Mark: pick a Score, confirm the formula and the Q1 wording.
- Apply elsewhere from the picks below, one surface at a time.
- Record: run
scripts/tours-walk.mjs(the OWASP Agentic tour stops on the Compliance row), add captures and theCYCLE.mdKey Changes entry. Updateexplainin the guide if it defines coverage. Extract the bar to the layer only once it has settled. Deploy when asked.
Checked in the browser on the running :3000, in light and dark, at 768–1600px. The tree is shared with other sessions, so check for foreign staged paths before any commit.
Where else these should apply
From walking the lab's pages. Ranked by how much each one gains.
| # | Surface | Posture | Score | Suggestion |
|---|---|---|---|---|
| 1 | Framework detail band | Yes | Yes | Compliance Posture takes Coverage's seat, Score replaces Open findings, and the Enforcement Posture donut stays. Otherwise the band and table tell different stories. |
| 2 | Compliance listing (/mock/compliance) | Yes | Yes | Replaces the Completeness % and Violations columns, which also finishes X5. This is the natural place to compare frameworks. Untracked frameworks show Posture as "what you'd have", with Score blank. |
| 3 | Exceptions and the Policy Config draft | Preview | No | "This exception takes OWASP LLM01 from 100% to 80%." The weakening shows at the moment of decision, in the risk-exception language Peterson asked for. Same for Track: "SOC 2 arrives at 25%". Computed purely from the draft, so cheap. Exception UI is c126's. |
| 4 | Teams listing and detail | Already there | Yes | The Teams PostureBar is this idea with different colours. Keep the ink bar and org ticks, since it shows mix and departure, but derive from the same fold. Add Score as a rate: it's the one view where normalizing by volume decides the comparison. |
| 5 | Posture report (reports/posture/Body.vue) | Yes | Yes | Section 8 shows counts with "no % (c123)", so it has to follow Q1. Over time is what problem:prove-posture-over-time asks for, and both metrics belong here as lines. c128's, and it needs modes replayed from Policy Config history. |
| 6 | Policy detail | No | Yes | The policy's Mode section already says its posture. Score gives its Findings section a base: "4 of 30.4k checks". c126's. |
| 7 | Policies listing | No | Maybe | The mode column already says it. A Score column only if the listing has room after c126's pass. |
| 8 | Home / index | Hold | Hold | A micro here is the most literal "single compliance %". If anything, "2 of 4 frameworks under 50%", and only after Q1. |
| — | Findings, Finding detail, Inventory, Endpoints, Cost | No | No | Findings is what Score is made of, so it would be circular. The others aren't framework-shaped. |
The word "posture"
It already means three things, and this adds a fourth:
- qdash
/postureand its fleet score are about launch settings on harnesses. - The c128 Posture report opens on that same score (78, with "what lowers the score"), so a report would show Posture score, Compliance Posture and Compliance Score together.
- Teams' posture is modes by team, the same idea as the new metric.
Compliance Posture is clear on framework pages. The report is where it collides, so I'd take that up at step 5 and rename the fleet one ("Fleet score" or similar) rather than the new one. X11 already lists /posture as part of the Frankenstein.