⚒️ The Foundry Pattern

The Pattern

Build with the Astro Stack

Plan Your Foundry gives the stack-neutral route. This page applies that route to one repository composition: Markdown knowledge in a content/ tree, an Astro site on GitHub Pages, TypeScript and Zod content contracts, Vitest validation, and shared @galaxy-foundry/* mechanics from foundry-lib.

The page owns the composition seam: the repository layout, order of work, instance-supplied policy, and end-to-end checkpoints. foundry-lib owns package behavior and APIs. A concrete Foundry owns its domain knowledge, kinds, vocabularies, renderers, targets, and acceptance policy. All three current instances use this composition. The TDA Bioinformatics Foundry is the active reference build: it consumes these shared contracts directly and feeds hardening improvements back into the earlier instances, especially Statistical Genomics.

Instance-owned · authored Domain knowledge content · glossary · examples · Molds
Instance-owned · composed here Repository policy kinds · registries · validators · renderers · targets
Shared · versioned foundry-lib schemas · links · site shell · casting mechanics
Generated · checked Reader and runtime outputs site · manifests · catalogs · cast artifacts · provenance
This guide owns the seam between instance policy and shared mechanics. Package internals stay canonical in foundry-lib.

From pattern commitment to implementation seam

What a Foundry Needs defines the pattern-level boundary. The foundry-lib shared-substrate guide explains which mechanics have earned a shared package, and its package boundaries page is canonical for what those packages can truthfully own.

Pattern concern This Astro composition Canonical detail
Knowledge Base (KB) Astro renders the content tree; the instance supplies identity, routes, corpus, and visual contract. content-reader boundary
Mold (typed actions) Instance-defined Zod kinds make action boundaries and reference requirements checkable. foundry-lib integration guide
Corpus-First, Not Invention-First Source directories, authored derivatives, licenses, and refresh posture remain instance content and policy. Plan Your Foundry
Progressive disclosure The shared reference vocabularies combine with instance-defined reference kinds and placement support. reference-contract package
Casting and Provenance The instance resolves and renders; shared helpers handle placement, reconciliation, licensing, and the provenance record. cast architecture

The domain’s external check remains instance-owned. Part 8 asks what can produce an independent verdict without pretending Astro or a shared package can answer that domain question.

Keep the handoffs explicit

How to use the checklist below

The checklist is rendered verbatim because it is a repository composition recipe: named paths, commands, and checkpoints precise enough to follow by hand or hand to an agent. Work it top to bottom and expect to loop back. When it names package behavior, the linked foundry-lib architecture and package documentation are canonical. Toolchain versions live in one REFERENCE STACK; current @galaxy-foundry/* ranges stay canonical in foundry-lib and are adopted by capability rather than mirrored here. The closing INSTALLED / MECHANICAL / YOURS grading keeps shared machinery distinct from instance policy and domain knowledge.

STANDING UP A FOUNDRY — WORKING CHECKLIST

This is prescriptive: it maps the abstract pattern onto ONE concrete implementation — an
Astro static site on GitHub Pages with schemas, validation and registries. The abstract "Plan Your
Foundry" page stays stack-agnostic; this one names packages, versions, paths and commands.
Nothing here is the only way to satisfy the pattern — it is the one worked example.

Order of work: the glossary comes FIRST (Part 1) — it is the KB's highest fan-in reference
and the shared vocabulary every cast carries, so pin it before anything answers to it. Then
the engineering substrate (Parts 2–4: reading surface, frontmatter contract, registries +
validator + CI), which is inherited — partly INSTALLED from @galaxy-foundry packages and
partly re-stood-up as known machinery, a split "The shared substrate" below draws and the
closing section grades. Then the rest of the domain knowledge (Parts 5–8: corpus, Molds, the
external check, composition), which only your domain can supply.

Do not read Parts 2–4 as one all-at-once site launch. The first reviewable loop is deliberately
small: the glossary, one explicit collection row, one kind with one real note, one content-reader
binding, the shared shell/frame, a built-output contract, and CI running a frozen install plus that
contract. Expand the taxonomy only after this slice proves its routes, links, emitted styles and
search coverage.


PART 1 — DEFINE THE VOCABULARY FIRST

A Foundry names things that did not exist before it, and the glossary is the KB's highest
fan-in reference — every cast carries it, and a human learns the domain from it.
Pin it first, in one glossary: content/meta/glossary.md (the deliberate loose-doc exception
inside the design-record directory, excluded by name from the `meta` collection and rendered by
Part 2's render-vault-doc helper) that the rest of the corpus answers to.

- Bootstrap it with the SHARED SUBSTRATE TERMS before coining a single domain term. Every
  Foundry glossary carries these eight, inherited from the pattern glossary (the authority —
  copy its definitions, do not re-invent them):
      Knowledge Base (KB), Mold, Reference, Cast, Casting, Skill artifact, Target, Provenance.
  Use these EXACT headwords so a term means the same thing in every instance.
- THEN the STACK terms, because adopting a stack means adopting its words. This one brings eight,
  pinned in the pattern site's Astro-Stack Glossary and inherited exactly as the eight above are —
  copied, not re-coined:
      Note, Kind, Shape, Companion, Collection, Kind context, Kind manifest, Tag/Facet.
  This is the tier an instance is likeliest to get wrong, and the reason is that these terms feel
  like yours: you will type `kind` fifty times a day in your own repository, and re-deriving what
  it means locally is the natural thing to do. It is also the tier that does not hold for a Foundry
  on another stack, so it is not the pattern's to demand.
  Note / Kind / Companion define each other in that order — a note declares a kind, a kind fixes
  whether its notes are files or directories, and only a directory-shaped kind can have
  companions. Pin all three or none: `companion` used before `note` is pinned degrades into "some
  file near another file", which cannot support one consistent layout contract.
  One collision to resolve out loud rather than inherit: `kind` names BOTH a note's kind and a
  reference's kind, and both are closed vocabularies you will write down. Say which you mean
  wherever both are in scope.
- THEN coin the domain terms, corpus-first — add a term when a Mold actually needs it, not
  before.
- One authoritative sentence per term: abstract meaning first, a brief (e.g. …) where it has a
  concrete realization.
- One word per concept; retire the rivals. Resolve singular/plural, capitalization, and
  half-synonym drift here before it spreads.
- ONE GLOSSARY, ordered for a reader learning YOUR domain — not partitioned by where each term
  came from. Terms arrive from three places (the pattern, your stack, your own coinage), but that
  is PROVENANCE, and provenance is a property of an ENTRY rather than a reason to split the page.
  Record it per term the way a kind records `layer` instead of living in a separate directory of
  substrate kinds: one marker, and a consumer asking what transferred computes the answer instead
  of trusting your headings. A reader meeting your vocabulary for the first time needs it grouped
  by MEANING; they did not arrive wanting a tour of which upstream supplied what.
  The marker still carries everything the older "keep them scoped apart" rule was actually after:
  an inherited term keeps its inherited DEFINITION, and a term you coined must not be able to pass
  as one the pattern handed you. Both hold wherever the entry sits on the page. What you must not
  do is redefine an inherited term to mean something local — that is the real failure, and moving
  it to a different section was never what prevented it.
- Make the glossary the tie-breaker: where two pages disagree, the glossary wins — state it
  outright. Casting copies glossary entries VERBATIM.


REFERENCE STACK  (toolchain versions checked 2026-08-06 — bump coupled peers together)

  Toolchain     Node 24 (engines: >=22.12), pnpm via corepack (lockfile committed, the version
                pinned by `packageManager` in package.json), TypeScript ^5.7.0
  Framework     astro ^7.1.6 (static output), @astrojs/check ^0.9.4, vite ^8.0.0 (dev)
  Styling       tailwindcss 4.3.3 + @tailwindcss/vite 4.3.3 + @tailwindcss/typography
                ^0.5.19; @fontsource/atkinson-hyperlegible ^5.2.8
  Markdown      marked ^18.0.2 (standalone render) + a thin src/lib/marked.ts wrapper;
                @astrojs/markdown-remark ^7.2.2 for the site's own pipeline (see Part 2)
  Math          remark-math ^6.0.0 + rehype-katex ^7.0.1 + katex ^0.16.45
  Search        astro-pagefind ^2.0.1 (integration) + pagefind ^1.5.2 (dev)
  Schema        zod ^4.4.3  (import `z` from 'zod', NOT from 'astro:content')
  YAML          js-yaml ^4.1.0 + @types/js-yaml ^4.0.9
  Validator     vitest ^4.1.10

  Foundry       Install @galaxy-foundry packages BY CAPABILITY using foundry-lib's current
                Getting Started guide and package metadata. Releases, APIs and peer compatibility
                are canonical there, not mirrored here. Commit the chosen ranges in
                site/package.json and the resolution in the lockfile. The substrate section below
                owns which capability each package supplies and when an instance needs it.

  Layout        repo root holds site/ and content/ as siblings; paths under content/ resolve
                from the site/ cwd as '../content/…'. content/meta/ holds flat `meta` design
                records; glossary.md is excluded by name and an ops log may remain a second
                explicit loose-doc exception.

  This table is the source of truth for this worked example's TOOLCHAIN versions. The substrate
  table below is the source of truth for package CAPABILITIES and adoption timing. foundry-lib is
  the source of truth for @galaxy-foundry package versions and APIs. The parts below name MODULES
  and WIRING; they do not mirror dependency ranges.

  "Coupled peers together" is not a style preference: `zod`, kind-manifest and kind-schema move
  TOGETHER, in one commit, because the schema packages peer on one zod major and inspect the same
  shapes the instance builds. See the zod bullet below for what a split bump does. Unrelated
  @galaxy-foundry capabilities do not need ceremonial lockstep bumps.


THE SHARED SUBSTRATE — INSTALL IT, DO NOT COPY IT

The baseline reader-and-contract substrate is not yours to author. Its capabilities ship from
jmchilton/foundry-lib under the @galaxy-foundry scope. Install each capability when the instance
reaches it, using foundry-lib's Getting Started guide and package metadata:

  @galaxy-foundry/license-policy      the license → redistribution table + loader
  @galaxy-foundry/kind-manifest       the manifest format: reader + zod-shape deriver,
                                      and the LAYOUT vocabulary a manifest carries
  @galaxy-foundry/kind-schema         the kind contract: what a kind IS, how one is assembled,
                                      path→kind routing, the kind.md loader, the companion check
  @galaxy-foundry/content-reader      collection-backed file selection, note ids, wiki-link map
                                      registration, and parsed/raw Markdown link binding
  @galaxy-foundry/reference-contract  the four behavior vocabularies every instance inherits
  @galaxy-foundry/site-kit            the reading shell, reference view and the behavior contracts
                                      for navigation, note framing, tags, theme state, search and
                                      deployment base
  @galaxy-foundry/tag-registry        the meta_tags.yml format and its accessors
  @galaxy-foundry/wiki-links          the [[Target]] grammar, path/term slugging, exact resolver,
                                      parsed- and raw-Markdown transforms

Additional packages are capability-specific, not part of the baseline install:

  @galaxy-foundry/cast                deterministic casting assembly, reference placement,
                                      reconciliation, licensing and provenance. Adopt when the
                                      first real Mold is ready to cast.
  @galaxy-foundry/audit-citations     scholarly-citation extraction, evidence capture and replay.
                                      Adopt only when citation auditing is part of the instance's
                                      acceptance policy. Experimental and deliberately not a
                                      generic external-check framework.

The package count is not a completeness score. A Foundry without citation auditing does not install
audit-citations; a reader-only vertical slice need not install cast. Capability-based adoption is
the package model, and the lockfile is the record of what this instance actually selected.

WHAT EACH PACKAGE DOES NOT DO is the half that matters. The package ships the FORMAT or the
MACHINERY; your instance supplies the CONTENT. Get the line wrong in either direction and you
have either a dependency dictating your domain or a copy that drifts:

  license-policy      Ships the table — which SPDX id permits verbatim carry. It names policy,
                      license-file obligation, copyleft and obligations; it names NO casting mode.
                      A license constrains what expression a note carries, not how a bundle places
                      it. `declaresVerbatimCarry(derived)` separates pass-through/quoted content
                      from Foundry-authored own-words prose before the row is applied. You keep
                      license COHERENCE: how a note's declared posture, license file and any
                      vendored source must agree. Those rules vary with the instance's note shape.
                      The package answers "what does this license permit", never "is this note
                      coherent with its license".
                      Load with bundledPolicy() — the table travels with the dependency, so a
                      content tree no longer has to CARRY it to be validated or cast against it.
  kind-manifest       Ships the format, the reader, and the deriver. You keep which kinds exist
                      and how your registries resolve into the context they are built against.
                      The package takes BUILT SHAPES rather than knowing that context. It also owns
                      the LAYOUT VOCABULARY —
                      `NoteShape`, `Companion`, `CompanionRequirement`, `CompanionDisposition` —
                      for the same reason it owns `KindLayer`: a manifest carries these values
                      across a repository boundary, so the closed sets are part of the FORMAT.
                      kind-schema re-exports them rather than declaring a second copy.
  kind-schema         Ships the CONTRACT and the machinery over it: `KindDefinition` (`build`,
                      optional `refine`, plus the `shape` + `companions` declaration it EXTENDS),
                      the `kindDefiner`/`defineKind` identity helper, `assemble` for one kind,
                      `buildKindUnion` for a mixed corpus, the `manifestKinds` bridge into
                      kind-manifest, path→kind routing (`./collections`), the `kind.md` reader
                      (`./docs`), and `companionsOf`/`checkCompanions` over a directory listing
                      you already have — PURE, no I/O, which is what keeps the main barrel
                      importable from browser code. Ships NO KINDS —
                      and, less obviously, NO CONTEXT. The context is the SEAM for the base
                      envelope, registries and field primitives a local kind may draw from, so
                      `KindDefinition` is generic over it and you alias the parameter away once.
                      Ships no collection TABLE either — that names your directories.
  content-reader      Ships the collection-backed filesystem walk, note ids, wiki-link map
                      registration, and parsed/raw Markdown transforms as ONE composition. You
                      keep the collection table, content path, route targets, extra loose targets,
                      and the vocabulary that makes a note addressable by aliases. Frontmatter is
                      read only when aliases or metadata-bearing targets opt in. The package owns
                      applying those aliases and its deterministic collision rules; the instance
                      does not rebuild the walk in order to learn its own vocabulary.
                      Ships no Astro collection exports or schemas. Keep those explicit so
                      `entry.collection` continues to discriminate precise frontmatter types.
  reference-contract  Ships used_at / load / modes / evidence. You keep `kinds` — exactly the half
                      that varies by domain. Inherit the four whole: a term you have not used YET
                      is inheritance, not drift. `modes` ships exactly two deterministic placement
                      rules — verbatim and sidecar — backed by concrete renderers. Narrow the
                      group to the modes your targets implement; the validator must reject
                      anything the caster cannot reproduce byte for byte. A narrowed group is
                      AUTHORED, so it rejoins the dead-vocabulary check.
  site-kit            Ships the shared MARKUP, behavior-critical component styles and the rules
                      inside them — active navigation, pre-paint theme state, Pagefind body
                      coverage, overflow behavior, the invariant ContentNote frame, TagChips, the
                      reference-contract view, deployment base handling, and SPECIMENS: named
                      props for every case its components claim to handle, with the surface each
                      case may render on. You keep the IDENTITY: both names, the description, the
                      repository, the list of destinations, and how many of them fit on the bar
                      before the rest go under "More". That last one is set by measuring this
                      instance's wordmark and links, not by copying another site's count.
                      Ships no complete GLOBAL THEME. It writes Tailwind utility classes, carries
                      structural styles beside the components that need them, and NAMES custom
                      properties and global classes your stylesheet defines. The palette is the
                      part of a reading surface that is genuinely an instance's. Point Tailwind at
                      the package source and use its emitted-CSS checks; both missing halves build
                      cleanly and render wrong.
                      Ships ONE value it does NOT take a prop for: the width of the reading column.
                      Treat that as part of the shared shell; a genuinely different measure is a
                      reason to revisit the contract rather than add an unused configuration prop.
                      Ships the BASE RULE — `shellBase`, which normalizes a deployment base into
                      the prefix every href in your site hangs off. Not a general utility that
                      happens to live here: the shell has to resolve its nav against a base, so
                      the definition is already its, and a page that computes its own is a second
                      answer to a question the chrome has already answered. Reads NO environment
                      to do it — you pass the value in.
  tag-registry        Ships the format and its three rules (see Part 4). Ships NO VOCABULARY:
                      facets are the browse axes of one domain, so there is no useful universal
                      subset to inherit.
  wiki-links          Ships the grammar, path and term slugging, the resolver, and parsed- and
                      raw-Markdown walks. It ships NO LINK MAP by itself: which notes exist and
                      what each is addressable by is instance data. content-reader owns registering
                      that data for collection-backed sites; a lower-level direct consumer uses
                      `fileSlug` and `slugify` so the path and authored-target halves cannot drift.
  cast                Ships `castMold`, cast-contract parsing, the generic skill-document shape,
                      Kind-layout companion expansion, deterministic placement/reconciliation and
                      provenance. You keep the kind declarations, target policy, corpus index and
                      hooks: domain skill sections, non-verbatim renderers, extra bundle files and
                      external checks. Pass the Kind table as `kindLayouts`; do not translate it
                      into another companion policy. Errors and drift return as values; your
                      caller decides how they affect the process. Do not disguise instance
                      vocabulary as substrate.
  audit-citations     Ships one replayable scholarly-citation audit and its strict wire formats.
                      You keep source selection, trusted hosts, artifact kinds and the acceptance
                      gate. It is experimental; its citation-specific candidates and verdicts are
                      not a base schema for every domain's external check.

COMPOSE, DO NOT RE-EXPORT. Where your instance supplies a half, write ONE composition point that
keeps the signature your callers already use, and let every other call site import from the
package directly:

    export function loadReferenceContract(p = findReferenceContractPath()): ReferenceContract {
      return buildReferenceContract({ kinds: loadInstanceKinds(p) });
    }

Done this way, adoption touches the composition point rather than every consumer. Re-export only a
TYPE a caller must name to build an options object. A barrel
re-exporting the package's whole API is a name list that drifts from the package's, and one more
place to look.

INTEGRATION FAILURES THAT WILL BITE:

- A package can install correctly and still not work, if something other than the module resolver
  has to find its files. site-kit is the case in this set: Tailwind does not scan node_modules, so
  its markup contributes no CSS until your stylesheet points at it explicitly. Part 2 has the line
  and the assertion that proves it. Treat the shape as general — when a package ships anything a
  BUILD TOOL rather than an IMPORT has to reach, expect a silent default and go looking for it.
- Declare the dependency in the package that IMPORTS it — INCLUDING site/. A package resolving a
  shipped YAML asset relative to import.meta.url, reached only through a LINKED workspace package,
  gets inlined into an SSR chunk by Vite, and the path then resolves against the chunk directory
  — site/dist/.prerender/chunks/ on 7.1.6 — instead of node_modules. It passes every test and
  every typecheck, and fails at `astro build`. The Part 2 anchoring rule is the same mechanism
  seen from the consumer's side; this is the half that bites while you are packaging.
- kind-manifest pins `zod` as a PEER dependency at ^4, because the deriver reads `_zod.def.type` —
  zod INTERNALS, since zod exposes no public reflection API and the alternative is the hand-written
  field table this mechanism exists to prevent. Three consequences, the last of which is what a
  port actually costs:
    * The peer range admits ONE major, with no v3 fallback. Supporting both was considered and
      rejected: a branch on internals is code whose untested arm renders every field `any` — a
      manifest that generates, validates and publishes while saying nothing. An instance upgrades
      zod and these two packages in the same commit, or it does neither.
    * The deriver must read the SAME zod instance your schemas were built with. A duplicated zod in
      the tree renders every field `any` by the same route. Keep one zod, and keep it in range.
    * What zod 4 renamed is not a detail you can skip past if you are porting a v3 instance: every
      type tag moved off `_def.typeName` and changed spelling (`ZodString` → `string`), an array's
      element is `element` not `type`, a literal carries `values` (plural — v4 literals may hold
      several) not `value`, an enum carries `entries` (an object) not `values`, a discriminated
      union now tags as plain `union`, and a `ZodRawShape`'s values type as the CORE schema, which
      carries neither `.isOptional()` nor enough structure to walk. All of that is the PACKAGE's
      problem, not yours — the point of installing it. What IS yours: `z.ZodObject<T, 'strict'>`
      becomes `z.ZodObject<T, core.$strict>` (the second parameter is a config object in v4, not a
      mode string), `z.ZodType` loses its middle `ZodTypeDef` parameter, `z.string().url()` becomes
      `z.url()`, `z.ZodIssueCode.custom` becomes the string `'custom'`, and `z.ZodIssue` becomes
      `z.core.$ZodIssue`. `superRefine`, `RefinementCtx`, `discriminatedUnion` and `.strict()` all
      survive unchanged, so the schema BODIES mostly do not move.
- The zod bump and the astro bump are ONE COMMIT, not two, and the reason is worth knowing before
  you plan the work: astro 5 types `defineCollection`'s schema as `ZodType<any, ZodTypeDef, any>`,
  which a zod-4 schema does not satisfy — so the intermediate state does not typecheck in either
  order. The runtime is worse than the types suggest: astro 5 reads `parsed.error.errors[0]`, which
  v4 renamed to `.issues`, so the FIRST invalid note throws a TypeError instead of reporting which
  field of which note failed. A validator that reports "cannot read property of undefined" where it
  used to name the field has not caught the note — it has lost the ability to.
- A caret on a 0.x version does not cross a minor. `^0.3.0` will not take 0.4.0, so every substrate
  range moves BY HAND on a minor release; there is no `pnpm update` that will do it for you, and
  nothing fails while it is not done — you keep the old package.
- A local package link can survive the release it was waiting for. Before a consumer PR leaves a
  worktree, remove its `overrides` and every `link:` resolution, regenerate the affected lockfile
  from the published range rather than editing a resolution by hand, then run
  `pnpm install --frozen-lockfile` and inspect `pnpm list --depth 0`. The manifest naming a release
  while node_modules or the lockfile still names a sibling worktree is not published-package
  adoption; it is a build whose success depends on a directory outside the repository.
- If your repo is a WORKSPACE, state ONE version of each package across every member. A range that
  admits the current release does not mean the lockfile resolved it; two installed versions can
  leave one member resolving a build that predates an export without producing a version-conflict
  error. Bump the ranges as a set, inspect the resolved tree, and check what is actually on disk.

THE RULE FOR WHAT MOVES THERE: share a stable mechanism or format, not a domain decision that only
looks reusable. The base note envelope, kind schemas, reference kinds, license-coherence rules and
domain renderers stay local until a real cross-instance contract exists. Duplication is evidence
to investigate; it is not by itself a package boundary.

Three boundaries are especially important:

  link vocabulary     What a note is addressable BY is domain. The instance supplies its alias
                      vocabulary; content-reader owns walking the admitted notes, registering the
                      addresses once, and applying collision precedence.
  domain furniture    ContentNote owns the invariant frame and TagChips owns the shared tag markup.
                      The things that render a Mold's phases, a paper's provenance, or a package's
                      software facts remain domain. They enter the frame through slots rather than
                      by copying the frame into one component per kind.
  the palette         The shell names theme roles without supplying instance values. A token the
                      instance forgets renders unstyled rather than failing, so assert the
                      package's style-gap helpers against emitted CSS.


PART 2 — STAND UP THE READING SURFACE

The corpus renders to a static Astro site under site/. Stand it up first (empty but for the
glossary): it is where every later artifact is read, and `astro build` is the fastest signal
that a note is well-formed.

- `pnpm create astro@latest site` — static output. Bind the base to a NAME in
  site/astro.config.mjs, and use the name everywhere else in that file:
    const base = '/<foundry-name>';
    export default defineConfig({ site: 'https://<user>.github.io', base, … });
  Outside the config, reuse `import.meta.env.BASE_URL`; never re-hardcode the base. Inside it that
  variable does not exist yet, which is the whole reason for the `const`. Repeating the literal in
  the config and wiki-link options creates two deployment bases that can drift without an error.
- Wire the stack's render deps into astro.config.mjs (packages/versions: the table above):
    markdown.processor = unified({          // from '@astrojs/markdown-remark'
      remarkPlugins: [[remarkWikiLinks, { base }], …your domain plugins],
    })
    compressHTML = true
    vite.plugins += tailwindcss()    // @tailwindcss/vite
    integrations += [pagefind()]     // astro-pagefind
  The wiki-link plugin is substrate. Other remark/rehype plugins are selected by the corpus: add
  math, citation or source-format transforms only when the domain content requires them.
  Set the body font to Atkinson-Hyperlegible — a human reader is one of the two audiences, so
  legibility is a substrate concern.
- ASTRO 7 RUNS NO REMARK OR REHYPE PLUGIN UNLESS YOU ASK. It renders markdown with Sätteri, a Rust
  pipeline, and `markdown.remarkPlugins` — the astro ≤6 spelling, and the one every tutorial still
  shows — is not the wiring any more, which is why the block above names `markdown.processor`
  instead: opting the unified pipeline back in explicitly. For this stack that is not a nicety —
  `[[wiki-links]]` is supplied by the installed wiki-links package, and Part 4's link check asserts
  against a grammar the renderer would no longer be applying.
  With `@astrojs/markdown-remark` absent the build fails and names the package to install. With it
  present, legacy `markdown.remarkPlugins` wiring produces a deprecation warning and is migrated
  onto a compatible unified processor. Write `markdown.processor` directly. The arrangement that
  still loses plugins quietly is setting `processor: satteri(…)`
  and leaving `remarkPlugins` beside it: the shim will not migrate onto a non-unified processor,
  so you get a warning and no plugins. Do not spell both.
- `compressHTML: true` is the pin in this part that DOES fail silently, and it is the one to spend
  the paranoia on. 7 defaults it to 'jsx', which drops the whitespace between inline elements that
  sit on their own source lines. Removing the pin on a finished site builds clean and glues prose
  together on every page — nav rendering as `The PatternThe CaseInstances`, and the spaces around
  inline links gone. Nothing goes red. Worth a comment in the config saying so, because a future
  reader deleting it gets no other signal.
  Both pins share a lesson bigger than either: ACCEPT A BUMP ON THE RENDERED PAGE, NOT THE BUILD.
  A green `astro build` is evidence about the build, and these are defaults that change the OUTPUT.
  Keep the pre-bump dist/, then diff the rendered TEXT of every page against it: strip the tags,
  collapse runs of whitespace, compare. A glued word merges two tokens into one, so it shows up as
  a changed line. Count the resolved `[[link]]` anchors on both sides too — equal counts is the
  check the processor migration actually needs.
  THE STYLESHEET IS OUTPUT TOO, and the markup diff will not show you it changed. Tailwind scans
  source text, comments included, so a comment can keep an otherwise-unused class alive. Diff the
  built stylesheet as a set of rules, not as a file: the filename carries a content hash and Astro's
  scoped-style ids change whenever a component changes path, so both differ for reasons that are
  not differences. Normalize those two, then compare.
  Strip each tag to the EMPTY STRING, not to a space. Replacing `</a>` with a space re-supplies
  the very whitespace compressHTML dropped, and the comparison passes clean on a site whose every
  page has visibly run together — which is this bullet's own failure mode, found by running it.
- Declare ONE detail route, site/src/pages/[collection]/[...slug].astro, and take its route segment
  from `entry.collection` — so the collection key IS the route, by construction rather than by
  everyone remembering. Keep an index.astro per collection; a browse page is a page, and they
  differ. The collection table (Part 3) is then the only list of collections in the repo, and
  everything that needs to know where a note lives — the Astro loader, the validator walk, the
  drift checks, the wiki-link map — reads it.
  One route keeps search indexing, tag placement, back navigation, title and summary behavior in
  one answering site. Express genuine per-collection differences as data, and dispatch domain
  furniture on `entry.collection` or the note's `type`.
    Precise frontmatter does not require separate routes. Spread explicit `getCollection` results
  into one array so they remain a discriminated union; mapping them through a homogeneous list
  widens them to their common shape. Test that the detail route derives its segment from the entry,
  every collection has a table row, and every collection carrying notes reaches content-reader.
- INSTALL the reading shell — @galaxy-foundry/site-kit — rather than writing one. What you write
  is site/src/layouts/Base.astro, and it is a COMPOSITION POINT of about fifteen lines: it hands
  SiteShell your identity plus your base and `Astro.url.pathname`, and receives the document
  skeleton, the header with its derived navigation, and the footer. Keep every page importing
  that layout; nothing else in your repo should know the shell is a package.
    The identity is one object in site/src/lib/site-identity.ts — short name, full name,
  description, repository, nav destinations, and how many stay on the bar. Write it out as DATA;
  active state is derived by the shared shell rather than repeated as per-entry closures.
    The BASE is one value in site/src/lib/site-base.ts — `shellBase(import.meta.env.BASE_URL)` —
  exported for the layout AND for every page and component that builds an href. ASSERT THAT IT IS
  THE ONLY MODULE READING BASE_URL, and assert the rule rather than the expression, because both
  ways of getting it wrong build green. Reading it unstripped is CORRECT at a domain root, where
  the strip does nothing — so dev and any root deploy confirm the bug, and only a sub-path deploy
  shows the doubled slash. A stripper that trims one character too many renders no error either,
  just links that land somewhere else. A page that computes the base inline renders byte-for-byte
  what an imported one does, so no diff of the built site will ever show you the next copy; only
  a rule read off the source will.
    READ THE ENVIRONMENT IN YOUR FILE, NOT THE PACKAGE'S. The base and the pathname are props for
  a reason: under vitest `import.meta.env.BASE_URL` is mirrored into `process.env` as `/`, and a
  child `astro build` prefers it over your config — so a suite that lets the shell read the
  environment asserts against a site nobody is shipping, and builds clean while doing it.
- POINT TAILWIND AT THE PACKAGE, AND ASSERT THAT YOU DID. Tailwind 4 does not scan node_modules,
  so add to your stylesheet:
    @source "../../node_modules/@galaxy-foundry/site-kit/src";
  Without it every utility the shell writes is missing from the emitted CSS. The build is green,
  every page renders, and the site has no layout. THE LINE CANNOT BE VERIFIED BY READING IT: a
  misspelled path resolves to nothing and Tailwind says nothing — `site-kitt` builds exactly as
  cleanly as the correct spelling. So assert on the OUTPUT: pick a utility the kit names and your
  repo does not, and check it has a rule in the built stylesheet. `min-h-dvh` is a useful canary.
    If your tests live inside the Vite root, exclude them — `@source not "../../tests"` — or the
  test that names the canary is what puts the class in the CSS, and it cannot fail.
- The PALETTE stays yours. The kit ships structural component styles but no complete global theme;
  it names custom properties and global classes your stylesheet defines. A token you forget
  renders as an unstyled element rather than an error, so use the package's style-gap helpers on
  emitted CSS.
- ACCEPT THE READING SURFACE VISUALLY with a component gallery when the instance has a real theme
  or local visual components. Import `SPECIMENS` from `@galaxy-foundry/site-kit/specimens`; do not
  copy the props cases from the reference app or a sibling Foundry. Render the inline groups on an
  instance-owned page through the ordinary Base layout, so the package's components wear the same
  palette, typography and classes as the corpus. Use `sharesPage(group)` to separate groups that
  need a route of their own: `isolated` components carry document-unique ids, while `document`
  components emit their own `<html>`. Give both real static routes and frame those routes from the
  index. Two headers written directly onto one page build clean and leave the second one's controls
  half-dead; `surface` exists because that failure is invisible to Astro.
    Add instance components and vocabulary cases as local `SpecimenGroup`s in the SAME address
  space. Their `id`, not their component name, is the route prefix and anchor. Derive vocabulary
  cases from the registry they demonstrate — for example every reference kind from the built
  contract — rather than making the gallery a second hand-kept kind list. Keep the gallery frame,
  prose, renderer imports and route placement local: those are the presentation choices that make
  two instances' galleries different. The package owns the cases and the constraints, not a page
  that would impose a visual system on its consumers.
    ASSERT THE BUILT GALLERY, not only the source: every shared and local specimen renders; every
  non-inline specimen has an emitted route; every iframe points to one of those routes; every new
  package group fails until a renderer is wired; the gallery has an inbound link; and standalone
  component documents are explicitly included in or excluded from search. Run the same emitted-CSS
  style-gap checks over the result. The current executable example, including minimum and designed
  themes, lives at:
    https://jmchilton.github.io/foundry-lib/#/development/component-gallery
- Use site-kit's `ContentNote` for the invariant note frame and `TagChips` for tag markup. The frame
  owns back navigation, metadata/badge/reference slots, tag placement, optional heading and
  summary, and the article boundary. Pass a based `tagBase` only after the instance has a tag route;
  without one, the same component renders controlled non-link chips rather than claiming a dead
  destination. Domain furniture stays yours: a Mold's phases, a paper's provenance, or a package's
  software facts enter through slots and never require the shared component to inspect frontmatter.
- Bind the collection-backed reader ONCE in site/src/lib/content-reader.ts with
  `createContentReader({ collections: COLLECTIONS, contentPath, targetOf, aliases? })`. The package
  composes kind-schema's collection matching with wiki-links' transforms, so note-file selection,
  ids, link-map registration, remark links and raw-Markdown resolution all read the same table.
  Do not walk the corpus or parse frontmatter in a caller to build aliases: return the instance's
  alternate addresses from `aliases(meta, id, collection)`, and use the same optional `meta` in
  `targetOf` when route targets need summary tooltips. Readers with neither requirement retain the
  directory-only path.
    Collection declaration order is contractual when two primary basenames collide; aliases fill
  empty addresses and never overwrite primaries; explicit extra targets apply last. Only files
  admitted by the collection table become note targets, so companions cannot acquire links to
  routes that do not exist. Keep these rules in the package and its tests; the instance supplies
  the vocabulary and the acceptance policy for unresolved links.
- Resolve wiki-links ([[Target]]) through that reader in BOTH the renderer and the validator. The
  underlying wiki-links package owns the grammar, exact resolver and tree walk. Two rules it
  settles are worth knowing before you are tempted to re-litigate them:
    * RESOLUTION IS EXACT — no prefix fallback. Prefix matching makes empty or partial targets
      resolve unpredictably and can turn a request for multiple notes into one arbitrary link. An
      unresolved link renders BOLD — visible to a reader, obvious to an author, and never claiming
      to lead somewhere.
    * A BACKTICK MEANS THE SYNTAX, NOT A LINK. `[[Target]]` is how documentation names the token
      and how a note names a slot it cannot link. The transform rewrites text nodes only, never
      inlineCode/code/html. The validator also strips code spans before scanning, so a backticked
      target is intentionally outside link validation. Put the rule in your authoring guide, not
      just in the transform.
- Render explicit loose docs that live OUTSIDE any collection (the glossary, an ops log) through a
  small local src/lib/render-vault-doc.ts composition: `contentReader.resolveMarkdown` supplies the
  same link map as typed notes, then the instance's Markdown renderer and term-anchor transform
  finish the document. Call it from a dedicated page — e.g. site/src/pages/glossary.astro reading
  content/meta/glossary.md. Exclude each one BY NAME from the collection that otherwise claims its
  directory; sharing `content/meta/` is a filing decision, not a reason for every file there to
  bypass validation. `content/meta/*.md` is a validated `meta` collection, with only the named
  loose docs skipped. The failure mode to avoid is a hand-written route registry standing in for a
  collection: it omits real pages silently, and no schema or drift check ever reports the gap.
- EVERY SITE MODULE THAT READS A FILE OUTSIDE site/ ANCHORS ON `root` FROM `astro:config/server`,
  never on a `../` count from its own `import.meta.url`. Give that one line its own module,
  src/lib/repo-root.ts, because the import resolves only inside an Astro build; anything worth
  testing outside one takes a directory rather than finds one.
  `astro build` collapses pages, components and lib modules alike into a single chunk directory.
  A hop count written against a source module can therefore resolve against a different depth in
  the built site. The failure often looks like an empty list rather than a build error, so assert
  expected item counts as well as rendered text. A test should walk site/src and fail any module
  combining `import.meta.url` with a literal `..`; strip comments before scanning to avoid false
  positives.
  Exempt an upward search that walks one `dirname` at a time until it finds what it wants: that is
  depth-tolerant by construction, and it is how you read an installed package's own version.
  `path.resolve('../content')` is a different and lesser hazard — cwd is not a build-time property,
  so it survives bundling and fails only if the build is invoked from outside site/. Keep it where
  the module is loaded before `astro:config/server` would resolve, content.config.ts imports in
  particular, and say in the file that this is why.
- Dev: `pnpm run dev` (astro dev). Build: `astro build --force`.


PART 3 — AUTHOR THE KINDS (THE CONTRACT IS INSTALLED; ONE KIND PER DIRECTORY)

Author the kinds one time and let both the site build and the standalone validator import them.
One encoding, two consumers — the schema is the sole authority on frontmatter shape. What a KIND IS
is not yours to define: `@galaxy-foundry/kind-schema` ships the contract, the assembly, the routing
and the doc reader. What you author is which kinds exist and what each requires. Author them as a
DIRECTORY PER KIND, not one module holding every kind. A kind is a unit of meaning: it has a
purpose, a required metadata set, and a worked example, and those three belong beside each other.

  types/
    context.ts          the shared kind context — the base envelope + field primitives, plus the
                        one-time alias of the package's generic (below)
    index.ts            the barrel: the ONE enumeration of the kinds this instance defines
    <kind>/schema.ts    export const kind = defineKind({…})  (this kind's contract — NOT
                        annotated `: KindDefinition`; see "defend the inference" below)
    <kind>/kind.md      what the kind is FOR, and why each required field is required
    <kind>/example.md   ONE minimal valid note of this kind — frontmatter and a short body

Adding a kind is adding a directory plus one barrel line. Nothing else is edited, and a reader who
wants to know what a Mold is opens types/mold/ rather than scrolling a 400-line union.

- AUTHOR THE `meta` DESIGN-RECORD KIND FIRST, beside the domain kinds. Design records accumulate
  whether or not you planned for them, and the ones sitting outside a collection are the ones that
  go stale unseen. Route them like any other note. The shared shape is:
    * `kind: 'meta'`, `layer: 'substrate'`, `shape: 'file'`, `companions: []`;
    * flat files under `content/meta/*.md`, excluding `glossary.md` and any other explicit loose
      document by name in the collection pattern;
    * `title`, `record_kind: foundation | infrastructure`, and integer `order` in addition to the
      honest lifecycle envelope your instance can support;
    * one design-record index sorted by `(record_kind, order)`, with a test rejecting duplicate
      order values within a shelf.
  A design record is about the Foundry rather than the domain it knows. It is not a Mold reference
  and is never cast merely because it lives under `content/`.

- SEED A SMALL ARCHITECTURE SET WITH ONE PURPOSE PER RECORD. The SPLIT is the rule: no catch-all
  architecture document, and every record able to say what it does not own. The five names below
  are the shared default, not a proven taxonomy — adopt them so a contributor can move between
  Foundries without relearning where to look, and if your instance has a concern these five do not
  cover, add a record and say so in the map. Do this after enough implementation exists to describe
  truthfully; a greenfield scaffold may say explicitly that a package, caster, or fixture layer
  does not exist, but must not document planned machinery as current. Use these paths and
  questions:
    * `content/meta/architecture.md` — the SHORT MAP: how do the major pieces and boundaries fit,
      and which focused record should a reader open next? Aim for an orientation pass, not an
      encyclopedia.
    * `content/meta/code-architecture.md` — what implementation components exist, what does each
      own, how do dependencies flow, and which cross-component contracts must hold?
    * `content/meta/content-model.md` — what kinds of knowledge exist, and how do notes, metadata,
      tags, references, links, and companions represent them?
    * `content/meta/build-and-validation.md` — how does authored source become checked, generated,
      cast (if implemented), rendered, and kept current?
    * `content/meta/repository-layout.md` — where do authored, generated, vendored, external, and
      temporary files live, and who owns each lifecycle?
  The CONTENT is instance-owned: one Foundry may have a package graph and caster while a younger
  one has one Astro application and an explicit casting gap. A record that keeps resisting its own
  boundary is telling you something about the taxonomy, not only about the record.
  The full map — every record's scope and exclusions, the change that obliges an edit to each, and
  the reverse index from a change to the record that owns it — is the pattern's Design Records page
  (`content/pattern/design-records.md`). Route a change through that index instead of opening every
  record to find where a sentence goes.

- GIVE EACH ARCHITECTURE RECORD AN EXCLUSION RULE. `code-architecture.md` does not enumerate note
  kinds or reproduce the file tree. `content-model.md` does not explain package dependencies.
  `build-and-validation.md` owns flows and gates, not every module. `repository-layout.md` owns
  placement, not conceptual rationale. `architecture.md` summarizes and links rather than
  restating all four. Domain concepts keep their more specific homes — glossary, positioning,
  guiding principles, Mold contract, casting semantics, corpus, or the domain's external-check
  record. When a focused record grows hard to scan, split the concern it is mixing; do not repair
  it by growing the map.

- BIND THE CONTRACT TO YOUR CONTEXT ONCE, in types/context.ts, and your kind files never mention
  the generic again:
      export type KindDefinition<T extends KindShape = KindShape> =
        LibKindDefinition<KindContext, T>;
      export const defineKind = kindDefiner<KindContext>();
  `kindDefiner` is curried because `defineKind` has to stay generic in the kind's SHAPE while
  already fixed to your CONTEXT, and TypeScript gives no way to bind one type parameter of a
  generic function and leave the other free at the call site. This is the whole adoption cost.
- A kind directory exports a KIND DEFINITION, not a bare schema. The contract is `build` plus
  `refine` over the FRONTMATTER, and `shape` plus `companions` over the LAYOUT — and nothing else:
      defineKind({
        kind: 'mold', title: 'Mold', layer: 'substrate' | 'instance', summary: '…',
        shape: 'directory',                        // or 'file'. Required of every kind.
        companions: [                              // [] means none, and means it as an assertion
          { file: 'eval.md', requirement: 'recommended', disposition: 'foundry-only',
            purpose: 'Fixture-independent property checks for this Mold.' },
        ],
        additionalCompanions?: 'forbid' | 'allow', // 'forbid' is the default
        build:   (ctx) => z.object({ type: z.literal('mold'), ...ctx.base, … }).strict(),
        refine?: (data, issues, ctx) => void,
      })
  Two properties of the installed type are worth understanding before you are tempted to work
  around them. The shape is BOUND to carry `type` (`{ type: z.ZodTypeAny } & z.ZodRawShape`):
  every kind you write would declare `type: z.literal(...)` anyway, but until the bound says so
  that is a convention some directories happen to follow rather than something a consumer can
  rely on — and it is what lets the kinds compose into a `z.discriminatedUnion`. And there is
  deliberately NO third slot for assembling an entry from anything but its own frontmatter (see
  the no-`transform` rule below). The package holds both of those with its own type-level tests,
  so you do not owe a `.test-d.ts` for the bound. What you DO still owe is a runtime assertion per
  kind: the shape declares `type`, and the literal parses to THAT kind's name rather than merely
  being present. Nothing in the type system checks that a kind named `mold` says `mold`.
  `kind` is the `type:` discriminator value — the sole note-kind discriminator, matching the
  directory name. `layer` records whether the kind is inherited substrate or something this
  domain added; it is what a cross-instance kind catalog groups by, so state it honestly.
  It is `layer` and NOT `origin` so manifest metadata stays disjoint from frontmatter vocabulary;
  a kind may legitimately use `origin` as one of its own domain fields.
- DECLARE THE LAYOUT AT THE KIND, NOT AT THE NOTE. `shape: 'directory'` means the note IS
  `index.md` and everything else in that directory is either a companion or another kind's note.
  Both fields are REQUIRED of every kind rather than opt-in: no kind legitimately does not know
  its own shape, and optional declarations leave holes in the cross-instance catalog. A kind-level
  declaration can say that a companion is required, recommended or forbidden and makes the same
  fact available to validation and catalogs. One walk handing `checkCompanions` a directory
  listing replaces per-kind allowlists and required-file constants; remove those older mechanisms
  when adopting the declaration.
- `companions: []` MEANS NONE, and an absent key is not an option. "Not modelled" versus "none,
  enforced" is a distinction invisible to a reader and unrenderable in a catalog without
  misreporting one of the two, so the open-set half is said out loud instead:
  `additionalCompanions: 'allow'` for a kind whose companion set is genuinely open — vendored
  sidecars beside a research note, the acquisition files beside a book. It is NOT "unmodelled": a
  kind may declare what it knows AND permit the rest. The default `'forbid'` is what makes a
  misnamed `scenario.md` an error rather than a file the walker silently drops.
- A SIBLING THAT IS ITSELF A NOTE IS NEVER A COMPANION, and nothing can infer that from a
  filename. `content/cli/<tool>/` holds an `index.md` that is a `cli-tool` and sibling `.md` files
  that are each a `cli-command`, so `cli-tool` declares `companions: []` and still has a directory
  full of markdown. Infer from the extension instead and every CLI command in the corpus reports
  as a stray. `checkCompanions` therefore takes the note-or-not answer as INPUT rather than
  deciding it; `kindOf(table, path)` from `./collections` is what answers, which is the second job
  the one collection table of Part 2 is doing.
- A companion `file` is a LITERAL NAME in ONE directory — no globs, no path separators. A trailing
  slash names a directory and is satisfied by that directory EXISTING; what is inside it is that
  directory's business. Prefer a fixed layout over a wildcard escape hatch, and test directory-
  shaped companions as well as files.
  `disposition` is ONE axis — whether the file reaches a skill artifact. `foundry-only` never
  leaves (eval, scenarios). `cast-input` means the caster READS it and it does not appear in the
  output (rendering metadata or a placement manifest). `bundled` is copied in (a vendored schema).
  The caster reads the referenced note's Kind layout and carries fixed `bundled` companions
  automatically. Derive target exclusions from the same declarations rather than restating names
  in target config. A file's presence in the directory does not make it eligible; the declaration
  does.
- COMPANIONS DESCRIBE LAYOUT, NOT DEPENDENCIES. If a companion declaration starts growing
  `used_at` / `load` / `mode`, it has turned into a second reference contract, and the answer is a
  `references:` entry instead. Worth watching precisely because both sides can be described as
  "a file this note needs" — the difference is that a companion says where a file SITS and a
  reference says what a note DRAWS ON.
- FIXED COMPANION MEMBERSHIP BELONGS TO THE KIND. A referenced directory note carries exactly the
  fixed companions whose disposition is `bundled`; `foundry-only` and `cast-input` cannot be
  reclassified by note metadata. This is not inference from the directory listing: the Kind names
  each eligible file, and validation separately checks whether the directory satisfies that
  declaration. Missing required companions remain refs so the cast reports the missing source;
  absent recommended or optional companions are skipped.
  An open Kind (`additionalCompanions: 'allow'`) may let a note enumerate additional bundle members
  in a per-note `companions:` field. Those are additions only. Reject absolute paths and `..`, and
  recurse declared directories into one provenance entry per file. A closed Kind accepts no
  per-note additions. Do not add a second `companions` permission to `reference_contract.yml`.
- A LAYOUT DECLARATION SAYS A FILE IS THERE, NEVER THAT IT SAYS ANYTHING. Whether `eval.md` exists
  beside a Mold is the declaration's business, and `recommended` is what produces the warning.
  Whether it contains a single property check is a rule about CONTENTS, which no layout vocabulary
  should learn to express — keep those in the validator. Draw this line when you adopt the
  declaration, or the checks it should have deleted stay, each now duplicating a warning the
  declaration already emits.
- `build` returns the BARE OBJECT; `refine` carries the cross-field rules over the fields already
  present. What that protects is the manifest generator's walk of `.shape` — and note that zod 4
  changed who ENFORCES it. Under zod 3 a refined object became a ZodEffects: no `.shape`, and not
  a legal discriminated-union member, so the separation was compulsory. Under zod 4 `.refine()`
  leaves the type alone and hangs a check off it — a refined object is still a ZodObject, still
  has `.shape`, still assembles into a union. The rule is now the CONTRACT's to hold, not the
  library's, which is exactly when a rule quietly stops being obeyed. Keep it: `.transform()` is
  the one operation that still wraps (as a `pipe`, whose `in` is the source type and which has no
  `.shape`), so the hazard did not go away — it narrowed to the slot the next bullet says not to
  add. A rule that has lost its enforcement mechanism and kept its reason is worth stating twice.
- DO NOT ADD A `transform` SLOT that assembles an entry from anything beyond its own frontmatter.
  It makes the note incomplete in isolation, invites schema-time filesystem reads, makes the kind
  manifest describe fewer fields than the runtime entry carries, and wraps the Zod object in a
  value with no `.shape`.
  The fix is to MATERIALIZE instead: a generator copies the assembled fields into each note's
  frontmatter, a `--check` mode regenerates and compares in CI, and the note then validates in
  place like every other one. Same trade kinds.generated.json already makes. Verify the generator
  is idempotent (a second run writes zero files) and that `--check` is non-vacuous (edit one
  source field, confirm it names exactly the affected notes and leaves the rest alone).
  `KindDefinition` therefore exposes `build` + `refine`, preserving the discriminated union and
  keeping the contract enforceable by the dependency.
- TYPE `refine`'S `data` AS THE KIND'S OWN INFERRED FRONTMATTER. Not `Record<string, unknown>`,
  not `any`. This is not a style preference — an untyped `data` makes a validator rule SILENTLY
  DEAD, and there is no runtime signal. Every refine rule is conditional: it fires only when
  another field takes a particular value, so a rule that never fires is indistinguishable from a
  rule with nothing to complain about. On a real corpus:

      if (d.axis === "source-specifc" && !d.source)     // typo — missing the 'i'

  compiled clean, exited 0, and sat there looking enforced while bad notes passed validation.
  Typed, the same line is `TS2367: This comparison appears to be unintentional … have no overlap`,
  which enumerates the four legal values. Guard it with a `.test-d.ts` putting a typo'd literal
  under `@ts-expect-error`, so widening the signature back fails the typecheck; verify it fails in
  both directions before believing it.
  If a discriminated union forces a cast somewhere, put it at the single DISPATCH site — where
  looking the member up by `d.type` already makes it correct — never in the callee's signature,
  which spreads the widening to every rule.
- `build` is a FACTORY taking the shared context, never a module-level constant, and the CONTEXT
  ITSELF takes the registries — `buildKindContext({ tags, contract, licensePolicy })`, not a
  module that loads YAML at import time. Both halves are needed and only the pair works. A context
  that loads its own registries can only ever exercise a kind against the REAL ones, so a test
  about tag validation has to add a tag to meta_tags.yml to run at all. Injected, you build a
  context declaring a tag that appears in no file in the repo and assert both directions: the
  schema ACCEPTS the synthetic tag, and REJECTS one the real registry declares. Check that pair is
  non-vacuous by replacing the registry lookup with `t.length > 0` — the reject assertion must
  fail, and pass again when restored. Neither assertion can hold while the injected registry is
  being ignored. Passing the context also makes the substrate visible: whatever every kind draws
  from is exactly what belongs in it.
- types/context.ts holds THE BASE ENVELOPE and the field primitives — the `tags` array (min(1)),
  the registry-checked `tag`, `licenseId`, the license-coherence refinement, the typed `reference`
  object, `wikiLink`. Each kind spreads `...ctx.base` and reaches into `ctx` for the rest. This is
  the shared-substrate/instance-extension split applied to the frontmatter contract: context.ts is
  what every Foundry's kinds have in common, the kind directories are what yours adds.
  Port the envelope HONESTLY. A field you cannot populate truthfully (a `created` date you would
  have to stamp with today across a corpus you did not author today, an `ai_generated` flag you do
  not actually track) is worse than an absent one — it manufactures provenance. Leave it out and
  let the kind catalog show the gap.
- types/index.ts is a STATIC barrel — `import { kind as mold } from './mold/schema.js'`, one line
  per kind, collected into `export const KINDS = [mold, pattern, …] as const`. Do NOT glob at
  runtime: the contract must load identically under tsc-to-dist, under vitest, and inside Astro,
  and only static imports do all three. Keep it honest with a drift test (Part 4) rather than with
  cleverness.
  `as const` is not tidiness — it is load-bearing, and this is the one place a new instance is
  most likely to lose the inference without any error to show for it. Assembly comes from the
  package in two forms and you may need only one: `assemble(definition, ctx)` gives ONE kind's
  schema, which is what a per-collection loader wants; `buildKindUnion(KINDS, ctx)` gives every
  kind in one schema dispatching on `type`, for a validator walking a mixed corpus that cannot
  know a note's kind before reading it. `buildKindUnion` is generic over the kind LIST, so it
  recovers each member's type from the tuple you hand it. Hand it a widened
  `readonly AnyKindDefinition<Ctx>[]` instead and validation still behaves identically while the
  output type degrades to `any` — every field access compiling and yielding nothing.
- DEFEND THE INFERENCE. Splitting a schema across files introduces annotations that can widen an
  inferred kind to its base shape or `any`, with the symptom appearing later in collection pages.
  Three concrete rules:
    * Do not annotate a kind `: KindDefinition` — that erases which fields it declares. Pass it
      through a `defineKind` identity helper generic over its shape, so the shape is INFERRED.
    * Do not type a shared field group as `ZodRawShape` (or a kind list as `KindDefinition[]`).
      An index signature erases every field a kind spreads in. Use `typeof theActualConst`.
    * Do not build the kinds map by MAPPING over the kind list. `.map` produces a homogeneous
      array and collapses every kind to the widest common type; write the entries out by name,
      and keep the LIST a tuple (`as const`) for the same reason.
  Run the SITE's type check, not just the schema's — and know that A CLEAN TYPE CHECK DOES NOT
  PROVE THE INFERENCE SURVIVED. A widening to `any` can satisfy every field access and produce no
  typecheck error at all. A site that already casts
  `entry.data as any` anywhere cannot catch it either. Zero errors is what both success and total
  erasure look like.
  So do not hold this with a typecheck, and do not hold it with a probe you delete. Diagnose with
  the probe — access a field that does not exist and read the error: it must name the fields of
  EVERY kind, the full union enumerated, and a real field must come back with its actual type
  rather than `any` — then COMMIT the check in a `.test-d.ts` that asserts the same thing
  permanently: the discriminator is assignable, narrowing on `type` reaches an arm-specific field,
  a field NO kind declares fails under `@ts-expect-error`, and an arm-specific field fails
  UNNARROWED. Re-verify by deliberately re-widening the kind list and confirming the file goes red.
- ONE COLLECTION TABLE, and everything that needs to know where a note lives reads it. A row is
  `{ base, pattern, kind }` — typed `as const satisfies Record<string, CollectionRoute>` against
  `@galaxy-foundry/kind-schema/collections`, with your schema hung off the same row:
      export const COLLECTIONS = {
        molds:          { base: 'content/molds', pattern: ['**/index.md'], kind: 'mold',        … },
        'cli-tools':    { base: 'content/cli',   pattern: ['*/index.md'],  kind: 'cli-tool',    … },
        'cli-commands': { base: 'content/cli',   pattern: ['*/*.md', '!*/index.md'],
                                                                          kind: 'cli-command', … },
      } as const satisfies Record<string, CollectionRoute & { kind: NoteKind; schema: unknown }>;
  Collection and kind are NOT required to be one-to-one; a collection is a LOCATION (its own browse
  route), a kind is what a note IS. One directory may hold two kinds
  (`cli/<tool>/index.md` is a cli-tool while `cli/<tool>/<cmd>.md` is a cli-command — note the
  `!`-exclusion that keeps them disjoint), and multiple collections may resolve to one kind when
  separate browse routes are useful.
  Astro's site/src/content.config.ts then stays a thin, EXPLICIT consumer. Bind each table row to a
  named `defineCollection({ loader: glob(...), schema: row.schema })` export and spell those names
  out in `collections`. Do not map the table through `Object.fromEntries`: Astro preserves the
  precise frontmatter type of each collection only when the schema-bearing entries remain explicit;
  a generic loop widens them to their common shape and can turn the detail route's discriminated
  union into `any` without a typecheck failure.
    Every OTHER consumer — the validator's corpus walk, registry drift and wiki-link binding —
  reads the same rows through content-reader or kind-schema's `matchesCollection` /
  `collectionOf` / `kindOf`, rather than hand-rolling another selector.
  Two things to get right. `base` is matched as a PLAIN PREFIX of the paths you pass in, so the
  frame is yours to pick — repo-relative, project-relative, absolute — but it must be the SAME
  frame as the paths and the same frame for EVERY row, or a collection silently claims nothing.
  Pick the frame the table is naturally written in and give it a name (`contentPath(rel)`), so a
  caller cannot half-convert. And assert your table PARTITIONS the corpus: `collectionOf` returns
  the first match in table order, so two rows claiming one path is a table bug that order merely
  hides — `collectionsClaiming` exists to write that test with.
