Migrate Posts to v2 Frontmatter
Concept of the skill
What it is: The rollout procedure for migrating all existing markdown posts to a new frontmatter contract.
Mental model: Treat the migration as a staged data change: add support, backfill content, validate everything, then enforce the new schema.
Why it exists: Required metadata changes can break every existing post unless compatibility and validation are sequenced deliberately.
What it is NOT: It is not a generic schema-design exercise or a debugging guide for a failed migration run.
Adjacent concepts: Content backfills, schema compatibility windows, tag normalization, date conversion.
One-line analogy: It is a bridge that lets old posts cross safely into the new metadata format.
Common misconception: Updating the validator first is harmless; populated content needs a compatibility window before enforcement.
Coverage
- The four-phase pattern for adding a required field to a populated content tree — add as nullable → backfill from existing data → verify → flip the validator to require it — and why collapsing any two phases into one is unsafe
- The backfill query — generating a
summary from each post's first paragraph, with a per-post manual-override fallback for cases where the auto-summary is wrong
- The verification gate between backfill and validator-flip — running
validate-posts.ts against the entire content/posts/**/*.md tree must return zero errors before the schema is updated
- The tag-normalization step — mapping every tag to its canonical form in
tag-vocabulary.ts, with a deny-list for tags that should be removed entirely (e.g., legacy synonyms now folded into a canonical tag)
- The dry-run gate — the migration script always runs in dry-run by default, printing the diff per post; the
--apply flag is opt-in and never the CI default
- The rollback path — what
ROLLBACK.md for this migration looks like and why "regenerate every summary" is wrong (overwrites authored summaries); the correct rollback restores the per-post .bak file the migration writes alongside each edit
Philosophy of the skill
A content-schema migration is a rare migration where being careful is cheaper than being clever. The temptation to combine the four phases into one "atomic" pass fails because the backfill produces some surprising auto-summaries, the human reviewer needs time to override them, and flipping the validator before the human pass is done means every build between then and the override fails. The four-phase pattern is verbose but unambiguous: each phase has a clear success criterion, each phase is re-runnable, and the rollback at any phase is well-defined. Pay the verbosity cost; the alternative is a build outage on a non-emergency migration.
Workflow
Each step has a clear precondition and a clear success criterion. Do not skip steps; the steps exist because skipping them is how content migrations corrupt authored data.
| Step |
Precondition |
Action |
Success criterion |
1. Add nullable summary |
The schema has no summary field |
lib/content/schema.ts: add summary: z.string().optional() (no required). Deploy. |
The schema accepts posts both with and without summary; the build does not fail on existing posts. |
| 2. Backfill |
Step 1 deployed |
Run scripts/migrate-frontmatter-v2.ts --apply --field summary which generates a draft summary per post from the first paragraph and writes a .bak for each modified file. |
Verification query reports 0 posts where summary is null or empty. |
| 3. Human review of auto-summaries |
Step 2 success |
Each post author reviews their auto-summary. Override by editing the post's frontmatter manually; the migration script will not re-run on a post whose summary was edited after step 2's .bak was written. |
Author sign-off recorded in audits/0007-frontmatter-v2/sign-off.md. |
| 4. Flip the validator |
Step 3 sign-off committed |
lib/content/schema.ts: change summary: z.string().optional() to summary: z.string().min(40) (required, with a minimum length). Deploy. |
Builds fail on any post that doesn't pass the v2 schema; the failure surface is the build log, not user-facing pages. |
When to back out
- Step 2 backfill produces too many surprising auto-summaries → reduce the auto-summary rule (e.g., first-sentence-only); the migration is still safe to resume from the same
.bak set.
- Step 3 reveals that some posts genuinely have no extractable summary → those posts need authored summaries before step 4; do NOT flip the validator with placeholder summaries in place.
- Step 4 is flipped and the build fails on a post whose summary was edited but didn't trip the
min(40) floor → revert step 4 (.optional() again), have the author rewrite the summary, re-flip.
Verification
Do NOT Use When
| Use instead |
When |
markdown-post-frontmatter-validation |
The task is reviewing or authoring a single post's frontmatter, not the migration that adds a new required field |
debugging |
A specific migration step is failing in CI and you need to reproduce |
documentation |
The task is writing a runbook or contributor doc about the migration |
| (a generic migration skill) |
The task is a different content migration with no relation to the v2 frontmatter rollout |
1---2# name: stable skill identifier. Match the skill directory name or the final namespace segment.3# Lowercase letters/numbers with hyphen, slash, or colon separators.4name: migrate-posts-to-v2-frontmatter5# description: routing-facing summary of what the skill covers and when it activates.6# Include concrete triggers and an explicit negative boundary; keep routing semantics out of prose-only ambiguity.7description: "Use when migrating every existing post in `content/posts/**/*.md` to the v2 frontmatter schema — adding the new required `summary` field, normalizing `tags` to the controlled vocabulary, converting bare-date `date` strings to ISO 8601 with timezone, and re-validating every post against the v2 schema before the next build runs. Activate this skill whenever the task references migration `0007-frontmatter-v2`, the v2 frontmatter rollout, or asks how to safely change a required-field set across a populated content tree without breaking the build. Do NOT use for unrelated migrations (use a generic content-migration skill or write a fresh one) or for general schema-design questions (use a schema-design skill)."89# === v8 Classification (subject + public; polyhierarchy via subjects[]) — see ADR-0017 ===10# subject: primary browse shelf — what the skill teaches. One of twelve closed values:11# backend-engineering / frontend-engineering / software-architecture / data-engineering / agent-ops / ai-engineering /12# quality-assurance / design / reasoning-strategy / software-engineering-method / knowledge-organization / product-domain.13subject: backend-engineering14# public: publishability/private-data gate. Boolean.15# true = publishable/shareable; false = private and excluded from public export.16# Project anchoring is carried separately by non-empty `project[]` plus `grounding`.17public: false18# taxonomy_domain: optional hierarchical sub-path within `subject`. Slash-delimited19# lowercase kebab-case segments. rename of the original v8 `domain`. Remove when the flat20# `subject` is sufficient.21taxonomy_domain: content/migrations22# scope: free-text PRD-style statement of what the skill teaches and what it excludes.23# (v8 required; not an enum). Mirrors Coverage + Do NOT Use When at frontmatter level.24scope: "Four-phase frontmatter migration workflow for the markdown-static-site example project — add nullable field, backfill, human review, flip validator."25# stability: lifecycle marker. One of:26# experimental (active development) / stable (production-ready) /27# frozen (no further changes expected) / deprecated.28# When `deprecated`, schema's allOf REQUIRES `superseded_by: <real-skill-name>`.29stability: experimental30# license: SPDX license identifier (e.g., MIT, Apache-2.0).31license: MIT32# compatibility: runtime compatibility object. Prefer structured fields33# (`agent_runtimes`, `node_version`) over free-text `notes`.34compatibility:35 agent_runtimes:36 - node37 node_version: ">=20"38 notes: "Markdown content tree under content/posts/; assumes Zod or similar runtime validator."39allowed-tools: Read Grep Bash40# keywords: semantic phrases for fuzzy router activation. v8 cap: max 10.41# Keep terms a user would actually type when starting a task in this skill's domain.42keywords:43 - migrate posts44 - frontmatter v2 migration45 - 0007 frontmatter v246 - content schema migration47 - tag vocabulary normalization48 - bare-date conversion49 - safe content migration50 - post backfill51 - frontmatter migration52# triggers: explicit-match activation phrases the router fires on literally.53# Use when label-based routing is intended; usually keywords + examples are enough.54triggers:55 - migrate-posts-to-v2-frontmatter56# paths: glob array of code surfaces this skill governs. Supports gitignore-style57# negation. Each glob should map to ONE canonical skill. Omit if purely conceptual.58paths:59 - "scripts/migrate-frontmatter-v2.ts"60 - "lib/content/schema.ts"61 - "lib/content/tag-vocabulary.ts"62# examples: 2-5 realistic user prompts the skill SHOULD activate for.63# Written in the user's voice. Improves retrieval recall beyond keywords alone.64examples:65 - "run the v2 frontmatter migration on every post in `content/posts/`"66 - "the v2 migration's tag-normalization step is rejecting some valid tags — what's safe to do?"67 - "verify that every post passes the v2 schema before flipping the validator"68 - "design migration 0008 to drop the legacy `excerpt` field now that `summary` is canonical"69# anti_examples: near-miss prompts that should route ELSEWHERE.70# Pair with relations.suppresses (or legacy boundary alias) to name the confusable territory's owner.71anti_examples:72 - "design a new frontmatter schema for a different domain"73 - "the migration is failing in CI — what's wrong?"74 - "write the v2 frontmatter doc for new contributors"75# relations: typed graph edges to sibling skills. Current fields:76# related (adjacency for browse / co-routing expansion) /77# suppresses (exclude listed skills from co-routing when THIS skill wins; write reason78# as "I own this exclusively over X", not "use X instead") /79# boundary (DEPRECATED alias of suppresses, retained for unmigrated skills) /80# verify_with (cross-check; co-loaded as one-hop expansion) /81# depends_on (composition; transitive — A→B→C loads all three) /82# broader / narrower (SKOS-style generalization) /83# disjoint_with (mutual exclusion for incompatible ownership).84relations:85 related:86 - documentation87 - debugging88 - refactor89 verify_with:90 - testing-strategy91 depends_on:92 - skill: testing-strategy93 min_version: "^1.0.0"94# grounding: required when `project[]` is non-empty. Declares the truth sources95# the skill anchors to and the failure modes those sources prevent. Omit when the96# skill is universal-knowledge. `subject_matter` replaces v8 `domain_object`.97grounding:98 subject_matter: "The 0007 frontmatter-v2 migration — a multi-step procedure that adds a required field, normalizes the tag vocabulary, converts bare-date strings to ISO 8601, and re-validates every post against the v2 schema before the validator is flipped"99 grounding_mode: repo_specific100 truth_sources:101 - scripts/migrate-frontmatter-v2.ts102 - lib/content/schema.ts103 - lib/content/tag-vocabulary.ts104 failure_modes:105 - validator_flipped_before_backfill_complete106 - tag_normalization_drops_a_valid_tag_silently107 - bare_date_conversion_picks_wrong_timezone108 - migration_runs_outside_dry_run_gate_first109 - rollback_step_overwrites_authored_summary_field110 evidence_priority: repo_code_first111# project: projects this skill is linked to. Array of {handle, role} objects.112# Non-empty project[] anchors the skill to a project and requires `grounding`.113# Suggested role values: source-of-truth, consumer, mirror. Replaces original v8 `workspace_tags`.114project:115 - handle: markdown-static-site116 role: primary117---118119# Migrate Posts to v2 Frontmatter120121## Concept of the skill122123**What it is:** The rollout procedure for migrating all existing markdown posts to a new frontmatter contract.124**Mental model:** Treat the migration as a staged data change: add support, backfill content, validate everything, then enforce the new schema.125**Why it exists:** Required metadata changes can break every existing post unless compatibility and validation are sequenced deliberately.126**What it is NOT:** It is not a generic schema-design exercise or a debugging guide for a failed migration run.127**Adjacent concepts:** Content backfills, schema compatibility windows, tag normalization, date conversion.128**One-line analogy:** It is a bridge that lets old posts cross safely into the new metadata format.129**Common misconception:** Updating the validator first is harmless; populated content needs a compatibility window before enforcement.130131## Coverage132133- The four-phase pattern for adding a required field to a populated content tree — *add as nullable → backfill from existing data → verify → flip the validator to require it* — and why collapsing any two phases into one is unsafe134- The backfill query — generating a `summary` from each post's first paragraph, with a per-post manual-override fallback for cases where the auto-summary is wrong135- The verification gate between backfill and validator-flip — running `validate-posts.ts` against the entire `content/posts/**/*.md` tree must return zero errors before the schema is updated136- The tag-normalization step — mapping every tag to its canonical form in `tag-vocabulary.ts`, with a deny-list for tags that should be removed entirely (e.g., legacy synonyms now folded into a canonical tag)137- The dry-run gate — the migration script always runs in dry-run by default, printing the diff per post; the `--apply` flag is opt-in and never the CI default138- The rollback path — what `ROLLBACK.md` for this migration looks like and why "regenerate every summary" is wrong (overwrites authored summaries); the correct rollback restores the per-post `.bak` file the migration writes alongside each edit139140## Philosophy of the skill141142A content-schema migration is a rare migration where being careful is cheaper than being clever. The temptation to combine the four phases into one "atomic" pass fails because the backfill produces some surprising auto-summaries, the human reviewer needs time to override them, and flipping the validator before the human pass is done means every build between then and the override fails. The four-phase pattern is verbose but unambiguous: each phase has a clear success criterion, each phase is re-runnable, and the rollback at any phase is well-defined. Pay the verbosity cost; the alternative is a build outage on a non-emergency migration.143144## Workflow145146Each step has a clear precondition and a clear success criterion. Do not skip steps; the steps exist because skipping them is how content migrations corrupt authored data.147148| Step | Precondition | Action | Success criterion |149|---|---|---|---|150| 1. Add nullable `summary` | The schema has no `summary` field | `lib/content/schema.ts`: add `summary: z.string().optional()` (no `required`). Deploy. | The schema accepts posts both with and without `summary`; the build does not fail on existing posts. |151| 2. Backfill | Step 1 deployed | Run `scripts/migrate-frontmatter-v2.ts --apply --field summary` which generates a draft summary per post from the first paragraph and writes a `.bak` for each modified file. | Verification query reports 0 posts where `summary` is null or empty. |152| 3. Human review of auto-summaries | Step 2 success | Each post author reviews their auto-summary. Override by editing the post's frontmatter manually; the migration script will not re-run on a post whose `summary` was edited after step 2's `.bak` was written. | Author sign-off recorded in `audits/0007-frontmatter-v2/sign-off.md`. |153| 4. Flip the validator | Step 3 sign-off committed | `lib/content/schema.ts`: change `summary: z.string().optional()` to `summary: z.string().min(40)` (required, with a minimum length). Deploy. | Builds fail on any post that doesn't pass the v2 schema; the failure surface is the build log, not user-facing pages. |154155### When to back out156157- Step 2 backfill produces too many surprising auto-summaries → reduce the auto-summary rule (e.g., first-sentence-only); the migration is still safe to resume from the same `.bak` set.158- Step 3 reveals that some posts genuinely have no extractable summary → those posts need authored summaries before step 4; do NOT flip the validator with placeholder summaries in place.159- Step 4 is flipped and the build fails on a post whose summary was edited but didn't trip the `min(40)` floor → revert step 4 (`.optional()` again), have the author rewrite the summary, re-flip.160161## Verification162163- [ ] Step 1 added the field as `.optional()`, not as required164- [ ] Step 2 was run with `--apply` only after a `--dry-run` was reviewed (the dry-run output is committed under `audits/0007-frontmatter-v2/dry-run.md`)165- [ ] Step 2's `.bak` files exist for every modified post and are committed (so rollback is a one-command restore)166- [ ] Step 3 sign-off is recorded for every post in `content/posts/**/*.md` — no post moved past step 3 without explicit author confirmation167- [ ] Step 4 was applied AFTER step 3 sign-off — never before, even if the author backlog is taking longer than expected168- [ ] The rollback path in `ROLLBACK.md` does NOT include "regenerate every summary" — that overwrites authored content. Rollback is `mv <post>.md.bak <post>.md` per file, with the `.bak`s already committed.169- [ ] An end-to-end CI test runs the migration in dry-run mode against the live `content/posts/` set and reports the diff in the PR description before any human merges step 4170171## Do NOT Use When172173| Use instead | When |174|---|---|175| `markdown-post-frontmatter-validation` | The task is reviewing or authoring a single post's frontmatter, not the migration that adds a new required field |176| `debugging` | A specific migration step is failing in CI and you need to reproduce |177| `documentation` | The task is writing a runbook or contributor doc about the migration |178| (a generic migration skill) | The task is a different content migration with no relation to the v2 frontmatter rollout |