← Corpus / context-vigilance-kit / handoff
Frontmatter Normalization — Remaining Repos
Done. All 436 changelog entries in the tree carry the editorial date pair, the legacy `date:` key is gone, and every build-breaking trap is closed — including one nobody had a name for until a lenient schema let a live ordering bug through.
- Path
- handoffs/Frontmatter-Normalization-Remaining-Repos.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Opus 5 (1M context)
- Tags
- Frontmatter · Normalization · Handoff · Context-Vigilance · Agent-Sweeps · Publish-Gate
Frontmatter Normalization — Remaining Repos
Why care?
A tree-wide audit found markdown files missing date_created or lacking
frontmatter entirely. Six repos have since been swept. This document hands off
the rest.
The important part is not the file list — it’s the three repo-specific traps below. Each was discovered by nearly breaking something, and none is inferable from the standard. Trap 1 now has a proven remedy, applied three times.
Current state
✅ COMPLETE — 2026-08-17
All 436 changelog entries in the tree conform. Final audit, originals only:
Legacy date: remaining | 0 |
| Missing the editorial pair | 0 |
| No frontmatter at all | 0 |
Every entry now carries date_authored_initial_draft and
date_authored_current_draft — the keys Graphiti anchors on. Nothing falls
through to a filesystem date or to mtime, which a reformat pass silently
rewrites.
~230 entries swept across 30+ repos in the closing session, on top of the four repos completed earlier. Every repo was screened for credentials, client names, and financial detail before any publish value was set.
Two residuals, deliberately out of scope: 177 entries lack site_uuid and
90 lack publish. These sit in repos that were already date-conformant when
the audit began, so they never appeared on the work list — memopop-orchestrator
(81) and dididecks-ai (85) are the bulk. Identity fields were never in the
original scope; they were added opportunistically to everything touched. Neither
gap blocks Graphiti.
What “no legacy key” did not mean
Worth preserving, because it caused a false finish. An earlier version of this
document declared the tier complete on the strength of the rename being done.
It wasn’t. The sweep had targeted repos by rename count, so repos that skipped
straight from no-convention to date_created — never using date: at all —
registered zero renames and were never visited. That was 143 entries across 25
repos, found only by re-auditing against the standard rather than against the
task list.
Audit against the standard, not against the work you planned.
The earlier counts were inflated — read this before quoting a number
This document previously reported 652 files across 47 repos. That number
double-counted third-party pinned repos. ai-labs/studies/ contains 67
foreign upstream repos (vectorize-io/hindsight, getzep/graphiti,
apache/arrow, jgm/pandoc, …). They are not ours to edit, and one of them
alone (hindsight) contributed 48 changelog files to the old total.
Any audit of this tree must exclude, at minimum:
node_modules/ .git/ dist/ .astro/ .vercel/
ai-labs/studies/** # any repo whose origin is NOT lossless-group
context-vigilance-kit/corpus/ # roll-up
lossless-changelog/src/stream/ # roll-up
*/splash/src/rollup/ # roll-up
site/src/generated-content/ # generated
mpstaton-site/src/content/context-v/ # roll-up
*/context-v/agent-skills/ # vendored copies of canonical skills
*/context-v/extra/ # gitignored scratch
ai-labs/augment-it/ content/ # operator-excluded
Enumerate foreign repos with:
find . -name .git -maxdepth 6 -not -path "*/node_modules/*" | while read g; do
d=$(dirname "$g"); r=$(git -C "$d" remote get-url origin 2>/dev/null)
case "$r" in *lossless-group*|"") ;; *) echo "${d#./}";; esac
done
Roll-up hygiene is already correct and needs no fix: sources.md only walks
for context-v/ directories, which foreign repos don’t have, and
ingest-changelogs-to-chroma.py skips /studies/ outright. Hindsight’s 48
changelog entries enter neither corpus. Our own studies’ context-v/ is
captured, which is right.
The standard
Do not restate it from this document — read the source, which is now accurate:
context-v/skills/context-vigilance/references/frontmatter-spec.mdcontext-v/skills/changelog-conventions/references/frontmatter-spec.md
Both skills auto-load in a fresh session. Point agents at those file paths as the authority rather than pasting rules into prompts — inconsistent restatement between batches is what produced the only real errors in this sweep.
Required keys, in brief:
| Scope | Keys |
|---|---|
context-v/**.md | date_created, date_modified, publish |
changelog/**.md | date_authored_initial_draft, date_authored_current_draft, publish |
Both specs now also define summary (agent-facing counterpart to lede) and the
write-once identity pair site_uuid / hex_code. Those are optional but should
be written on new files going forward; retrofitting them is a separate
directed pass with its own hazard (see Identity fields below).
The three traps
1. A key rename can break a build — and there is now a fix for it
date: → date_authored_initial_draft: is sanctioned for changelog entries
only, and in an Astro site it is not safe by default.
The failure has two shapes, and the second is worse:
- Hard failure. A collection declaring
date: z.coerce.date()— required — fails validation on every entry the moment the key is renamed. The build exits non-zero. Loud, but at least obvious. - Silent failure. Index pages commonly filter on
entry.data.datebeing truthy (.filter(e => e.data.title && e.data.date)). An entry that survives validation without the key silently vanishes from the changelog index. Nothing announces it.banner-siteanddark-matterboth had this.
Status: RESOLVED tree-wide. No content collection anywhere still declares a
required date. Verified 2026-08-17.
| Repo | State |
|---|---|
fullstack-vc, dark-matter, banner-site, mpstaton-site | Fixed — schema tolerant, renderers resolve through a fallback chain |
twf_site, reach-edu-hub, memopop-site (lenientDate) | Were already lenient |
| Everything else | No schema reads date |
Re-run the check before trusting this, since a new collection can reintroduce it:
grep -rn --include="*.ts" -E "^\s*date:\s*z\.(coerce\.)?date\(\)\s*,?\s*$" . | grep -v node_modules
But see 1b below — this check alone is not sufficient.
The remedy — applied three times, now routine
- Make every date spelling optional in the collection schema —
date,date_authored_initial_draft,date_authored_current_draft,date_created,date_modified, each.nullable().optional(). Nothing date-shaped stays required. - Add a resolver returning the first key that parses, falling back to the
YYYY-MM-DDin the entry id. Reference implementations:astro-knots/sites/fullstack-vc/src/lib/changelog-date.tsastro-knots/sites/banner-site/src/utils/changelog-date.tsastro-knots/sites/dark-matter/src/lib/dates/resolveEntryDate.ts
- Route every renderer through it — including sort comparators
(
entryDateMs) and truthiness filters (hasEntryDate). Miss a filter and you get the silent failure above. - Then rename the keys.
Check the legacy key FIRST in the precedence chain. Not deference to the old
standard — it makes the change provably zero-diff (entries carry both keys with
identical values during transition, so no rendered date can move), and a
hand-authored date is more trustworthy than a stat-derived editorial key
(trap 2). Once date is dropped from a file the editorial keys take over with no
code change. That is what makes the rename file-at-a-time rather than a flag day.
Verification that actually proves something — build at three points and diff the rendered output:
pnpm build # baseline, before any change
# ...apply schema + resolver...
pnpm build && diff <before> <after> # plumbing must be ZERO-diff
# ...apply renames...
pnpm build && diff <before> <after> # must STILL be zero-diff
For an SSR site a passing build only proves schema validation — it does not prove the page renders. Run the server and drive the routes:
pnpm dev --port 4399
curl -s localhost:4399/changelog | grep -c "Invalid Date\|NaN" # must be 0
dark-matter renders changelog SSR and needed exactly this; its three views
(/changelog, /changelog/variant-1, /changelog/variant-3) plus detail pages
were driven live before the work was called done.
1b. A LENIENT schema is not proof the rename is safe — the harder variant
Trap 1 as scanned above finds strict schemas. lfm proved that is only half
the check, and the half that fails loudly.
lfm’s splash schema accepted every date spelling and even falls back to
storing raw frontmatter when validation fails. The scan said “clean.” It was
the most dangerous repo in the sweep.
The exposure was in the consumer, not the schema. Both changelog pages resolved a date through a list of field names, taking the first that resolves:
date_modified ?? date_first_published ?? date_created ?? date
date_authored_initial_draft is not on that list — the list was written when
date was the newest spelling that existed. And
changelog/2026-04-22_01.md carried only date:.
Renaming it away would have left the chain nothing to resolve: build passes,
page renders, entry appears — with a blank where the date was, and a sort
key of 0 sinking it to the bottom of the index. No error, no warning, no log
line. Detectable only by opening that one page.
The strict schema is the safer case. It fails loudly and you fix it. The lenient one lets the problem through silently.
The check is therefore two questions, not one:
- Does any schema require the key being removed? (build failure)
- Does every chain that reads a date know the editorial keys — and does any file depend solely on the key being removed? (silent blank)
The remedy: append the editorial keys to the end of each chain, preserving
existing precedence, and verify that change is zero-diff on its own before
renaming any content. Worked example in lfm/splash/src/pages/changelog/.
Any consumer written before the editorial convention landed has this blind spot. Grep for chains, not just schemas:
grep -rn "date_modified ??\|date_created ??\|\.data\.date\b" src --include="*.astro" --include="*.ts"
2. Filesystem dates lie — stat is the last resort
Whole directories in this tree carry a birthtime from a bulk copy or machine
recovery rather than from authorship. Observed: ~100 changelog entries spanning
five months all reporting created=2026-05-06, and release-notes files reporting
the birthtime of the day the sweep ran while git dated them nine months earlier.
Source precedence, per the spec:
- existing frontmatter on the file
- a date in the filename or parent directory name
- a date stated in the document body
git log --diff-filter=A --follow --format=%ad --date=short -- <file> | tail -1stat— and treat the result as suspect
The tell is a uniform birthtime across files of obviously different ages.
3. publish is a judgment, and its default is repo-specific
There is no safe tree-wide default. Tree-wide the split runs roughly 2:1
toward true, but individual repos run the other way, deliberately. Count before
deciding:
grep -rh '^publish:' --include='*.md' context-v/ | sort | uniq -c
Two calibration points from this sweep:
memopop-orchestratorran 47falseto 5true. A content-only rule (“real content → true”) marked 26 documents publishable; a screened re-read kept 3. Substance and sensitivity run in the same direction — the meatier acontext-v/doc is, the more client detail it tends to carry.fullstack-vc’s members-only session narratives are deliberately public. Participants consent; their headshots are already served by the site. Marking thosefalsewas wrong.
The rule is genericize rather than hide, and the document’s job comes first:
if it is materially better with the specific names in it, keep them and set
publish: false. It stays in the repo for us. Variable and env-var names,
architecture, schemas, and candid post-mortems are all fine to publish.
What the sweep actually taught — the durable part
The file lists are spent; these are not.
Two traps had to be found by breaking something
Trap 1 (strict schema) is loud: a required date field fails the build the
moment the key is renamed. Easy to scan for, and now resolved tree-wide.
Trap 1b (lenient schema) is the dangerous one, and the scan for trap 1 walks straight past it. A schema that requires nothing still lets a consumer break: the code reading the date walks a list of field names, and if that list predates the editorial convention while some file depends solely on the new keys, the date resolves to nothing. Build passes, page renders, date is blank, sort key is zero.
Found live on the context-v/skills splash, where the newest changelog entry was
rendering at the bottom of the list. Nobody had noticed.
The check is two questions, not one: does any schema require the key being removed, and does every chain that reads a date know the editorial keys?
The publish flag is the wrong control at an aggregation boundary
The costliest lesson. publish: true is set by whoever authored an entry, for
their repo’s surface. A private client repo can legitimately mark everything
publish: true meaning “publish on the client’s gated site” — and a public
aggregator that reads it as consent will republish it to the world. That is
exactly what happened, and it put a client’s confidential fundraise position on
a live public URL.
Both roll-up scripts now gate on repository visibility and on whether the
source is client work, default-deny, failing closed. See
ai-labs/splash/scripts/rollup-sync.ts and
astro-knots/splash/src/loaders/rollupFetch.ts.
And visibility alone is not sufficient. reach-edu-hub is a public repo
and a named client engagement. “Private” and “confidential” are different
properties; a gate that checks only the first will leak the second.
Screen before you set publish, not after
Setting publish: true on a client repo and screening afterwards is how the
above happened. On the repos swept after that lesson the screen ran first, and it
caught a memo pipeline enumerated by company name, an LP share-label naming a
real firm, and a lead investor named in four incidental asides.
Other things that cost time
- Enumerate content directories from the filesystem, not from a collection config. Reading the config finds only what the site already points at. One repo had split its changelog across two directories, each half looking complete from where it stood — and the two were visible to different audiences.
- A fix is not live until it reaches the branch
.gitmodulesnames, which is not necessarily the branch the repo is checked out on. - The parent’s changelog holds copies of submodule entries. A confidentiality fix in a submodule is not finished until the parent copy is checked.
- A tree-wide
grepfor existing identifiers times out and returns empty, which reads as “no collisions” when it means “no data.” Scope the grep and assert the result is non-empty before trusting it. - Derive editorial dates from
date_created, neverdate_modified— a mere file open bumps mtime, so it is not evidence of a substantive revision.
Open scoping question — context-v/skills/
The canonical skills tree is the single largest block: 102 mechanical, 61 with no frontmatter. It is deliberately excluded from the 331 total above, pending an operator decision.
The case for excluding it: those files are references/*.md, README.md,
CLAUDE.md — skill internals that happen to live under a context-v/ path,
not context-v/ documents. This handoff already excludes context-v/agent-skills/
on exactly that logic (SKILL.md frontmatter is a machine contract Claude Code
parses). The same reasoning appears to apply one directory up.
If it should be swept, the tree total becomes 433. If it is out of scope, 331 stands and the exclusion belongs in the spec so no future audit re-surfaces it.
Operational notes
- Edit originals, never rollups.
context-vigilance-kit/corpus/,astro-knots/sites/lossless-changelog/src/stream/andsplash/src/rollup/are derived. A naivefindfor a repo name will surface the corpus copy first — resolve paths throughsources.mdor the changelog walker instead. - Stage path-scoped. Every repo swept so far had unrelated dirt — submodule pointers, lockfiles, untracked scripts, concurrent sessions.
- Watch for nested repos.
dark-matter/changelogis its own git repo insidedark-matter. Two commits, ordered: changelog first, then the parent (which will show a moved submodule pointer). - Frontmatter-only edits. A
date:inside a fenced code block in the body is not frontmatter. Detect the---fences and operate between them; a blindsed s/^date:/.../will corrupt documentation examples. - A mid-flight
publishcorrection cannot be delegated. The permission classifier blocks a subagent from flippingfalse→trueon relayed authority, correctly. Whoever holds the operator’s actual instruction must apply those edits directly. - Known spec defect: “never flip
publishfalse → true” cannot distinguish a standing decision from a value the current sweep wrote minutes earlier. It wants an explicit carve-out for values written by the running sweep.
Identity fields — do NOT retrofit with a naive script
site_uuid / hex_code are now in both specs and in the templates, so new
files get them. A retrofit pass over the ~2,700 existing documents is an operator
decision, and a careless one is destructive: roll-up copies mean a find-based
pass would assign different site_uuids to copies of the same document,
permanently breaking the dedup property that justifies the field. One blueprint
in this tree resolves to eight paths across originals, roll-ups, and generated
content — all correctly sharing one uuid today.
fullstack-vc/changelog (32 entries) is the worked precedent for doing it
safely. What made it safe: it was operator-directed, and it touched originals
only — changelog/ in the source repo, never a roll-up path. Roll-ups
regenerate from the original and inherit the value, which is the correct
direction. Any future retrofit should follow the same rule: mint on the original,
let derivation carry it, and never walk a corpus/, stream/, rollup/, or
generated-content/ path.
Also: never let an agent type an identifier. Seven site_uuid values already
in the tree contain non-hex characters (…a2f98752z7b9, …396h-4rb4…,
y8f59v34-…) — each a model emitting a plausible-looking string instead of
calling a generator. Use uuidgen | tr 'A-Z' 'a-z' and
LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c6.
Known issues surfaced, not fixed
These change existing values rather than adding keys, so each needs its own directed pass:
- Broken ledes. Seven were repaired by hand in
memopop-orchestrator. More exist:lede: "---"where an extractor captured a horizontal rule, ledes truncated mid-sentence on the period insidee.g., and six files whosetitleis"Summary"or"Overview"taken from the first##. A lede is written, never extracted — see the spec. summary:used wherelede:belongs across most offullstack-vc’s older changelog entries (22 of them). This is now a name collision, not just untidiness: the specs definesummaryas the agent-facing field. A renderer doinglede ?? summary—fullstack-vchas one — will render agent prose in a human slot on any file that adopts the new meaning without alede. The fix is to write real ledes, not to shorten the summaries.- Date disagreeing with filename:
dark-matter/changelog/releases/2025-12-06_01.mdcarries2025-12-25— a 19-day gap. Preserved verbatim through the rename rather than guessed at. The filename is usually the better source. - Stale documentation teaching the old key:
banner-site/changelog/2026-01-19_02.mdcontains a fenced frontmatter example usingdate:. Left alone — rewriting a shipped changelog body edits a historical record — but it will keep teaching the deprecated spelling. Arguably wants relocating to acontext-v/doc. - Three
memopop-orchestratorentries dated 2025-04 appear to be 2026 entries with a year typo propagated from filename into frontmatter. Fixing means renaming files. - Credential values committed:
dididecks-ai/context-v/reminders/Auth-Loose-Ends.mdcarries plaintext passcodes and a production database hostname.publish: falsedoes not fix that — those want rotating. - Two unfixed access-control weaknesses described in
dididecks-ai/context-v/specs/Calmstorm-Auth-Inventory.md. Engineering bugs, not disclosure settings.
See also
- [[Graphiti-Over-The-Lossless-Corpus]] — the other thread in this session; the frontmatter work directly improved its temporal anchors (undated changelog entries fell from 77 to 26).
- [[Tidy-Context-Vigilance-Files-Across-All]] — the broader quality plan this sweep clears the way for.
context-v/skills/context-vigilance/references/frontmatter-spec.mdcontext-v/skills/changelog-conventions/references/frontmatter-spec.md