fix(#4439): render pending-todo 'created' as date-only, matching [date] (#4494)

* test(#4439): failing-first regression coverage for pending-todo date rendering

renderPendingTodoBullet currently echoes the todo's `created` frontmatter
verbatim into the rendered bullet's [date] bracket, so a full ISO-8601
timestamp (the actual stored shape, per add-todo.md's create_file step)
leaks through instead of the date-only format documented in
docs/reference/state-md.md and docs/COMMANDS.md. This commit adds the
regression coverage only; the renderer fix lands in the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4439): render pending-todo 'created' as date-only, matching [date]

renderPendingTodoBullet echoed the todo's created frontmatter verbatim
into the rendered STATE.md bullet. That value is always a full
ISO-8601 timestamp by design (add-todo.md's create_file step writes
init.todos' timestamp field), but docs/reference/state-md.md and
docs/COMMANDS.md document the bullet as `- [date] ...` — a short
calendar date. Added pendingTodoDateOnly(), a pure display-only
formatter: a value starting with a well-formed YYYY-MM-DD is shortened
to just that; anything else ('unknown', a malformed string) passes
through unchanged. The stored frontmatter and the JSON todos[].created
field are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(#4439): cover a non-4-digit-year near-miss for the date-only formatter

Standards review flagged a gap: the malformed-date coverage exercised
a non-padded month/day but not a non-4-digit year, the other way the
input can look almost-but-not-quite like YYYY-MM-DD. Same pass-through
code path as the existing malformed-date test; no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4439): changeset for the pending-todo date-only bullet fix

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4439): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-07 13:33:30 -04:00
committed by GitHub
parent 8b7a0b696b
commit e38d246015
3 changed files with 45 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 4494
---
**Pending-todo bullets in STATE.md now show a date, not a full timestamp** — `renderPendingTodosMarkdown` was echoing the todo's `created` frontmatter verbatim (a full ISO-8601 instant) into the rendered `[…]` bracket, instead of the date-only `[date]` format documented in `docs/reference/state-md.md` and `docs/COMMANDS.md`. (#4439)

View File

@@ -2308,8 +2308,24 @@ function pendingTodoLinkTarget(todo: Record<string, unknown>, projectRoot: strin
return rel;
}
/**
* #4439: the stored `created:` frontmatter (and the JSON `todos[].created`
* field it round-trips through) is always a full ISO-8601 timestamp, by
* design — this is the display-only seam that reformats it to the
* date-only `[date]` bullet documented in docs/reference/state-md.md and
* docs/COMMANDS.md. A value that doesn't start with a well-formed
* `YYYY-MM-DD` (the 'unknown' fallback, or any other non-conforming
* string) passes through unchanged rather than being mangled.
*/
function pendingTodoDateOnly(value: string): string {
const match = value.match(/^\d{4}-\d{2}-\d{2}/);
return match ? match[0] : value;
}
function renderPendingTodoBullet(todo: Record<string, unknown>, projectRoot?: string): string {
const date = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown'));
const date = pendingTodoDateOnly(
sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown')),
);
let area = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['area'], 'general'));
let title = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['title'], 'Untitled'));
// Strip trailing "." so the fixed "Needs ....` template below never

View File

@@ -103,6 +103,27 @@ test('renderPendingTodosMarkdown: boundary — 240 chars (limit) is not truncate
assert.equal(line, `- [2026-09-01] [api] ${title} — [todo file](${todo.path})`);
});
test('renderPendingTodosMarkdown: full ISO-8601 \'created\' renders a date-only bracket (#4439)', () => {
const line = renderPendingTodosMarkdown([makeTodo({ created: '2026-09-01T00:00:00.000Z', needs: undefined })]);
assert.match(line, /^- \[2026-09-01\] \[api\]/);
assert.doesNotMatch(line, /2026-09-01T/, 'full ISO timestamp must not leak into the rendered bullet');
});
test('renderPendingTodosMarkdown: \'created\' fallback (\'unknown\') is not sliced by the date-only formatter', () => {
const line = renderPendingTodosMarkdown([makeTodo({ created: undefined, needs: undefined })]);
assert.match(line, /^- \[unknown\] \[api\]/);
});
test('renderPendingTodosMarkdown: a malformed non-padded date is passed through unchanged, not mis-sliced', () => {
const line = renderPendingTodosMarkdown([makeTodo({ created: '2026-9-1', needs: undefined })]);
assert.match(line, /^- \[2026-9-1\] \[api\]/);
});
test('renderPendingTodosMarkdown: a non-4-digit year is not mis-sliced (near-miss YYYY-MM-DD shape)', () => {
const line = renderPendingTodosMarkdown([makeTodo({ created: '202-09-01T00:00:00.000Z', needs: undefined })]);
assert.match(line, /^- \[202-09-01T00:00:00\.000Z\] \[api\]/);
});
test('renderPendingTodosMarkdown: boundary — 241 chars (limit+1) truncates, needs dropped first', () => {
const base = makeTodo({ needs: 'x', title: 'X' });
const probe = renderPendingTodosMarkdown([base]);
@@ -142,7 +163,7 @@ test('property: rendered body always has one line per todo, each line <= 240 cha
// than a vacuous one; the pathological "cap not achievable" case is
// covered separately by the fixed "pathological" unit test above.
const todoArb = fc.record({
created: fc.constantFrom('2026-01-01', '2025-12-31', 'unknown'),
created: fc.constantFrom('2026-01-01', '2025-12-31', 'unknown', '2026-01-01T00:00:00.000Z', '2026-9-1'),
area: fc.string({ minLength: 0, maxLength: 40 }),
title: fc.string({ minLength: 0, maxLength: 500 }),
path: fc
@@ -320,6 +341,7 @@ test('cmdInitTodos: real todo file produces a rendered bullet via the CLI', (t)
const json = runQueryInitTodos(dir);
assert.equal(json.pending_read_ok, true);
assert.match(json.pending_todos_markdown, /Fix retry logic/);
assert.match(json.pending_todos_markdown, /^- \[2026-09-01\] \[api\]/m);
// The Needs clause is asserted on the STRUCTURED field, not the rendered
// bullet: the bullet embeds the todo file's ABSOLUTE path, so its total
// length varies by runner tmpdir (macOS CI's /private/var/folders/… plus