feat(work): add ISO timestamps + auto-bump on staged work-doc changes
- New scripts/work/bump-updated-timestamps.mjs stamps the `updated:`
frontmatter field to the current ISO 8601 UTC timestamp on every
staged docs/work/**/*.md file. Idempotent; adds the field after
`created:` if missing.
- .husky/pre-commit invokes the bump script as step 2 (before
rebuild-state) so _state.json sees the fresh timestamp.
- Backfill all existing work docs (4 PRDs + 3 epics + 21 stories):
* created: promoted from \`YYYY-MM-DD\` -> ISO timestamp using
git log --diff-filter=A on each file (first-commit date for
stories that had no \`created:\` line, midnight UTC for PRDs
and epics that had date-only created).
* updated: added from \`git log -1 --format=%aI\` on each file
(last-commit timestamp); will be re-stamped to "now" by the
pre-commit hook on this commit.
Stories that had no \`created:\` line now get one.
This commit is contained in:
78
scripts/work/bump-updated-timestamps.mjs
Normal file
78
scripts/work/bump-updated-timestamps.mjs
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Stamp every staged `docs/work/**\/*.md` file's frontmatter `updated:` field
|
||||
* to the current ISO 8601 timestamp. Runs from `.husky/pre-commit` before
|
||||
* `pnpm work rebuild-state`, so `_state.json` sees the fresh value.
|
||||
*
|
||||
* Idempotent: re-running produces the same result. The script silently no-ops
|
||||
* on files without frontmatter or without an `updated:` line to replace —
|
||||
* adds the field after `created:` if missing.
|
||||
*
|
||||
* Only stamps files explicitly listed in the staged diff; never walks the
|
||||
* tree. That way the timestamp tracks "the last commit that actually
|
||||
* modified the file," not "the last commit period."
|
||||
*/
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const REPO_ROOT = execSync("git rev-parse --show-toplevel", {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
|
||||
function stagedWorkDocs() {
|
||||
const out = execSync("git diff --cached --name-only --diff-filter=ACMR", {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
});
|
||||
return out
|
||||
.split("\n")
|
||||
.map((p) => p.trim())
|
||||
.filter(
|
||||
(p) =>
|
||||
p.startsWith("docs/work/") &&
|
||||
p.endsWith(".md") &&
|
||||
!p.endsWith("README.md"),
|
||||
);
|
||||
}
|
||||
|
||||
function stampUpdated(content, isoNow) {
|
||||
const fmMatch = content.match(/^(---\n)([\s\S]+?)(\n---)/);
|
||||
if (!fmMatch) return content;
|
||||
const [full, openDelim, body, closeDelim] = fmMatch;
|
||||
|
||||
let newBody;
|
||||
if (/^updated:\s*/m.test(body)) {
|
||||
newBody = body.replace(/^updated:\s*.*$/m, `updated: ${isoNow}`);
|
||||
} else if (/^created:\s*/m.test(body)) {
|
||||
// Insert `updated:` immediately after the `created:` line.
|
||||
newBody = body.replace(/^(created:\s*.*)$/m, `$1\nupdated: ${isoNow}`);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
return content.replace(full, `${openDelim}${newBody}${closeDelim}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const files = stagedWorkDocs();
|
||||
if (files.length === 0) return;
|
||||
const isoNow = new Date().toISOString();
|
||||
let touched = 0;
|
||||
for (const rel of files) {
|
||||
const abs = path.join(REPO_ROOT, rel);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const original = fs.readFileSync(abs, "utf8");
|
||||
const next = stampUpdated(original, isoNow);
|
||||
if (next === original) continue;
|
||||
fs.writeFileSync(abs, next);
|
||||
execSync(`git add ${JSON.stringify(rel)}`, { cwd: REPO_ROOT });
|
||||
touched++;
|
||||
}
|
||||
if (touched > 0) {
|
||||
console.log(
|
||||
`bump-updated-timestamps: stamped ${touched} file(s) at ${isoNow}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user