feat(okr): leading/lagging-balanse som rubrikk-dimensjon

This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 10:13:25 +02:00
commit 3f7ba74afa
4 changed files with 87 additions and 9 deletions

View file

@ -32,7 +32,7 @@ Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen —
## Din oppgave
Scor mot ALLE 10 dimensjonene i den kanoniske rubrikken
Scor mot ALLE 11 dimensjonene i den kanoniske rubrikken
(`${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md`,
fem ankere per dimensjon). Bær ikke et eget scorebånd — rubrikkfila eier anker → skala.
@ -43,12 +43,13 @@ fem ankere per dimensjon). Bær ikke et eget scorebånd — rubrikkfila eier ank
- **Scope** — riktig dimensjonert for én tertial
- **Alignment** — koblet oppover til org-OKR/tildelingsbrev
2. **Scor hvert Key Result** mot de 5 Key Result-dimensjonene:
2. **Scor hvert Key Result** mot de 6 Key Result-dimensjonene:
- **Målbarhet** — konkrete tall med baseline → target
- **Outcome** — reell effekt heller enn output/aktivitet
- **Ambisjon** — riktig stretch (~70 %), ikke sandbagget
- **Datakilde** — spesifisert og faktisk tilgjengelig
- **Uavhengighet** — teamet kan påvirke utfallet vesentlig (påvirkning, ikke nødvendigvis full kontroll)
- **Leading/lagging-balanse** — kobler en leading-indikator som måles underveis til et lagging-utfall som bekrefter effekten, med erkjent tidsforsinkelse
3. **Sjekk for antipatterns** fra `references/okr-antipatterns.md`:
- Aktivitetsorientert

View file

@ -47,9 +47,9 @@ Scoringen bruker ÉN kanonisk sannhetskilde — bær ikke et eget scorebånd ell
inline-rubrikk her:
`${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md`
Fila har fem ankere per dimensjon (svakest → sterkest) over alle 10 dimensjonene:
Fila har fem ankere per dimensjon (svakest → sterkest) over alle 11 dimensjonene:
- **Objective (5):** Inspirerende, Klarhet, Outcome-fokus, Scope, Alignment
- **Key Result (5):** Målbarhet, Outcome, Ambisjon, Datakilde, Uavhengighet
- **Key Result (6):** Målbarhet, Outcome, Ambisjon, Datakilde, Uavhengighet, Leading/lagging-balanse
Les ankerbeskrivelsen per dimensjon, finn nivået OKR-en faktisk treffer, og scor 1-5.
Anker → 0-10-skala er definert i rubrikkfila («Bruk»-avsnittet): anker 1 → 1-2,

View file

@ -98,6 +98,17 @@ Måler om teamet **kan påvirke vesentlig** — om det rår over de viktigste dr
4. **Anker 4** — Teamet rår over de fleste driverne, med en mindre ekstern avhengighet.
5. **Anker 5 (sterkest)** — Teamet rår over de viktigste driverne og kan påvirke utfallet vesentlig.
### Leading/lagging-balanse
Måler om Key Result-settet kobler en **leading**-indikator (måles underveis, teamet påvirker den i perioden) til et **lagging**-utfall (bekrefter effekten, men foreligger først i etterkant). Riksrevisjonen har som kjernefunn at statlige årsrapporter gir informasjon om aktiviteter og tjenesteleveranser «og i liten grad effekter» — rene lagging-mål er vanskelige å rapportere tertialvis, og svaret er å måle begge deler bevisst, ikke å droppe effekten. KOSTRA er et typisk lagging-mål: nøkkeltallene gjelder foregående år og publiseres foreløpig 15. mars og endelig 15. juni, så de kan aldri følges opp tertialvis.
1. **Anker 1 (svakest)** — Kun aktivitetsmål; ingenting i settet sier om aktiviteten virket.
2. **Anker 2** — Kun leading-indikatorer; effekten er påstått, men ingen måler den.
3. **Anker 3** — Både leading og lagging finnes, men koblingen mellom dem er ikke uttalt.
4. **Anker 4** — Leading og lagging er koblet, men tidsforsinkelsen er ikke erkjent (lagging-utfallet behandles som om det forelå i perioden).
5. **Anker 5 (sterkest)** — Eksplisitt par av leading-indikator og lagging-utfall, med uttalt tidsforsinkelse og navngitt tidspunkt for når lagging-tallet faktisk foreligger.
> **Merk:** et lagging-mål med årlig publisering hører hjemme som *lagging*-halvdelen av paret, aldri som tertialvis KR. Se `okr-kommunal-styring.md` for KOSTRA-kadensen.
> **Avveining Outcome ↔ Uavhengighet (samfunnseffekt-KR):** Et sterkt Outcome-KR (Anker 5: reell samfunnseffekt for bruker/samfunn) er ofte et *delt* utfall teamet ikke rår fullt over — det scorer da lavt på Uavhengighet. Dette er en reell spenning, ikke en formuleringsfeil, og den skal ikke «løses» ved å svekke outcome-ambisjonen. **Done-condition:** par samfunnseffekt-KR-et (aspirational/delt) med ett eller flere **committed leading-tiltak** teamet faktisk kontrollerer. Scor da Uavhengighet på *tiltakene*, og aksepter lav Uavhengighet på selve outcome-KR-et som prisen for å måle noe som faktisk betyr noe. Et KR skal aldri nedgraderes fra outcome til ren aktivitet kun for å heve Uavhengighet-scoren.
---

View file