- Encode cross-field rules as zod `.superRefine(...)` raising a specific, path-anchored issue,
  never as prose in a README, and never a second time in the validator. Real examples: an on-demand
  reference requires a `trigger`; a hypothesis-evidence reference requires a `verification`; a note
  may not declare verbatim carry under an own-words-only license. If a rule matters, the schema
  enforces it — and enforces it ONCE. A rule over the fields of one note belongs in the schema; only
  a rule needing OTHER notes (wiki-link resolution, reference-target types) belongs in
  the validator.
- Strip a trailing /index in the loader's generateId (a `stripIndex` helper) so entry ids and
  wiki-link basenames stay clean (`msmb/chap1`, not `msmb/chap1/index`).
- Links are one-directional and backlinks are DERIVED — compute incoming references from the
  same link fields and render them. Do not check that a link is reciprocated: it asks authors to
  hand-maintain what the renderer already produces, and it complains to the wrong note, since a
  hub collects one complaint per page citing it.

Emit a KIND MANIFEST — types/kinds.generated.json, committed, regenerated by a script with a
`--check` mode CI runs:

    { "instance": "<slug>", "version": 1,
      "source": { "repo": "<owner>/<name>", "path": "<path to this file>" },
      "kinds": [ { "kind": "mold", "title": "Mold", "layer": "substrate",
                   "summary": "…", "doc": "<kind.md body>", "example": "<example.md body>",
                   "shape": "directory", "locations": ["molds", "research/experiments"],
                   "companions": [ { "file": "eval.md", "requirement": "recommended",
                                     "purpose": "…", "disposition": "foundry-only" }, … ],
                   "fields": [ { "name": "tags", "required": true, "type": "string[]" }, … ] } ] }

