Compare commits

...

3 Commits

Author SHA1 Message Date
Julien Calixte
7c2da0f7ee docs(context): note the plan span in the Week definition 2026-06-17 01:40:39 +02:00
Julien Calixte
d499433f93 test(model): cover the authored plan span
Add coverage for start/end extending, snapping, never-clipping, and the
empty-plan case. The bundled sample now demonstrates the keys, so its
expected week range moves to the authored bounds.
2026-06-17 01:40:34 +02:00
Julien Calixte
d95f0a78c7 feat(model): add optional start/end plan span
Optional top-level `start`/`end` dates widen the plan's week range with
lead-in / trailing columns. They only extend the auto-fitted range, never
narrowing it or clipping a Feature, so a momentarily-tight bound can't hide
work in progress.
2026-06-17 01:40:29 +02:00
6 changed files with 51 additions and 3 deletions

View File

@@ -37,7 +37,7 @@ A Feature's *current* delivery confidence (a snapshot, overwritten each review):
_Avoid_: health, RAG, risk _Avoid_: health, RAG, risk
**Week**: **Week**:
A column of the plan: one real calendar week, identified and labelled by the date of its first workday (Monday). Columns run contiguously from the earliest Feature start to the last marker or Milestone — empty weeks in between are still drawn. A column of the plan: one real calendar week, identified and labelled by the date of its first workday (Monday). Columns run contiguously from the earliest Feature start to the last marker or Milestone — empty weeks in between are still drawn. An optional authored **start**/**end** widens this span with lead-in or trailing empty Weeks; it only ever extends the range, never narrowing it or hiding a Feature.
_Avoid_: column, period, sprint _Avoid_: column, period, sprint
**Now line**: **Now line**:

View File

@@ -3,6 +3,12 @@
// 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"
# Optional plan span: pad the plan with lead-in / trailing weeks.
# Rule: start ≤ every Feature's start, and end ≥ every Feature's last week.
# Omit both to auto-fit the columns to the Features and Milestones.
start = 2026-05-25
end = 2026-08-03
# A Feature: start week, Original Estimate (the immovable baseline), then any # A Feature: start week, Original Estimate (the immovable baseline), then any
# Re-estimates, an optional Delivery, and an optional Learning / Status. # Re-estimates, an optional Delivery, and an optional Learning / Status.
# Dates are TOML date literals; any day is snapped to that week's Monday. # Dates are TOML date literals; any day is snapped to that week's Monday.

View File

@@ -26,6 +26,8 @@ export function parseMacroplan(source: string): RawPlan {
return { return {
title: data.title != null ? String(data.title) : 'Untitled Macroplan', title: data.title != null ? String(data.title) : 'Untitled Macroplan',
start: data.start != null ? toYmdOr('plan', 'start', data.start) : undefined,
end: data.end != null ? toYmdOr('plan', 'end', data.end) : undefined,
features, features,
milestones, milestones,
} }

View File

@@ -111,8 +111,8 @@ describe('bar extent relative to now', () => {
describe('plan derivation', () => { describe('plan derivation', () => {
it('derives a contiguous week range and places the now line', () => { it('derives a contiguous week range and places the now line', () => {
const plan = buildPlan(parseMacroplan(SAMPLE_PLAN), TODAY) const plan = buildPlan(parseMacroplan(SAMPLE_PLAN), TODAY)
expect(plan.weeks[0]).toBe('2026-06-01') // earliest start expect(plan.weeks[0]).toBe('2026-05-25') // authored span start (lead-in before earliest Feature)
expect(plan.weeks.at(-1)).toBe('2026-07-20') // latest marker (Payments delivery) expect(plan.weeks.at(-1)).toBe('2026-08-03') // authored span end (trailing past last marker)
// contiguous, weekly // contiguous, weekly
expect(plan.weeks).toContain('2026-06-29') expect(plan.weeks).toContain('2026-06-29')
expect(plan.nowWeek).toBe('2026-06-15') expect(plan.nowWeek).toBe('2026-06-15')
@@ -128,6 +128,40 @@ describe('plan derivation', () => {
}) })
}) })
describe('authored plan span (start / end)', () => {
const body = '[[feature]]\nname="X"\nstart=2026-06-08\noriginal=2026-06-15\ndelivered=2026-06-15\n'
it('extends the range earlier to `start` and later to `end`', () => {
const plan = buildPlan(parseMacroplan(`start = 2026-06-01\nend = 2026-06-29\n${body}`), TODAY)
expect(plan.weeks).toEqual(weekRange('2026-06-01', '2026-06-29'))
expect(plan.weeks[0]).toBe('2026-06-01') // before the earliest Feature week (06-08)
expect(plan.weeks.at(-1)).toBe('2026-06-29') // after the last marker (06-15)
})
it('snaps authored bounds to their Monday', () => {
const plan = buildPlan(parseMacroplan(`start = 2026-06-03\nend = 2026-06-24\n${body}`), TODAY)
expect(plan.weeks[0]).toBe('2026-06-01') // Wed 06-03 → Mon 06-01
expect(plan.weeks.at(-1)).toBe('2026-06-22') // Wed 06-24 → Mon 06-22
})
it('only extends — a marker outside the authored bounds is never clipped', () => {
// end 06-08 is before the Feature's 06-15 delivery → the range still includes it
const plan = buildPlan(parseMacroplan(`start = 2026-06-08\nend = 2026-06-08\n${body}`), TODAY)
expect(plan.weeks[0]).toBe('2026-06-08')
expect(plan.weeks.at(-1)).toBe('2026-06-15')
})
it('renders an empty plan across the authored bounds when there are no Features', () => {
const plan = buildPlan(parseMacroplan('start = 2026-06-01\nend = 2026-06-22\n'), TODAY)
expect(plan.rows).toHaveLength(0)
expect(plan.weeks).toEqual(weekRange('2026-06-01', '2026-06-22'))
})
it('rejects a non-date span bound', () => {
expect(() => parseMacroplan('start = 123\n')).toThrow(/start/)
})
})
describe('parse validation', () => { describe('parse validation', () => {
it('rejects a feature missing its Original Estimate', () => { it('rejects a feature missing its Original Estimate', () => {
expect(() => parseMacroplan('[[feature]]\nname = "A"\nstart = 2026-06-01\n')).toThrow( expect(() => parseMacroplan('[[feature]]\nname = "A"\nstart = 2026-06-01\n')).toThrow(

View File

@@ -25,7 +25,11 @@ export function buildPlan(raw: RawPlan, today: Date | string = new Date()): Plan
}) })
// Range: earliest start to last marker/milestone (CONTEXT.md). Empty weeks drawn. // Range: earliest start to last marker/milestone (CONTEXT.md). Empty weeks drawn.
// Optional authored `start`/`end` widen this span with lead-in / trailing weeks;
// they only ever extend it — a Feature or marker outside them is never clipped.
const allWeeks: WeekId[] = [] const allWeeks: WeekId[] = []
if (raw.start != null) allWeeks.push(mondayOf(raw.start))
if (raw.end != null) allWeeks.push(mondayOf(raw.end))
for (const r of rows) { for (const r of rows) {
allWeeks.push(r.startWeek, r.barEndWeek) allWeeks.push(r.startWeek, r.barEndWeek)
for (const mk of r.markers) allWeeks.push(mk.week) for (const mk of r.markers) allWeeks.push(mk.week)

View File

@@ -23,6 +23,8 @@ export interface RawMilestone {
export interface RawPlan { export interface RawPlan {
title: string title: string
start?: string // yyyy-mm-dd — optional authored left edge of the plan's span
end?: string // yyyy-mm-dd — optional authored right edge of the plan's span
features: RawFeature[] features: RawFeature[]
milestones: RawMilestone[] milestones: RawMilestone[]
} }