@ -108,9 +108,9 @@ function divergentEnumeration(line) {
// ==================== parser-sanity (jf. B1) ====================
test('parser-sanity: headingsOf finner 10 rubrikk-dimensjoner', () => {
test('parser-sanity: headingsOf finner 11 rubrikk-dimensjoner', () => {
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
assert.equal(dims.length, 10, `forventet 10 dims, fant ${dims.length}`);
assert.equal(dims.length, 11, `forventet 11 dims, fant ${dims.length}`);
});
test('parser-sanity: mdTables finner confidence-tabellen i okr-framework.md', () => {
@ -386,10 +386,10 @@ function dimTokens(body) {
return tokens;
}
// (g) F-e agent 10 dims. RED til Step 10. Utled dimensjonsnavnene, ikke hardkod.
test('(g) F-e: kvalitetssjekker-agent daekker alle 10 rubrikk-dimensjoner', () => {
// (g) F-e agent 11 dims. RED til Step 10. Utled dimensjonsnavnene, ikke hardkod.
test('(g) F-e: kvalitetssjekker-agent daekker alle 11 rubrikk-dimensjoner', () => {
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
assert.equal(dims.length, 10, `parser-sanity: forventet 10 dims, fant ${dims.length}`);
assert.equal(dims.length, 11, `parser-sanity: forventet 11 dims, fant ${dims.length}`);
const tokens = dimTokens(readDoc('agents/kvalitetssjekker-agent.md'));
const missing = dims.filter((d) => !tokens.has(d));
assert.deepEqual(missing, [], `kvalitetssjekker-agent mangler rubrikk-dims: ${missing.join(', ')}`);
@ -404,6 +404,72 @@ test('(g2) F-e: dim-vakten skiller Outcome fra Outcome-fokus', () => {
assert.ok(!tokens.has('Outcome'), '"Outcome" skal IKKE dekkes av "Outcome-fokus"');
});
// --- (g3) dimensjonstall drift-laas paa tvers av de TRE flatene (D7 steg 15) ---
//
// Rubrikkfila er sannhetskilden; kvalitet.md og kvalitetssjekker-agent.md OMTALER
// antallet i prosa. Da steg 15 la til en 11. dimensjon var det nettopp de omtalene
// som kunne bli staaende paa "10" -- samme defektklasse som (d) F-g loeser for
// antipattern-antallet: ALDRI hardkod tallet i vakten, utled det.
//
// Tre teller-akser, fordi prosaen baerer alle tre: totalen ("alle 11 dimensjonene"),
// Objective-gruppa og Key Result-gruppa. En vakt paa bare totalen ville sluppet
// gjennom "de 5 Key Result-dimensjonene" etter at gruppa ble seks.
function rubricDims() {
const body = readDoc(`${REF}/okr-quality-rubrics.md`);
const groups = { Objective: [], 'Key Result': [] };
let cur = null;
for (const line of body.split('\n')) {
const g = line.match(/^## (Objective|Key Result)-dimensjoner\s*$/);
if (g) { cur = g[1]; continue; }
if (/^## /.test(line)) { cur = null; continue; }
const d = line.match(/^### (.+?)\s*$/);
if (d && cur) groups[cur].push(d[1]);
}
return groups;
}
test('(g3) dimensjonstall i kvalitet + kvalitetssjekker == faktisk antall i rubrikkfila', () => {
const groups = rubricDims();
const obj = groups.Objective.length;
const kr = groups['Key Result'].length;
const total = obj + kr;
// parser-sanity: gruppene skal finnes og summere til overskrifts-tellingen.
assert.ok(obj > 0 && kr > 0, `parser-sanity: fant ${obj} Objective- og ${kr} KR-dims`);
assert.equal(
total,
headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /).length,
'gruppe-summen skal daekke alle ###-dimensjoner (ingen dim utenfor de to gruppene)',
);
// Hver dimensjon skal ha NOEYAKTIG fem ankere -- ankerformen er det rubrikkfila lover.
const blocks = readDoc(`${REF}/okr-quality-rubrics.md`).split(/^### /m).slice(1);
const feilAnkertall = blocks
.map((b) => [b.split('\n')[0].trim(), (b.match(/^\d+\. \*\*Anker \d/gm) ?? []).length])
.filter(([, n]) => n !== 5)
.map(([navn, n]) => `${navn}: ${n} ankere`);
assert.deepEqual(feilAnkertall, [], `hver dimensjon skal ha fem ankere:\n${feilAnkertall.join('\n')}`);
// Drift-laasen: hvert tall prosaen oppgir maa matche den utledede tellingen.
const akser = [
[/(\d+)\s+dimensjonene?\b/gi, total, 'totalt antall dimensjoner'],
[/\bObjective\s*\((\d+)\)/g, obj, 'Objective-gruppa'],
[/\bKey Result\s*\((\d+)\)/g, kr, 'Key Result-gruppa'],
[/\bde (\d+) Objective-dimensjonene\b/g, obj, 'Objective-gruppa'],
[/\bde (\d+) Key Result-dimensjonene\b/g, kr, 'Key Result-gruppa'],
];
const drift = [];
for (const f of ['commands/kvalitet.md', 'agents/kvalitetssjekker-agent.md']) {
const body = readDoc(f);
for (const [re, forventet, hva] of akser) {
for (const m of body.matchAll(re)) {
if (Number(m[1]) !== forventet) drift.push(`${f}: "${m[0].trim()}" != ${forventet} (${hva})`);
}
}
}
assert.deepEqual(drift, [], `dimensjonstall-drift (rubrikkfila = ${obj}+${kr}=${total}):\n${drift.join('\n')}`);
});
// (h) F-a fjerning. RED til Step 7. Score->modenhet-avledning borte; sandbagging-trendsignal til stede.
test('(h) F-a: ingen score->modenhet-avledning; sandbagging-trendsignal til stede', () => {
const problems = [];