The FORMAT, the reader, and the zod-shape deriver come from @galaxy-foundry/kind-manifest — do not
re-encode them. The BRIDGE between the two comes from kind-schema:

    buildKindManifest({
      instance: '<slug>',
      source: MANIFEST_SOURCE,                                  // yours: repo + path
      kinds: manifestKinds(KINDS, ctx, {
        docs:        loadKindDocs(KINDS, 'src/types'),
        collections: COLLECTIONS,        // `locations` are DERIVED from it, never hand-listed
        examples:    <yours>,            // no loader ships for this one — see below
      }),
    });

The third argument is an OPTIONS OBJECT, not a positional docs record. Passing `loadKindDocs(…)`
bare yields a manifest with every `doc` missing because the object has no `docs` key.

`examples` is the one extra with no loader behind it: `loadKindDocs` is fixed to `kind.md`, so a
`kind name -> example.md body` record is yours to read — the same walk your kind-directory test
already does to prove each example parses. A catalog should treat an absent `example` as unknown,
not as an assertion that the kind has none.

`locations` is the collection bases routing to this kind, derived from the table rather than
declared, for the same reason the field list is derived from the zod shape that validates: a
hand-written location list is a second encoding of the routing table, and it drifts. Deriving also
gets the many-to-one case right for free — two collections resolving to one kind yield two
locations.

