← 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: remaining0
Missing the editorial pair0
No frontmatter at all0

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.md
  • context-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:

ScopeKeys
context-v/**.mddate_created, date_modified, publish
changelog/**.mddate_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.date being 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-site and dark-matter both had this.

Status: RESOLVED tree-wide. No content collection anywhere still declares a required date. Verified 2026-08-17.

RepoState
fullstack-vc, dark-matter, banner-site, mpstaton-siteFixed — schema tolerant, renderers resolve through a fallback chain
twf_site, reach-edu-hub, memopop-site (lenientDate)Were already lenient
Everything elseNo 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

  1. 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.
  2. Add a resolver returning the first key that parses, falling back to the YYYY-MM-DD in the entry id. Reference implementations:
    • astro-knots/sites/fullstack-vc/src/lib/changelog-date.ts
    • astro-knots/sites/banner-site/src/utils/changelog-date.ts
    • astro-knots/sites/dark-matter/src/lib/dates/resolveEntryDate.ts
  3. Route every renderer through it — including sort comparators (entryDateMs) and truthiness filters (hasEntryDate). Miss a filter and you get the silent failure above.
  4. 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:

  1. Does any schema require the key being removed? (build failure)
  2. 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:

  1. existing frontmatter on the file
  2. a date in the filename or parent directory name
  3. a date stated in the document body
  4. git log --diff-filter=A --follow --format=%ad --date=short -- <file> | tail -1
  5. stat — 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-orchestrator ran 47 false to 5 true. 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 a context-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 those false was 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 .gitmodules names, 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 grep for 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, never date_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/ and splash/src/rollup/ are derived. A naive find for a repo name will surface the corpus copy first — resolve paths through sources.md or 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/changelog is its own git repo inside dark-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 blind sed s/^date:/.../ will corrupt documentation examples.
  • A mid-flight publish correction cannot be delegated. The permission classifier blocks a subagent from flipping false → true on relayed authority, correctly. Whoever holds the operator’s actual instruction must apply those edits directly.
  • Known spec defect: “never flip publish false → 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 inside e.g., and six files whose title is "Summary" or "Overview" taken from the first ##. A lede is written, never extracted — see the spec.
  • summary: used where lede: belongs across most of fullstack-vc’s older changelog entries (22 of them). This is now a name collision, not just untidiness: the specs define summary as the agent-facing field. A renderer doing lede ?? summary — fullstack-vc has one — will render agent prose in a human slot on any file that adopts the new meaning without a lede. The fix is to write real ledes, not to shorten the summaries.
  • Date disagreeing with filename: dark-matter/changelog/releases/2025-12-06_01.md carries 2025-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.md contains a fenced frontmatter example using date:. Left alone — rewriting a shipped changelog body edits a historical record — but it will keep teaching the deprecated spelling. Arguably wants relocating to a context-v/ doc.
  • Three memopop-orchestrator entries 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.md carries plaintext passcodes and a production database hostname. publish: false does 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.md
  • context-v/skills/changelog-conventions/references/frontmatter-spec.md