refactor(model): name Feature status by domain, not RAG color

StatusLevel held green/orange/red — the RAG vocabulary CONTEXT.md
explicitly lists under _Avoid_ for the Status term, leaking a
presentation concern into the model and authored TOML. Replace with
on-track/at-risk/off-track; color now lives only in the renderer's
tone() mapping. Breaking change to authored .toml status values.
This commit is contained in:
Julien Calixte
2026-06-18 18:13:53 +02:00
parent 64efc91392
commit 40ee9f509c
6 changed files with 14 additions and 20 deletions

View File

@@ -33,7 +33,7 @@ A free-text takeaway captured against a Feature once it is delivered — what th
_Avoid_: retro note, lesson, postmortem _Avoid_: retro note, lesson, postmortem
**Status**: **Status**:
A Feature's *current* delivery confidence (a snapshot, overwritten each review): **green** (all good), **orange** (in trouble but we have a plan), **red** (in trouble and we have no plan). May carry a comment. Applies only while in-flight; once delivered, the **Learning** takes over and the Status is dropped. An overdue Feature (past its latest estimate, not delivered) is expressed through an orange/red Status, not a dedicated symbol. A Feature's *current* delivery confidence (a snapshot, overwritten each review): **on-track** (all good), **at-risk** (in trouble but we have a plan), **off-track** (in trouble and we have no plan). May carry a comment. Applies only while in-flight; once delivered, the **Learning** takes over and the Status is dropped. An overdue Feature (past its latest estimate, not delivered) is expressed through an at-risk/off-track Status, not a dedicated symbol.
_Avoid_: health, RAG, risk _Avoid_: health, RAG, risk
**Week**: **Week**:

View File

@@ -58,20 +58,14 @@ const gridStyle = computed(() => ({
function tone(row: FeatureRow): Tone { function tone(row: FeatureRow): Tone {
if (row.delivered) return row.onTime ? 'success' : 'error' if (row.delivered) return row.onTime ? 'success' : 'error'
if (row.status === 'green') return 'success' if (row.status === 'on-track') return 'success'
if (row.status === 'orange') return 'warning' if (row.status === 'at-risk') return 'warning'
if (row.status === 'red') return 'error' if (row.status === 'off-track') return 'error'
return 'neutral' return 'neutral'
} }
function statusWord(row: FeatureRow): string { function statusWord(row: FeatureRow): string {
return row.status === 'green' return row.status ? row.status.replace('-', ' ') : 'in flight'
? 'on track'
: row.status === 'orange'
? 'at risk'
: row.status === 'red'
? 'blocked'
: 'in flight'
} }
function markerAt(row: FeatureRow, w: WeekId): MarkerKind | null { function markerAt(row: FeatureRow, w: WeekId): MarkerKind | null {

View File

@@ -1,5 +1,5 @@
// Default Macroplan shown on first load — exercises every state: // Default Macroplan shown on first load — exercises every state:
// on-time delivery, late delivery with slips, in-flight (green/orange/red), // on-time delivery, late delivery with slips, in-flight (on-track/at-risk/off-track),
// an overdue Feature, and a Milestone with unmet required Features. // an overdue Feature, and a Milestone with unmet required Features.
export const SAMPLE_PLAN = `title = "Q3 — Checkout revamp" export const SAMPLE_PLAN = `title = "Q3 — Checkout revamp"
@@ -32,7 +32,7 @@ learning = "Vendor lead time was the real constraint — derisk vendors up front
name = "Dashboard" name = "Dashboard"
start = 2026-06-01 start = 2026-06-01
original = 2026-06-08 # already past 'now' and undelivered → overdue original = 2026-06-08 # already past 'now' and undelivered → overdue
status = "red" status = "off-track"
note = "No recovery plan yet — needs an owner." note = "No recovery plan yet — needs an owner."
[[feature]] [[feature]]
@@ -40,14 +40,14 @@ name = "Search"
start = 2026-06-08 start = 2026-06-08
original = 2026-06-22 original = 2026-06-22
reestimates = [2026-07-06] # re-estimated once, still in flight → △ reestimates = [2026-07-06] # re-estimated once, still in flight → △
status = "orange" status = "at-risk"
note = "Third-party search API is flaky; spike a fallback." note = "Third-party search API is flaky; spike a fallback."
[[feature]] [[feature]]
name = "Notifications" name = "Notifications"
start = 2026-06-22 start = 2026-06-22
original = 2026-07-06 original = 2026-07-06
status = "green" status = "on-track"
[[milestone]] [[milestone]]
name = "Code freeze" name = "Code freeze"

View File

@@ -11,7 +11,7 @@ export class PlanParseError extends Error {
} }
} }
const STATUSES = ['green', 'orange', 'red'] as const satisfies readonly StatusLevel[] const STATUSES = ['on-track', 'at-risk', 'off-track'] as const satisfies readonly StatusLevel[]
// ── Field schemas ────────────────────────────────────────────────────────── // ── Field schemas ──────────────────────────────────────────────────────────
// A TOML date (smol-toml returns a Date subclass) or a yyyy-mm-dd string, // A TOML date (smol-toml returns a Date subclass) or a yyyy-mm-dd string,

View File

@@ -76,10 +76,10 @@ describe('F2 — on-time / late classification (ADR-0001)', () => {
}) })
it('in-flight (undelivered) → ◯ only, onTime null, bar ends at the furthest estimate', () => { it('in-flight (undelivered) → ◯ only, onTime null, bar ends at the furthest estimate', () => {
const r = rowOf(base('reestimates = [2026-06-29]\nstatus = "red"'), 'X') const r = rowOf(base('reestimates = [2026-06-29]\nstatus = "off-track"'), 'X')
expect(r.onTime).toBeNull() expect(r.onTime).toBeNull()
expect(r.delivered).toBe(false) expect(r.delivered).toBe(false)
expect(r.status).toBe('red') expect(r.status).toBe('off-track')
expect(r.barEndWeek).toBe('2026-06-29') // furthest open estimate expect(r.barEndWeek).toBe('2026-06-29') // furthest open estimate
expect(r.markers.some((m) => m.kind === 'original')).toBe(true) expect(r.markers.some((m) => m.kind === 'original')).toBe(true)
}) })
@@ -91,7 +91,7 @@ describe('bar extent relative to now', () => {
it('extends an overdue undelivered bar to the now week, keeping ◯ at the estimate', () => { it('extends an overdue undelivered bar to the now week, keeping ◯ at the estimate', () => {
// original 2026-06-08 is before now (week of 2026-06-15) and undelivered → overdue // original 2026-06-08 is before now (week of 2026-06-15) and undelivered → overdue
const r = rowOf(feat('status = "red"'), 'X') const r = rowOf(feat('status = "off-track"'), 'X')
expect(r.delivered).toBe(false) expect(r.delivered).toBe(false)
expect(r.barEndWeek).toBe('2026-06-15') // runs up to now, past the 06-08 estimate expect(r.barEndWeek).toBe('2026-06-15') // runs up to now, past the 06-08 estimate
expect(r.markers.find((m) => m.kind === 'original')?.week).toBe('2026-06-08') expect(r.markers.find((m) => m.kind === 'original')?.week).toBe('2026-06-08')

View File

@@ -1,6 +1,6 @@
import type { WeekId } from './week' import type { WeekId } from './week'
export type StatusLevel = 'green' | 'orange' | 'red' export type StatusLevel = 'on-track' | 'at-risk' | 'off-track'
// ── Raw model: as authored, after TOML parse + validation, before derivation ── // ── Raw model: as authored, after TOML parse + validation, before derivation ──