`manifestKinds` lives in kind-schema rather than kind-manifest because kind-manifest describes a
kind it is HANDED and must not learn what a `KindDefinition` is — its reader half has a consumer
that only ever reads manifests other Foundries produced and never defines a kind. kind-schema knows
both sides.

`loadKindDocs` (from `@galaxy-foundry/kind-schema/docs`) reads `<typesDir>/<kind>/kind.md` for every
kind in the list and TRIMS each body — trimmed because your `--check` gate byte-compares the
manifest, and a trailing newline that varies by editor would fail it on whitespace. It is driven by
the kind LIST rather than by a directory listing, so a kind with no `kind.md` is an error naming
itself and an unrelated directory under `types/` is not mistaken for a kind. It THROWS rather than
exiting: whether that means exit 1 is your command's call. This is a separate entry point because
it is the only part of kind-schema that touches a filesystem — everything else imports nothing from
`node:`, which is what lets your site pull `KindDefinition` into browser code without dragging `fs`
in behind it.

What is left for you to write is the composition: which kinds exist, and how your registries resolve
into the context they are built against. Keep your own `buildKindManifest({ instance, docs,
...registries })` signature over the package calls, so nothing downstream cares that the derivation
moved.

`fields` is DERIVED from the zod shape — walked, with optionality read off the schema — never
hand-written. A hand-written required-metadata list is a second encoding and it drifts the first
week. That derivation has just moved one repo over; the rule has not changed.

