fix(3805): schema-aware log_to_state row appending in fast.md (#85)
* fix(3805): schema-aware log_to_state row appending in fast.md REPRO: fast.md log_to_state unconditionally echoed a hardcoded 4-cell row (| date | fast | task | ✅ |) into STATE.md. When quick.md Step 7 had already created the "Quick Tasks Completed" table with 5 columns (| # | Description | Date | Commit | Directory |), fast.md appended a malformed 4-cell row → broken Markdown table. FIX: fast.md log_to_state now reads the existing table header, counts columns, and checks for the expected column names from quick.md Step 7. If the 5-column schema is confirmed, it appends a properly-formed 5-cell row. If the schema is unrecognized, it skips the write with a warning rather than corrupt the table. Pattern source: quick.md Step 7 (schema-aware matching). ANTI-PATTERN SWEEP: Only fast.md and quick.md contain direct STATE.md table writes in workflows/. quick.md Step 7 is already schema-aware. No other workflow candidates found. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add changeset for #3805 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/3805-fast-md-log-to-state-schema-aware.md
Normal file
5
.changeset/3805-fast-md-log-to-state-schema-aware.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3805
|
||||
---
|
||||
**`fast.md` `log_to_state` no longer appends a malformed row when the STATE.md table uses the 5-column quick.md schema (#3805)** — the workflow unconditionally wrote a hardcoded 4-cell row (`| date | fast | task | ✅ |`) into STATE.md; when `quick.md` Step 7 had already created a 5-column "Quick Tasks Completed" table the mismatched row broke the Markdown table. `fast.md` now reads the existing header, counts columns, and verifies the quick.md column names before appending a properly-formed 5-cell row. If the schema is unrecognized the write is skipped with a warning rather than corrupting the table.
|
||||
@@ -63,14 +63,33 @@ Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:`
|
||||
</step>
|
||||
|
||||
<step name="log_to_state">
|
||||
If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table.
|
||||
If the table doesn't exist, skip this step silently.
|
||||
If `.planning/STATE.md` exists and has a "Quick Tasks Completed" table, append a row
|
||||
that matches the existing table's schema. If no table exists, skip silently.
|
||||
If the table's schema is unrecognized, skip with a brief log rather than append a
|
||||
malformed row.
|
||||
|
||||
```bash
|
||||
# Check if STATE.md has quick tasks table
|
||||
# Detect whether STATE.md has a Quick Tasks Completed table
|
||||
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
|
||||
# Append entry — workflow handles the format
|
||||
echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md
|
||||
# Read the table header line to determine the column schema.
|
||||
# quick.md Step 7 creates a 5-column table:
|
||||
# | # | Description | Date | Commit | Directory |
|
||||
# Count pipe characters in the header to determine column count.
|
||||
HEADER_LINE=$(grep -A2 "Quick Tasks Completed" .planning/STATE.md 2>/dev/null | grep "^|" | head -1)
|
||||
# Count columns: number of | separators minus 1 gives column count
|
||||
COL_COUNT=$(echo "$HEADER_LINE" | awk -F'|' '{print NF-1}')
|
||||
|
||||
if [ "$COL_COUNT" -eq 5 ] && echo "$HEADER_LINE" | grep -qi "Description" && echo "$HEADER_LINE" | grep -qi "Commit" && echo "$HEADER_LINE" | grep -qi "Directory"; then
|
||||
# 5-column schema from quick.md Step 7: | # | Description | Date | Commit | Directory |
|
||||
# Determine the next row number by counting existing data rows (non-separator, non-header).
|
||||
NEXT_NUM=$(awk '/Quick Tasks Completed/{found=1} found && /^\|/ && !/^[|][-: |]*[|]$/ && !/Description/{count++} END{print count+1}' .planning/STATE.md 2>/dev/null || echo "1")
|
||||
# Get the latest commit hash (short)
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "—")
|
||||
echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — |" >> .planning/STATE.md
|
||||
else
|
||||
# Unrecognized table schema — skip to avoid appending a malformed row.
|
||||
echo "⚠ fast.md log_to_state: Quick Tasks Completed table has unrecognized schema (${COL_COUNT} columns); skipping STATE.md update."
|
||||
fi
|
||||
fi
|
||||
```
|
||||
</step>
|
||||
|
||||
143
tests/bug-3805-fast-md-log-to-state-schema.test.cjs
Normal file
143
tests/bug-3805-fast-md-log-to-state-schema.test.cjs
Normal file
@@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// Reads get-shit-done/workflows/fast.md whose deployed text IS the product —
|
||||
// the workflow markdown is executed verbatim by LLM runtimes.
|
||||
|
||||
/**
|
||||
* #3805 — fast.md log_to_state appends a schema-blind 4-column row to the
|
||||
* 5-column "Quick Tasks Completed" table created by quick.md Step 7.
|
||||
*
|
||||
* quick.md Step 7 creates the table with 5 columns:
|
||||
* | # | Description | Date | Commit | Directory |
|
||||
*
|
||||
* Before this fix, fast.md's log_to_state step appended a hardcoded 4-cell
|
||||
* row unconditionally:
|
||||
* echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md
|
||||
*
|
||||
* This produces malformed Markdown when the existing table has a different
|
||||
* column count.
|
||||
*
|
||||
* Covers:
|
||||
* - fast.md does NOT contain the hardcoded 4-cell echo template
|
||||
* - fast.md log_to_state step reads/introspects the existing table header
|
||||
* before appending (schema-aware insertion)
|
||||
* - fast.md log_to_state step matches the 5-column schema from quick.md
|
||||
* Step 7 when that table is present
|
||||
* - fast.md log_to_state step skips (does not corrupt) the STATE.md write
|
||||
* when the table schema is unrecognized
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const FAST_MD_PATH = path.join(REPO_ROOT, 'get-shit-done', 'workflows', 'fast.md');
|
||||
|
||||
// The 5-column schema defined in quick.md Step 7 (non-validate mode).
|
||||
// Column count is 5: # | Description | Date | Commit | Directory
|
||||
// Named constant for traceability — mirrors quick.md Step 7's table header.
|
||||
const QUICK_MD_STEP7_COL_COUNT = 5;
|
||||
const QUICK_MD_STEP7_COLUMNS = ['#', 'Description', 'Date', 'Commit', 'Directory'];
|
||||
|
||||
describe('bug #3805: fast.md log_to_state must be schema-aware', () => {
|
||||
let fastMdContent;
|
||||
|
||||
test('fast.md workflow file exists and is readable', () => {
|
||||
assert.ok(fs.existsSync(FAST_MD_PATH), `fast.md not found at ${FAST_MD_PATH}`);
|
||||
fastMdContent = fs.readFileSync(FAST_MD_PATH, 'utf-8');
|
||||
});
|
||||
|
||||
test('fast.md log_to_state step does NOT hardcode a 4-cell row template', () => {
|
||||
// The old broken template: | date | fast | task | ✅ |
|
||||
// This regex matches the exact hardcoded pattern that ignores table schema.
|
||||
// A 4-cell row has exactly 4 pipe-delimited fields plus the surrounding pipes.
|
||||
const hardcoded4CellPattern = /echo\s+["'][|][^|]*[|][^|]*[|][^|]*[|][^|]*[|]\s*["']/;
|
||||
const match = fastMdContent.match(hardcoded4CellPattern);
|
||||
assert.ok(
|
||||
!match,
|
||||
[
|
||||
'fast.md log_to_state still contains the hardcoded 4-cell row template:',
|
||||
` "${match?.[0]}"`,
|
||||
'This appends a malformed row to the 5-column Quick Tasks Completed table',
|
||||
'created by quick.md Step 7.',
|
||||
`Expected table schema (${QUICK_MD_STEP7_COL_COUNT} cols): | ${QUICK_MD_STEP7_COLUMNS.join(' | ')} |`,
|
||||
].join('\n')
|
||||
);
|
||||
});
|
||||
|
||||
test('fast.md log_to_state step reads the existing table header (schema introspection)', () => {
|
||||
// The fix must inspect the existing STATE.md table header before appending.
|
||||
// Acceptable signals: reading STATE.md content, grepping for the header line,
|
||||
// or parsing columns from the header row.
|
||||
const hasHeaderRead =
|
||||
// Reads STATE.md to inspect it (awk/sed/grep on the file for header detection)
|
||||
/grep.*Quick Tasks Completed.*STATE\.md/.test(fastMdContent) ||
|
||||
/awk.*Quick Tasks Completed/.test(fastMdContent) ||
|
||||
/sed.*Quick Tasks Completed/.test(fastMdContent) ||
|
||||
// Reads the header line explicitly (head -n, sed -n, awk NR==)
|
||||
/head\s+-n/.test(fastMdContent) && /STATE\.md/.test(fastMdContent) ||
|
||||
// Counts pipe separators / columns from existing header
|
||||
/col.*count|column.*count|count.*col|NF|awk.*\|/.test(fastMdContent) ||
|
||||
// References schema detection in prose
|
||||
/schema|header|column\s+count|existing.*table/.test(fastMdContent);
|
||||
|
||||
assert.ok(
|
||||
hasHeaderRead,
|
||||
[
|
||||
'fast.md log_to_state step does not appear to introspect the existing table schema.',
|
||||
'The step must read the STATE.md table header to detect column count before appending.',
|
||||
'quick.md Step 7 uses schema-aware matching — fast.md must follow the same discipline.',
|
||||
].join('\n')
|
||||
);
|
||||
});
|
||||
|
||||
test('fast.md log_to_state step references the 5-column quick.md schema', () => {
|
||||
// The fix must handle the 5-col schema: | # | Description | Date | Commit | Directory |
|
||||
// Test that all 5 column names appear in the log_to_state step's context.
|
||||
// Extract the log_to_state step content to scope the check.
|
||||
const logToStateMatch = fastMdContent.match(/<step name="log_to_state">([\s\S]*?)<\/step>/);
|
||||
assert.ok(logToStateMatch, 'fast.md must contain a <step name="log_to_state"> element');
|
||||
|
||||
const stepContent = logToStateMatch[1];
|
||||
|
||||
// All 5 column names from quick.md Step 7 must be referenced in the step.
|
||||
for (const col of QUICK_MD_STEP7_COLUMNS) {
|
||||
assert.ok(
|
||||
stepContent.toLowerCase().includes(col.toLowerCase()),
|
||||
[
|
||||
`fast.md log_to_state step does not reference column "${col}"`,
|
||||
`Expected all 5 columns from quick.md Step 7: ${QUICK_MD_STEP7_COLUMNS.join(', ')}`,
|
||||
`(${QUICK_MD_STEP7_COL_COUNT}-column schema)`,
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('fast.md log_to_state step skips STATE.md write on unrecognized schema', () => {
|
||||
// The fix must not blindly append when the table schema is unknown.
|
||||
// Check for a guard that skips or logs rather than corrupting the file.
|
||||
const logToStateMatch = fastMdContent.match(/<step name="log_to_state">([\s\S]*?)<\/step>/);
|
||||
assert.ok(logToStateMatch, 'fast.md must contain a <step name="log_to_state"> element');
|
||||
|
||||
const stepContent = logToStateMatch[1];
|
||||
|
||||
// Must have a skip/guard path for unrecognized schemas
|
||||
const hasSkipGuard =
|
||||
/skip/i.test(stepContent) ||
|
||||
/unrecognized|unknown|mismatch/i.test(stepContent) ||
|
||||
/else\b/.test(stepContent) ||
|
||||
/warn|log/i.test(stepContent);
|
||||
|
||||
assert.ok(
|
||||
hasSkipGuard,
|
||||
[
|
||||
'fast.md log_to_state step does not appear to guard against unrecognized table schemas.',
|
||||
'When the existing STATE.md table does not match an expected schema,',
|
||||
'the step must skip the write (with a brief log) rather than append a malformed row.',
|
||||
].join('\n')
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user