← Design · c123

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 PostureCompliance Score
QuestionHow well are we set up?How well is the activity holding to that setup?
Comes fromPolicy Config: modes and exceptionsFindings, against how much activity was checked
TimeNow. It changes when someone changes configA window: the shared when: pill, default 30 days (c128 D5)
Can be previewedYes, purely from the draftNo

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.

#QuestionDefaultWhy it matters
Q1Does 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.
Q2What 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):

FrameworkGreenOrangeRedNo policyPlain average of rows
OWASP LLM (tracked)163050%
OWASP Agentic0100054%
NIST AI RMF1131354%
ISO 27001011143%
SOC 20003none

The bars vary, so the rule does tell rows apart. The run turned up four traps:

  1. 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.
  2. Pills and bars will disagree. standing() reports the strongest setting anywhere, so P-DATA-BOUNDARY and P-FILES (mode Off, one enforcing exception) read "enforced" while reaching 14% of people. P-MCP reads "enforced" at 43%. Default: the pill shows the policy's own mode, and the bar and its hover carry the exceptions.
  3. 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.
  4. 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 asGoodWeak
A. Severity box + base2 High · 1 Med / "of 30.4k checks"Keeps today's Findings box, and the base adds scaleMost ink in the row
B. Two numbers30.4k checks · 4 findingsMark's suggestion, and plainestLoses severity; a Critical reads like a Low
C. Clean sessions1,204 sessions · 4 with findingsSessions dedupe across policies and finding.sessionId already exists, so it's likeliest to be real soonInstall-style policies have no session
D. Percentage99.99% clean + barMatches Posture's shapeFindings are rare, so every row reads 99.9-something and nothing separates
E. Rate1.3 per 10k checksCompares rows of very different volumeNeeds learning; not a number anyone has a feel for
F. 0–100 score92Literally a scoreWeights 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

  1. Model. Add postureOf() and scoreOf() beside coverage() in governance-fixture.ts, plus seeded check counts. Add scripts/c123-metrics-check.mts covering the four traps, the thresholds and distinct rollups.
  2. 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's PolicyCompletenessCell is 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.
  3. Score options behind ?v.score=, A–F.
  4. Review with Mark: pick a Score, confirm the formula and the Q1 wording.
  5. Apply elsewhere from the picks below, one surface at a time.
  6. Record: run scripts/tours-walk.mjs (the OWASP Agentic tour stops on the Compliance row), add captures and the CYCLE.md Key Changes entry. Update explain in 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.

#SurfacePostureScoreSuggestion
1Framework detail bandYesYesCompliance Posture takes Coverage's seat, Score replaces Open findings, and the Enforcement Posture donut stays. Otherwise the band and table tell different stories.
2Compliance listing (/mock/compliance)YesYesReplaces 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.
3Exceptions and the Policy Config draftPreviewNo"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.
4Teams listing and detailAlready thereYesThe 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.
5Posture report (reports/posture/Body.vue)YesYesSection 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.
6Policy detailNoYesThe policy's Mode section already says its posture. Score gives its Findings section a base: "4 of 30.4k checks". c126's.
7Policies listingNoMaybeThe mode column already says it. A Score column only if the listing has room after c126's pass.
8Home / indexHoldHoldA 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, CostNoNoFindings 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 /posture and 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.

Qpoint Brand Style Guide