The manifest is what a cross-instance kind catalog renders, and what lets any number of Foundries be diffed
by machine instead of by eye. Because its consumer is ANOTHER REPOSITORY, `source` is split by who
actually knows each fact: YOU declare `repo` and `path`, so a consumer vendoring a copy does not
hard-code your identity on your behalf; WHOEVER TAKES A SNAPSHOT records the `revision`, via the
package's withRevision(). Do not put `revision` in your own generator — a committed artifact whose
gate regenerates it and string-compares can never carry the commit it was generated at, so
`--check` would fail on every commit.

The manifest is also what makes the no-`transform` rule above concrete: it lists the fields the
SCHEMA declares, so a kind assembled from anything other than its own frontmatter is described
here as fewer fields than it carries, and a reader of the cross-instance catalog is comparing a
partial row against a complete one without being told. Materialize; then the manifest is honest
for free.


PART 4 — STAND UP THE REGISTRIES, THE VALIDATOR, AND CI

Registries — TWO YAML files at the REPO ROOT, each holding only the half that is yours. The
formats, the loaders and the inherited vocabularies are installed (see "The shared substrate"):

- license-policy.yml — THERE IS NO SUCH FILE. The table ships in @galaxy-foundry/license-policy;
  read it with bundledPolicy(). SPDX id → { name, policy: verbatim-ok | own-words-only,
  license_file, copyleft, obligations }; unknown/missing → deny-by-default row with
  `defect: true`. Apply the row to third-party pass-through or quoted expression;
  `declaresVerbatimCarry(note.derived)` makes Foundry-authored own-words prose explicitly out of
  scope. No row names a casting mode. What stays yours is license COHERENCE, which lives in your
  validator: how the note's derived posture, declared license, optional license file and any
  vendored source must agree.
