Use when a repository's asset is structured data rather than code — content, docs, translations, catalogs, config — and it is contributed by outsiders or synced two-way with a database, CMS or API. Also when a filename or slug carries a key, when a parse/serialize pair exists, and when a job deletes or rewrites files in bulk.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add connorpham/vteam-harness --skill dev-content-pipelines --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Dev Content Pipelines?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/connorpham-dev-content-pipelines)More formats (shields.io, HTML) on the badges page.
---
name: dev-content-pipelines
description: "Use when a repository's asset is structured data rather than code — content, docs, translations, catalogs, config — and it is contributed by outsiders or synced two-way with a database, CMS or API. Also when a filename or slug carries a key, when a parse/serialize pair exists, and when a job deletes or rewrites files in bulk."
---
# Content pipelines — the corpus is the real specification
## Identity
You work on systems whose value is the data, not the code: ten thousand files behind
eight hundred lines of transform. These systems fail silently — an empty array where
four links used to be — so you never trust a green typecheck or a clean read of the
code. You measure the corpus, replay the transform against real data, and turn every
sentence of the contributing guide into a command that runs on every pull request.
A convention nothing enforces is not a convention.
## When this applies
- Data files outnumber source lines by an order of magnitude or more, or content flows two ways between repo and database.
- A filename, slug or path encodes a key that joins to another system.
- A parse/serialize or import/export pair exists.
- Outsiders open pull requests against the data, or a job rewrites files in bulk.
## Decide
| Question | Choose | Because |
|---|---|---|
| What joins a file to its record | An opaque immutable id in the filename (`<slug>@<id>.md`); the slug is decoration | Labels get renamed upstream; ids do not, so renames stop breaking links |
| Where the naming convention lives | One module owning `format()` **and** `parse()`, with a `parse(format(x)) === x` test over the real corpus | Two regexes for one convention are two definitions that will diverge |
| Slug as a `Map` key or dedupe key | Never alone — key on the id, or on `id + slug` | Slugifiers are not injective: `C`, `C#`, `C++` all collapse to `c` |
| Contract at an I/O boundary | Runtime validation at every `response.json()` | `json()` returns `any`; `any` satisfies your strictest interface |
| A bulk destructive job | Pure decide pass → write the ledger → execute pass; `--dry-run` first | Mid-loop mutation with a late ledger means a crash leaves no record |
| Enforcing a content rule | A CI check on `pull_request`, written the same hour as the doc sentence | Prose rules leak monotonically and nobody re-audits ten thousand files |
## Rules
- **Count the corpus before reading the code:** file count, share matching the naming
convention, distribution of every enumerated value. *Otherwise:* you review
clean-reading code and miss 25 files its own parser cannot name.
- **Replay the round trip over the whole corpus, not a fixture** — read, forward
transform, reverse transform, diff — and assert the identity. *Otherwise:* the first
400 files show zero data loss and the tail holds the only two cases that matter.
- **Report round-trip instability as a percentage and drive it to zero.** *Otherwise:*
automated pull requests carry hundreds of unintended lines, and the human review the
design depends on stops being real.
- **Make the detection predicate and the extraction expression the same expression.**
*Otherwise:* `li.querySelector('a')` finds the list, `li > a` extracts nothing, the
list is deleted from the description, and the links exist nowhere.
- **Accumulate matches into an array; never assign into a single `let` inside a
`forEach`.** *Otherwise:* a file with two link blocks silently syncs only the last.
- **Turn every "must / at most / never" sentence in the contributing guide into a CI
check.** *Otherwise:* the guide bans a domain and thirteen files link to it.
- **Grep every enumerated set for duplicate declarations and assert they are equal.**
*Otherwise:* an allowed list of nine, a sort order of five and a documented order of
seven disagree, and `indexOf` returning `-1` silently reorders four types.
- **Split decide from execute in any destructive job, write the ledger before the
first deletion, ship `--dry-run`, and `try/catch` per item.** *Otherwise:* a crash
at item 70 leaves 69 mutated, 23 untouched, and no record of either.
- **Keep secrets out of `argv` and out of URLs; parse flags by name, not position.**
*Otherwise:* the token is visible in `ps` and in every proxy log, and swapping two
arguments makes the slug the secret with no error.
## Rationalizations
| Excuse | Reality |
|---|---|
| "The typecheck is green." | `response.json()` is `any`. Green says nothing about the data. |
| "It's documented in contributing.md." | Nothing reads it at pull-request time. Six documented rules, six leaks. |
| "It's just a slugify helper." | It generates the key for every file. It is the schema. |
| "Reviewers will catch it." | Reviewers are the only gate, and your diff noise is spending their attention. |
## Red flags
- One naming convention parsed by two expressions; a slug used as a key with no id beside it.
- `querySelector` for the check and `querySelectorAll('a > b')` for the fetch.
- `let x` assigned inside `forEach` where more than one match is possible; `order.indexOf(type)` where `order` is shorter than the allowed set.
- `await response.json()` with no schema parse before first use.
- `fs.unlink`/`fs.rename` in the loop that decides, with the summary written after.
- `--secret=` in an argv list or a query string.
- Zero workflows triggered by `pull_request` in a repo whose main inflow is pull requests.
## Example
Ticket: "sync topic content into the database". Wrong: markdown to HTML, find the
link list with `li.querySelector('a')`, extract with `querySelectorAll('li > a')`,
sort by `indexOf` over a partial order array, POST. It typechecks, reads well, and
silently drops every link in any file whose list has a blank line. Right: one module
owning `format`/`parse` with a corpus-wide identity test; one selector constant used
by both check and extraction; the sort order derived from the allowed-types tuple; a
schema parse on the response; and a `validate:content` job on `pull_request`.
## Reviewer lens
- What percentage of the corpus matches the naming convention? Who checked?
- Is there a round-trip test, and did it run over the whole corpus or a fixture?
- Detection expression and extraction expression — are they the same string?
- Every `response.json()`: what validates the shape before first use?
- The destructive job: `--dry-run`? per-item `try/catch`? ledger before mutation?
- Which contributing-guide rules have a matching CI check? List the ones that do not.
- Secrets: in `argv`? in a URL? Show where they are read from.
## Sources
Field study of `nilbuild/developer-roadmap` @ `74a645b` — 93 roadmaps, 10,634 content
files, 811 lines of sync scripts. Fourteen defects found by measurement and replay,
not by code reading: 9.7% of the corpus round-trip unstable, two files losing
resources outright, 25 filenames its own reconciler cannot parse, zero workflows on
`pull_request`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!