- reference_contract.yml — holds `kinds` ONLY, and nothing else: the reference kinds YOUR Molds
  author. used_at/load/modes/evidence come from @galaxy-foundry/reference-contract, and the loader
  refuses a file that re-declares an inherited block (and refuses a shared table that declares
  `kinds`) — the boundary is enforced in both directions, because it is the kind of split that
  otherwise decays quietly. Compose the two halves in src/lib/reference-contract.ts. Your kinds
  keep their own `href` into your spec docs; the inherited four link to the Foundry Pattern, which
  is where they are specified. Once the instance adopts @galaxy-foundry/cast, a kind may also carry
  the delegated `cast:` block Part 7 defines. Load it through `loadCastReferenceContract`; a
  reader-only composition deliberately refuses that field because no installed capability acts on
  it.
- meta_tags.yml — the controlled tag vocabulary, grouped into FACETS: `version: 1` plus
  `facets: <facet>: { label, description, values: { <tag>: <gloss> } }` (`values` may be omitted
  while a facet is still empty). Parse it with @galaxy-foundry/tag-registry — loadTagRegistry()
  and the isValidTag/facetOf/facets accessors — never `yaml.load(...) as TagRegistryFile`. The
  parser refuses a missing `facets` block, a facet without label/description, a tag with no gloss,
  and a tag declared by more than one facet.
  The three rules the format rests on, which the package enforces so you do not have to:
    * A tag is valid when it is an exact key under some facet's `values`. Nothing else validates.
    * EVERY facet is a CLOSED enum — no open/free-form/prefix-wildcard escape hatch, ever. A tag
      with no gloss is a tag the browse surface cannot document and a reader cannot learn from, so
      the registry is the complete, permanent catalog of what the corpus can carry. If an external
      vocabulary changes upstream, enumerate the slice you actually use and re-run the drift check.
    * Membership is DECLARED, not parsed off the `/` prefix — so the slash is a naming convention,
      a bare key is an ordinary value (use one when a facet has exactly one flag-shaped member),
      and browse pages group by the DECLARING facet, which makes an "other" bucket impossible
      rather than merely empty.
  What stays yours: the FACET VOCABULARY, and the schema rules over it — tags are cross-cutting
  facets ONLY (note-kind is the `type:` discriminator and is never copied into `tags:`), and
  `tags` min(1) so every note carries at least one facet. The package ships no vocabulary and
  decides nothing about what a tag MEANS for a note. Seed corpus-first from the tags your first
  notes actually use.
  If a Foundry designates one note as the landing note for each value of a facet, declare that
  relationship in frontmatter rather than infer it from the note's filename. The note schema checks
  that the value belongs to the intended facet and that the note carries the tag it claims to lead.
  The whole-corpus drift check then holds the relationship in both directions: every declared value
  has exactly one landing note, and no value has two. Ordinary tag coverage is not enough — another
  kind may carry the tag while its landing note is absent. Render the landing note distinctly on
  the tag page, and derive a built-page assertion from the declared relationships so “landing note”
  remains a reader-visible role rather than only metadata.
- LICENSES/  — vendored verbatim SPDX texts the policy's `license_file` rows point at, plus a
  README. The table travels with the dependency; the texts a cast hashes are in YOUR tree.

Validator — vitest, one command, running the SAME schema the build uses:

- package.json scripts: "test": "vitest run" and alias "validate": "vitest run".
- site/tests/corpus-conformance.test.ts — walk the corpus THROUGH THE COLLECTION TABLE (Part 3),
  parsing each note against its row's schema. The second consumer that makes "one schema, two
  consumers" real. Walk it with the table's own matcher rather than a local `**/index.md` glob:
  a walk that selects differently from the loader is a set of notes the site validates and the
  validator does not, or the reverse, and neither surfaces as a failure.
- The same walk, one level up: CHECK THE LAYOUT, not only the frontmatter. For every directory
  note, hand `checkCompanions` the listing you already have plus that note's kind, and act on the
  three results it returns — `missingRequired` is an error, `unknown` is an error unless the kind
  declared `additionalCompanions: 'allow'`, and `missingRecommended` is a WARNING. That middle
  level is the one a test cannot carry: a test has pass and fail and nothing between, so
  "expected, but its absence is not a defect" needs a validator with severities or it is not
  asserted at all. Whichever you have, SAY SO — an instance that quietly demotes `recommended` to
  `optional` because its runner has two outcomes has edited the spec to fit the tooling.
  Two wiring notes. Tell it which sibling entries are NOTES; it will not guess, and the reason is
  in Part 3. And drive the walk from the collection table, so the directories checked are exactly
  the ones the loader routes — same single-walk rule as the conformance test above, same reason.
  This check REPLACES per-kind allowlists; it does not sit beside them. Two carve-outs remain, and both are
  about a file's CONTENTS rather than its presence: a rule that one companion's frontmatter uses a
  fixed decision vocabulary, and the eval/scenario body checks. Those are not layout, and moving
  them into a layout declaration is how that declaration starts growing a second reference
  contract. And exclude dotfiles from the listing before you hand it over: editor and OS state is
  not something a kind failed to declare, and it is the one exclusion the declaration cannot state
  for itself.
- site/tests/frontmatter-schema.test.ts — a negative-fixture table: each broken frontmatter
  asserts the SPECIFIC issue path+message it must raise, with a positive baseline per type. A
  rule with no negative fixture is a rule you are only hoping is enforced.
- site/tests/kind-directories.test.ts — the types/ layout must hold, or the isolation it buys is
  imaginary. Assert, per kind: the barrel enumerates EXACTLY the directories on disk (both
  directions — a directory the barrel forgot is a kind that silently does not exist, a barrel line
  with no directory is a stale import); `kind` equals the directory name; kind.md and example.md
  both exist; and example.md's frontmatter PARSES AGAINST THAT KIND'S OWN SCHEMA. That last one is
  what keeps the documentation executable — an example that stopped validating is a kind whose docs
  now lie, and it is the cheapest positive baseline per kind you will ever get.
  (A missing kind.md is already fatal to manifest generation, since `loadKindDocs` is driven by the
  kind list and names the kind it cannot read. Keeping the assertion here anyway costs one line and
  fails in the suite rather than in a generator, but example.md and the two directions of the barrel
  are what only this test holds.)
  Assert the walk found kinds at all, so a path change cannot turn the whole check vacuous.
  Assert also that every kind SPREADS THE BASE ENVELOPE. A kind that spreads only a field
  GROUP (the shared source-note fields, say) and skips `...ctx.base` passes every other check
  and silently misses the next field added to the envelope.
- site/tests/*.test-d.ts — the TYPE-LEVEL guards from Part 3. Two, now that the shape bound is the
  package's to hold: the assembled UNION (discriminator assignable, narrowing on `type` reaching an
  arm-specific field, a field no kind declares failing, an arm-specific field failing unnarrowed),
  and a typo'd literal in a `refine`. Each negative case under `@ts-expect-error`. These prove rules
  that have no runtime behaviour to assert, so nothing else can hold them — and as Part 3 says, the
  site typecheck provably does not. Three things to get right: vitest does NOT collect `.test-d.ts`,
  so they run under `pnpm run typecheck` and are worth nothing if your tsconfig excludes them —
  confirm the typecheck actually fails when you delete a directive; put the directive where
  TypeScript reports the error, which for a kind definition is on the `build:` property rather than
  above the `defineKind(` call; and verify non-vacuity by the widening itself, not by deleting a
  line — re-widen the kind list to `AnyKindDefinition[]` and confirm the file goes red.
- site/tests/kind-manifest.test.ts — an INTEGRATION test, not a unit suite. The synthetic-shape
  cases (does the deriver unwrap a defaulted array, is a refined string still `string`) live in
  @galaxy-foundry/kind-manifest with the renderer they exercise; re-testing a dependency's unit
  behaviour here would be the duplication the package removed, in a new place. What is only
  testable HERE: your kinds against your registries, every kind's `type` field resolving to its
  own literal, byte-stability across two builds so the `--check` gate means something, and a
  round trip through the shared reader so a format break fails in this repo rather than in
  another one. The `--check` gate alone is never enough — it regenerates with the same code and
  string-compares, so a bug in the deriver produces a wrong manifest that `--check` then blesses
  forever, and the manifest is the one artifact whose consumer is ANOTHER REPOSITORY.
- site/tests/registry-drift.test.ts — the registries and the corpus must agree BOTH ways. The
  schema already rejects a note carrying an unregistered value; this asserts the other direction,
  that nothing you AUTHORED is carried by zero notes — every tag under a facet's `values`, every
  facet, every reference `kind` you trimmed to, every kind in the barrel. Assert the converse for
  kinds too, against the collection table: every kind must have somewhere to PUT notes. A kind no
  collection routes to is UNAUTHORABLE — its schema can never run, so "declared by zero notes" is
  the only thing that would ever notice, and it reports the symptom rather than the cause.
  Scope it to the per-instance vocabularies: the
  INHERITED ones (the installed license-policy table, and the contract's used_at/load/modes/
  evidence) are deliberately complete, so unused terms there are inheritance, not drift. The
  exception is a group you NARROWED — narrowing makes it authored, so it rejoins this check.
  Dead vocabulary is drift the schema's one-way membership check cannot report.
  For any facet with designated landing notes, compare the facet's complete declared value set with
  those notes' declared anchor fields and reject duplicates. Do not substitute the set of tags
  carried anywhere in the corpus; membership and ownership answer different questions.
  Assert here too that the raw meta_tags.yml declares no facet key the format does not define.
  An unrecognized `open: true` is a silent no-op and reading the accessor cannot catch it. Read the
  raw file for this assertion.
  Keep this a WHOLE-CORPUS test; do NOT fold it into a validator that runs over an arbitrary
  directory. "Registered but carried by nothing" is only meaningful against every note at once —
  point the same check at a small fixture and almost the entire registry looks unused. Also assert
  the walk found tags at all, so a frontmatter-reader change
  cannot turn the whole check into a vacuous pass.
- Parse frontmatter with js-yaml (yaml.load), so an unquoted `access_date: 2026-07-13` coerces
  to a Date exactly as Astro's loader does — the footgun surfaces in the validator, not in prod.

CI / deploy — two GitHub Actions workflows, working-directory: site:

- .github/workflows/ci.yml (on pull_request + push to main): actions/checkout@v6,
  actions/setup-node@v6 (node-version 24), `corepack enable`, then
  `pnpm install --frozen-lockfile` → `pnpm run typecheck` (astro check) → `pnpm test` (fails fast)
  → `pnpm run build`. Do NOT also pin the pnpm version in this file: corepack reads
  `packageManager` from package.json, so the manifest is the one place it is stated, and a second
  pin in CI is a copy that drifts silently the first time only one of them is bumped.
- .github/workflows/deploy.yml (on push to main): withastro/action@v6 (path: site) then
  actions/deploy-pages@v5; permissions: pages: write, id-token: write.


PART 5 — BRING THE DOMAIN KNOWLEDGE AND ITS GROUNDING CORPUS

The real asset is what the Foundry knows. Bring the sources before building machinery around them.

- One directory per source: content/research/<papers|tutorials|books>/<id>/ holding a regenerable
  faithful summary index.md and, by exception, an owned guidance.md listing the specific questions
  to pull from it. Summary = regenerable cast; guidance = what you maintain.
- Grade a note by RECOVERABILITY, not coverage: good enough only if the target skill could be
  rebuilt from the captured notes alone — no re-reading the source, no model memory. Hold the
  specific facts recovery needs: numbers, thresholds, exact procedure, named decision criteria,
  load-bearing quotes.
- Summary posture follows the LICENSE, not source type, and is recorded in the note's `derived:`
  field: own-words-only → paraphrase (keep only short functional strings verbatim as facts —
  error text, parameter names, thresholds, equations); verbatim-ok → short marked load-bearing
  quotes allowed.
- Generate summaries in a CLEAN context (a fresh agent that sees only the source + its guidance.md,
  never your analysis). Guidance directs attention, not conclusions ("does this source state X?
  quote it" — never "confirm X"). Carry [summarizer-inferred]/access/re-check flags forward verbatim.
- Synced multi-chapter source: scripts/sync-book.sh reads content/research/books/<id>/manifest.tsv
  (<num>TAB<url>), fetches into a gitignored raw/, pins SHA256SUMS. Redistribute NO source text —
  only manifest + pin (provenance) and your derived summary. Book-invariant metadata (license/
  attribution/derived) lives ONCE in a co-located book.yml and is COPIED INTO each chapter's
  frontmatter by a generator with a `--check` mode, never merged in at load — this is the
  no-`transform` rule of Part 3, and this is the exact kind that taught it. book.yml stays the only
  place to edit; the generated block is overwritten; `--check` regenerates and compares in CI. The
  chapter then validates from its own frontmatter like every other note, and its manifest row lists
  the ten fields it carries rather than the six it declared.


PART 6 — IDENTIFY THE ACTIONS AND AUTHOR THE MOLDS

Decompose the domain into repeatable decision-and-handoff units — one Mold per action worth casting.

- Directory-note convention: content/molds/<slug>/index.md bears the frontmatter (typed reference
  manifest + tags); siblings hold eval.md (fixture-independent property checks — the abstract
  oracle, never packaged into a cast), scenarios.md (a fixture binding + its expected verdict),
  and author-facing usage.md / refinement.md. Only index.md is validated.
- Declare each Mold's references[] as a typed manifest, each entry drawing kind / used_at / load /
  mode / evidence from reference_contract.yml. Mark evidence honestly: hypothesis (speculative —
  requires a verification), corpus-observed, or cast-validated.
- Get the boundary right: not one monolith, not a dust of fragments.


PART 7 — CAST ONE MOLD TO ONE TARGET

Adopt @galaxy-foundry/cast only after one real Mold and all of its references validate. The reader,
Kind catalog and reference view are a complete earlier vertical; do not install a caster merely to
make its configuration look anticipated.

- LOAD BOTH HALVES OF THE REFERENCE CONTRACT at the one composition point that already supplies
  the site schema. `loadCastReferenceContract` delegates the `cast:` block to the cast parser while
  returning the same reader-facing contract the site renders. A reference kind's block declares
  strategy, never companion membership or target placement:

      kinds:
        environment:
          label: Environment
          description: A runnable fixture and its declared bundled companions.
          ref_shape: wiki-link
          cast:
            resolve: note
            default_mode: verbatim

  `resolve` answers where the primary payload comes from (`note`, `package-export`, or
  `payload-companion`). `note_types` widens a reference kind that intentionally addresses several
  note types; do not rename the corpus to make the two vocabularies coincide. Omit `cast:` to make a
  reference kind reader-visible but uncastable. Before cast is installed, load the contract with
  `buildReferenceContract` instead; its refusal of an unknown `cast:` field prevents dead
  configuration from looking implemented.

- BUILD ONE CORPUS INDEX and project every cast input from it: the slug map, parsed frontmatter by
  path, and note targets. The map must apply the same aliases and collision precedence as prose and
  frontmatter links, and it must be driven by the collection table so companions never become note
  targets. Do not add a cast-only walk or slug policy. If the shared reader cannot expose a needed
  projection, improve that seam upstream rather than make two indexes agree by convention.

- PASS THE KIND TABLE AS `kindLayouts`. Casting consumes only `shape`, `companions`, and
  `additionalCompanions`, but it must consume the same declarations the validator and generated
  manifest use. A missing layout is a cast error. Do not translate the table into target excludes,
  note defaults, or a second list of companion filenames.

- DECLARE ONE TARGET in its own directory. The target owns bundle placement, document vocabulary,
  per-reference-kind destinations, extensions and supported modes; the reference kind owns source
  resolution. A minimal target is explicit about the runtime-facing document:

      bundle_path: 'skills/{mold}'
      document:
        path: SKILL.md
        noun: skill
      kinds:
        environment:
          dst_dir: references/environments/
          dst_extension: .md
          modes: [verbatim]

  Quote `{mold}` in YAML. Keep `_provenance.json` out of target vocabulary: it is the caster's
  runtime-neutral account, not a format-specific document. A target may constrain a kind's modes
  but cannot make an unsupported mode or uncastable kind valid.

- KEEP HOOKS DOMAIN-SHAPED. Supply the cast document's lede and sections, any implemented
  non-verbatim renderers, extra bundle files and bundle checks. Register `payloadCompanion` or
  `packageLoader` only when a reference kind selects those strategies. An absent implementation is
  an error; never fall back to packaging a plausible but different source.

- CALL `castMold` with the loaded Mold, composed reference/cast contracts, reference kinds, corpus
  index, Kind layouts, target and hooks. Treat `errors`, `drift` and `wrote` as values: aggregate
  findings in the caller, set the process exit there, and publish only when the complete staged
  bundle clears every check. A refused cast must leave the working tree untouched.

- HOLD THE FIRST BUNDLE AS AN EXECUTABLE CONTRACT. A write run produces the document, eligible
  references, eligible companions and provenance. A subsequent `--check` reports no drift and
  writes nothing. Exercise at least: an unresolved ref; a wrong note type; a missing Kind layout;
  a missing required companion; an absent recommended companion; a `foundry-only` companion; a
  bundled file and directory; a duplicate destination; stale owned output; and a license refusal.
  Assert provenance hashes the actual source and destination bytes. Run the inert check in CI and
  commit the bundle only when committed casts are part of the instance's distribution contract.


PART 8 — BUILD THE EXTERNAL CHECK

Decide what "correct" means in your domain and what can decide it. This is the sharpest, most
domain-specific part; the substrate deliberately leaves it open, so paths below are structural,
not a fixed tool.

- Prefer a deterministic decider where output is parseable (a validator/CLI that parses and
  checks). Where correctness is not mechanically decidable, construct an empirical check the field
  already trusts and make THAT the deliverable.
- Encode the check at the protocol altitude, not as a property buried on one Mold.
- If it must recognize bad work, ground it in a two-pole corpus: content/patterns/<slug>/index.md
  with `pole: established-good` (methods + when they apply) AND `pole: cautionary-bad` (named
  failure patterns + signatures + remedies). A checker that has only seen good examples cannot referee.
- Optionally keep standing scorecards for the doer skills (e.g. content/research/mold-eval/rubrics/):
  a few near-orthogonal -ability axes graded as a VECTOR, never a rolled-up composite. Which axis
  is low is the work-list.


PART 9 — COMPOSE, IF THE WORK IS SEQUENTIAL

Where a domain's task is an inherently multi-step journey, compose Molds into an ordered
end-to-end protocol and orchestrate them; a domain whose actions stand alone needs no such layer.
Keep composition optional and the individual Molds standalone.


INHERITED vs. SUPPLIED

Parts 2–4 are the substrate, and they now arrive in three grades rather than one:

  INSTALLED   The @galaxy-foundry capabilities this instance actually reaches. Not re-implemented,
              copied or mirrored — dependencies selected by capability. The reader-and-contract
              baseline is license policy, kind manifest/schema, the collection-backed content
              reader, the four inherited reference vocabularies, tag-registry format, wiki-link
              grammar and the reading shell/frame. Add cast when a real Mold is ready to compile;
              add audit-citations only when scholarly-citation replay belongs to the acceptance
              policy. Each keeps instance facts at one explicit composition point. The consuming
              package.json and lockfile record the selected versions; foundry-lib, not this
              checklist, owns compatible ranges.
  MECHANICAL  Re-stood-up rather than installed, because it is wiring rather than a format: the
              site/ Astro app, the one-schema-two-consumers arrangement, the types/ directory
              layout, the vitest validator, the ci.yml/deploy.yml workflows, the repo-root anchor
              and the test that enforces it.
  YOURS       The halves the packages deliberately decline. Which kinds exist and what each
              requires; your facet vocabulary; your reference `kinds`; your link aliases, routes
              and unresolved-link policy; your license COHERENCE rules; your base envelope, ported
              honestly; your site's identity and its palette; the domain furniture rendered
              inside the shared note frame; and, once casting exists, reference strategies,
              targets, hooks, corpus projections and the release threshold for cast findings.

Mechanical wiring is a package candidate when it exposes a stable, reusable contract. Duplication
is a prompt to investigate that contract, not sufficient evidence by itself. Reconcile genuine
differences before extraction so the package does not become the place where unresolved instance
decisions accumulate.

SURVEY BY THE QUESTION ANSWERED, NOT BY THE FILENAME. One responsibility may be split across
several modules or hidden behind different names. Ask which code answers a question — such as what
a note detail page looks like — and compare every answer. If an existing package already owns that
question, absorb local implementations at the instance's composition point. If no package owns it,
define the reusable contract before extracting code.

A PACKAGE SHOULD SHIP THE RULES IT CLAIMS TO OWN. If package documentation specifies behavior but
each consumer must implement it, the missing mechanism is an upstream package gap. Keep corpus
inputs local while moving stable interpretation and grouping behavior into the owning package.

PREFER STRUCTURAL FIXES TO CHECKS. One authoritative route or composition point removes whole
classes of drift; retain a small check that documents and protects that structure. For site URLs,
`shellBase` is the authoritative normalization rule. Each instance supplies one `lib/site-base.ts`
composition point beside `site-identity.ts`, and all pages and components import from it.

Part 1 and Parts 5–8 are the domain — but even the vocabulary starts from a shared spine: the
substrate terms in Part 1 are inherited from the pattern glossary and the stack terms from the
Astro-Stack Glossary. The terms beyond them, the corpus, the Molds, and the external check that
defines what "correct" means here are yours to supply. That last one is the part no other instance
can hand you.