Skip to content

split: cut the community layer over to its own repository - #2266

Merged
KodeStar merged 9 commits into
mainfrom
split/core-cutover
Aug 21, 2026
Merged

split: cut the community layer over to its own repository#2266
KodeStar merged 9 commits into
mainfrom
split/core-cutover

Conversation

@KodeStar

@KodeStar KodeStar commented Aug 21, 2026

Copy link
Copy Markdown
Owner

The core cutover of the community-repo split (.claude/community/PLAN.md,
migration order steps 4-5). data/works-community/ - 3,049 works, 154 packs, the
whole CC BY-SA layer - leaves this repository for
KodeStar/audiosilo-meta-community,
and the release build composes the two checkouts back into one artifact.

Community-side companion: KodeStar/audiosilo-meta-community#2 (the
notify-core.yml dispatch). Neither PR is merged; they should land together,
core first.


Divergence check (before deleting anything)

diff -r of this repo's data/works-community/ against a fresh shallow clone of
audiosilo-meta-community at 88ddce29eda9249dd8ef9a9c9c55bed3f4c69922:
no differences. The community repo is byte-identical to what is deleted here,
verified rather than assumed.

The equivalence proof (the cutover's acceptance test)

At the same content, two artifacts:

Build Command
(a) pre-cutover main, one tree metabuild -data <pre-cutover data/> --built-at 2026-08-21T00:00:00Z -o A.sqlite
(b) this branch's core tree + a clone of the community repo metabuild -data data --community <clone>/data --built-at 2026-08-21T00:00:00Z -o B.sqlite
76ce2cd5e725db2660b7d43263e8005816c2e4088ed7c63279d7d859189a7d23  A.sqlite
76ce2cd5e725db2660b7d43263e8005816c2e4088ed7c63279d7d859189a7d23  B.sqlite

Byte-identical. (APFS clone of the pre-cutover data/ in a scratch dir;
1.67GB each; 277,628 works / 291,553 recordings / 123,118 people / 45,172 series
/ 43,203 characters / 19,065 recaps on both sides, and identical advisory
censuses.)

--built-at is pinned because it is the one thing that legitimately differs
between two runs. Run unpinned, the first pair came out
b8bd980… vs aeb35b2… - and a table-by-table comparison found the difference
in exactly one row, meta.built_at (a wall-clock stamp), with every other
table (works, recordings, people, series, characters,
character_aliases, recaps, recap_summaries, chapters, work_genres,
work_isbns, work_authors, series_works, series_authors,
recording_asins, recording_isbns, recording_narrators, redirects, and all
five search_fts_* shadow tables) hashing equal. Worth recording, because it is
the shape a future re-proof will hit too.

What changed

1. The data

git rm -r data/works-community/ - 154 packs, ~634k lines. Nothing else in
data/ is touched.

2. Core runs as ProfileCore wherever it names a profile

  • check.yml: metacheck --profile core, metafmt --check --profile core.
  • intake.yml: all three metafmt --write / metacheck sites (the libex-fill
    step and both arms of the rebase sweep) pass --profile core.
  • release.yml: core-side validation is metacheck --profile core; the
    cross-tree rules run in the compose, which is the authoritative gate.
  • scripts/README.md and scripts/pack-union-merge.sh's usage recipes.
  • In code: internal/repair and internal/audit gain Options.Profile
    (see 5).

CLI defaults are unchanged - every --profile still defaults to all, and
that is deliberate: "a data root" with nothing said must keep meaning the whole
database, which is what the fixtures and most tests are. Changing a default to
fit one repository's shape would silently redefine every other caller's silence.
CI says core out loud instead.

The teeth are the file accounting, which was already total: under core a
works-community/ root stops being a family root, so every file under it is an
unrecognized location. A reappearing copy here - a bad merge, a stray revert, a
misdirected contribution - is a red check, not a family silently re-adopted.

3. release.yml composes the pair

  • A second actions/checkout of KodeStar/audiosilo-meta-community at main
    into community/. Public repo, so github.token suffices and no secret is
    involved
    ; shallow for the same reason the core checkout is.
  • metabuild -data data --community community/data -o meta.sqlite.
  • A new Record the composed SHAs step, and the release notes now name both
    SHAs - one sha no longer identifies an artifact.
  • A repository_dispatch trigger, types: [community-data], so a community-side
    merge can cut a release.

Unchanged on purpose: the asset contract (meta.sqlite.gz +
meta.sqlite.gz.sha256 + meta.sqlite.sha256 + the best-effort
meta.sqlite.patch.from-<PREV_TAG>.zst), the prev-release selection, the
data-release concurrency group, the tag scheme, and the signed metaserve
webhook. metaserve, the poller and the patch chain see nothing new.

Not added: pkg/check/** to the paths: filter. The filter names what
changes the artifact; the compose rules there change whether a release
passes, not what it contains. Flagging it in case you disagree.

4. Intake side of core

  • .github/ISSUE_TEMPLATE/add-characters.yml and add-recaps.yml deleted (they
    live in the community repo).
  • config.yml gains a contact link pointing a sidecar contributor there, so the
    forms do not simply vanish for someone who lands here.
  • intake.yml had no template-specific routing branches to remove - routing
    is entirely issueform.TemplateFromLabels, by design. Two stale comments that
    named add-characters are corrected.
  • The composer stays. internal/issueform/compose_sidecar.go, the fSidecar*
    constants and TemplateFromLabels' characters/recaps rows are all untouched:
    the community repo runs this cmd/metaissue under the community profile, so
    deleting the code would break the tree the forms moved to.
  • TestFieldLabelsExistInTemplates loses its two sidecar cases (it reads the
    template files, which are no longer here) with a comment saying where that
    drift guard now belongs. TestTemplateFromLabels still pins the
    characters/recaps routing, because the routing is still live.

5. Profile threading in code

internal/repair (Options.Profile, zero value ProfileAll so every existing
caller is byte-identical) is threaded through all three places that read or write
the tree - the store, the post-write internal/format pass, and the post-write
pkg/check validation - because a pass that healed under one profile and was
then judged under another would report a file it had just been told not to touch.

writeFamiliesIn narrows the write list to the families the profile holds. That
is not tidiness: pack.OpenForProfile refuses a named family the profile
disclaims, and under core, works-community is one. Nothing else about a wave
moves - no sidecar loads from a core tree, so the sidecar-moving arm of a merge
has nothing to move, and sidecar-member-collision simply stops occurring here.

internal/audit gets the same field for one reason: metarepair re-runs
metaaudit's detectors in process, so the two must be told the same thing
about the same root or a report is taken under one reading and applied under
another. Both CLIs gain --profile.

Deliberately not threaded: internal/importer (its writeFamilies never
included works-community) and internal/issueform (per-template family gating,
and its profile is the parallel community-intake task's business).

This threading found a real bug (third commit). Smoke-testing
metarepair --profile core over the real post-cutover tree, every merge died:

read works-community entry "1984": family "works-community" is not in the core
tree profile (this root holds people, series, works)

The merge planner asks every loser whether it carries a works-community member,
unconditionally - and pack.Store.Get refuses an out-of-profile family by
name, correctly, since a misdirected read or write must be loud. Narrowing the
write list was not enough: the read happens first. The plan's view now marks
such a family out-of-profile and reads empty - "this tree holds no such
family" and "this tree holds no such entry" are the same answer to that question,
and it is the answer a whole-database store already gives for a merely absent
family. Only the READ is gated; a put/del still reaches the store and is
still refused by name.

Pinned as parity, not as "it does not crash":
TestMergeWorksUnderTheCoreProfileMatchesTheDefault merges one fixture under
both profiles and requires the same report and the same tree - confirmed to fail
without the fix. The two profiles also agree over the real tree: both plan
5 applied / 0 refused / 6 tombstones on the same --op merge-works --limit 5
wave.

This is worth a reviewer's attention as evidence for the threading: the same
shape would have bitten the first post-cutover repair wave, and under all it is
invisible.

6. The site's sidecar hand-offs

site/src/lib/github-prefill.ts's addCharactersIssueUrl /
addRecapsIssueUrl built new-issue URLs against ISSUE_BASE - this
repository - naming templates that just moved out. Left alone, every "add
characters" / "add recaps" button on meta.audiosilo.app would land a contributor
on a chooser with no such form. They now build against a new
COMMUNITY_ISSUE_BASE; nothing else about them changes (same template names,
same work_ref, read by the same resolveWorkRef - which the community repo's
bot runs, since it runs this repo's cmd/metaissue). The existing host/path
assertion is the guard and now pins the community path. Not on the brief's list;
it is a link the cutover breaks.

7. Docs

CLAUDE.md (what-this-is, the gate section, the storage block, data/,
.github/, the metaaudit/metarepair/internal-repair entries, Phase 2),
PACK-SPEC.md (the layout block and the profiles table, which now says which
profile lives where), LICENSING.md (a new "the licence split is now a
REPOSITORY split" section - this repo holds no share-alike content, and the
artifact still mixes both, which is why the per-record license field stays
authoritative), GOVERNANCE.md (the intake form list, plus why the review bar
differs between the repositories), README.md, and CONTRIBUTING.md (which
described a four-family tree this repo no longer has; not on the brief's list but
it was the most contributor-visible staleness).

Verification

  • go build ./..., go vet ./..., go test ./..., go test -race ./...,
    golangci-lint run - all green. No test needed weakening: the two removed
    cases read deleted .github files, everything else is fixture-based and
    untouched. The real-data tests (pkg/check's TestRealDataTree,
    internal/repair's TestRealTreeDryRunComposesAPlan) pass over the
    post-deletion tree as they are.
  • go run ./cmd/metacheck --profile core -> green: ok: 277628 works, 123118 people, 45172 series (2532 advisory).
  • go run ./cmd/metafmt --check --profile core -> clean, exit 0.
  • Plain metacheck (ProfileAll) over the post-deletion tree is also green,
    with byte-identical output to the core run: pack.ListProfile tolerates a
    missing family root by design, so an absent family is an empty one. The census
    line differs from pre-cutover in exactly the sidecar class
    (369 mis-scaled sidecars -> 0), which is the layer being gone.
  • Site gate (yarn install --frozen-lockfile && yarn build && yarn run check && yarn test): build clean, astro check 0 errors / 0 warnings / 0 hints,
    408 tests passing.
  • actionlint clean on all workflows. I cannot run GitHub Actions, so
    release.yml's two-checkout compose and the repository_dispatch trigger are
    reviewed-only; the workflow diffs are kept minimal and commented for that
    reason. The first release after merge is the live test - and it is recoverable
    (a failed compose publishes nothing).

One-time maintainer setup (not done here - creating credentials is yours)

For the community-data dispatch to work:

  1. Create a fine-grained PAT scoped to KodeStar/audiosilo-meta with the
    Contents: read and write repository permission (what
    POST /repos/{owner}/{repo}/dispatches requires).
  2. Save it as the Actions secret CORE_DISPATCH_TOKEN in
    audiosilo-meta-community.

Until then the community workflow logs
no CORE_DISPATCH_TOKEN configured - core release will pick this up with the next core-side data/tooling change or a manual dispatch and exits 0; nothing is lost,
because release.yml always composes from the two mains. The setup is recorded
in release.yml's own trigger comment (tracked, where an operator looks) and in
the community PR.

Follow-ups (out of scope here)

  • Workspace docs (a different repo): ~/dev/audiosilo/CLAUDE.md's repo table
    needs a seventh row for audiosilo-meta-community, and CROSS-REPO.md §17
    describes the community-metadata seam as a three-repo contract that is now
    four.
  • AUTHORING.md / EXTRACTION.md / EXTRACTION-AUDIO.md are still here
    and already in the community repo. The plan moves them; the brief for this PR
    enumerated the docs to touch and did not include them, so they are duplicated
    until a follow-up deletes core's copies (metaextract itself stays here).
  • .claude/community/PLAN.md (untracked, and outside this worktree, so I
    could not edit it): steps 4-5 are what this PR is; the PAT setup above wants
    recording there.
  • PLAN.md's own tooling follow-up is unaffected: core is still unfetchable as
    a Go module, and the community repo still pins a SHA and sparse-checks out the
    tooling dirs.

Review round (2026-08-21)

The high-effort review's findings, applied. Its top finding - "no working sidecar
path anywhere" - is resolved by the parallel PR #2265 (issueform/metaissue
--profile, artifact-backed key verification, the community repo's intake), not
here: the sequencing is #2265 merges first, this rebases onto it. Items 3 and
4 of the review's list are staged for after that rebase; 4 landed now because
metabuild --community is already on main.

1. Release tag collision (fixed)

The tag was date + core short sha, which stopped being unique the moment a
community merge could trigger a release: a community-data dispatch on a day that
already released leaves core's HEAD unmoved, so the second run composes a
genuinely different artifact under an existing tag and gh release create fails -
and two community merges in one day fail the same way.

  • The tag now carries both short SHAs:
    data-vYYYY.MM.DD-<core7>-<community7>.
  • Consumers are unaffected, and the workflow says so. Nothing reads the tag's
    shape: latestDataRelease and this workflow's own prev-release lookup both
    select by asset presence at the maximum published_at. Older
    data-vYYYY.MM.DD-<sha> releases stay valid and selectable.
  • Build identity is computed up front and every subsequent step is guarded on
    it, so an existing tag - same day, same pair, genuinely nothing new - skips
    with a ::notice:: and exits 0
    for the cost of one API call, rather than
    paying a full compose to fail at the end.

2. The sidecar-collision refusal was structurally blind (fixed - the big one)

The read-empty fix in the previous round made merges plan again, and in doing so
made the collision guard answer "no sidecars" for every cluster.

Measured over the real pair: the first 30 non-advisory merge proposals contain
25 sidecar collisions.
A blind wave would have destroyed 25 CC BY-SA sidecars,
surfacing much later as a release-blocking LoadComposed error nothing connects to
the wave that caused it.

metarepair gains --community <dir>, and the guard now has three modes
(sidecarSource), never inferring safety:

Mode When Behaviour
sidecarInTree the root holds works-community pre-split: read and written, unchanged
sidecarReadOnly --community given the collision question is answered from there, refusal works exactly as pre-split; nothing is written to another repository
sidecarUnknown neither every merge-works proposal refused as community-data-required, naming the flag

Details that matter:

  • Under --community the members keep their retired keys and ride the slug
    tombstone
    to the survivor (the compose-time re-key) until the community re-key
    sweep lands them - and the applied record's notes say so, so the operator
    knows a follow-up exists.
  • Staging is identical in all three modes, deliberately: a read-only run still
    stages the merged entry, which is the only thing that catches two clusters
    folding two characters-carrying works onto one target. view.queue is the
    single place the modes part.
  • openCommunity refuses at the door both a --community root the tree already
    holds the family for, and one carrying no works-community packs - metabuild's
    own rule, because pointing the flag at the community repo's top level rather
    than its data/ would answer "no sidecars anywhere" for every cluster: the
    original blindness wearing the flag that was meant to end it.

Deviation - APPROVED by the coordinator. metarepair and metaaudit
default --profile to core, unlike metacheck/metafmt which stay all. Without this
the dangerous case is reachable through the default: under all over a core
tree the family is present-but-empty, which is sidecarInTree - blind again. The
axis is the cost of being wrong: metacheck/metafmt are read by CI with the flag
spelled out and a wrong reading is a red check; these are pointed at this tree by
a human typing a command and one of them deletes records. The library default
(Options.Profile zero value) stays ProfileAll, so every existing caller and
fixture is byte-identical. The two must also always agree with each other, since
metarepair re-runs metaaudit's detectors.

Verification

  • Real-tree parity: the same wave over the pre-cutover whole tree
    (--profile all) and over the split pair (core + --community) both plan
    5 applied / 25 refused / 5 tombstones, with the same 25
    sidecar-member-collision refusals. (One summary line differs: "op this pass
    does not apply" 21,363 vs 20,543 - the audit's REF-SIDECAR class, which only
    fires where sidecars are in the same tree. It is a class count, not a merge
    decision.)
  • Fixture parity: TestMergeWorksOverTheSplitPairMatchesTheWholeTree merges
    one cluster both ways and requires the same decision and the same database,
    read through check.LoadComposed - so the tombstone and the compose-time re-key
    are what make it pass, which is exactly how the failure would otherwise reach a
    release. Plus tests for the unknown-mode refusal, the collision through
    --community, and both openCommunity door refusals.
  • Every new refusal was confirmed to fail without its guard.

3. Core intake refuses-and-redirects sidecar submissions (landed)

Rebased onto main with #2265 merged - clean, no conflicts.

intake.yml now passes --profile core to metaissue. The two sidecar forms left
with the layer, but a data:characters / data:recaps label can still arrive

  • a maintainer applying one by hand, a bookmarked prefill URL, the sibling
    contributor tool - and routing is by label, not by which forms this repo ships.
    Under the default profile the composer would write a works-community entry into
    a family this root does not hold: a green-looking bot PR putting CC BY-SA content
    in the CC0 repo, permanently unmergeable since CI is core-profile too.

Nothing was missing on the Go side. #2265's templateBelongsElsewhere already
derives the rule from each template's writeFamilies (so it cannot drift from
what the composer would write), its message already names the community repo for
this direction, and TestCoreProfileSendsASidecarFormToTheCommunityRepo already
pins it. Both direction tests re-run and pass. The rebase sweep runs no metaissue

  • only metafmt and metacheck, both already core-profile.

Verified end to end against the real tree:

$ metaissue --labels '["data","data:recaps"]' --data data --profile core
{"status": "needs-human", "template": "recaps",
 "messages": ["the recaps form contributes to the CC BY-SA community layer, which
  this data root does not hold - open it on the community repository instead:
  https://github.com/KodeStar/audiosilo-meta-community/issues/new/choose"]}

The same run without the flag walks past the gate entirely (it stops later, on
an unrelated field) - which is the hazard this closes.

4. PR-time cross-tree signal (landed)

check.yml gains a compose job: shallow-clone the community repo, run
metabuild -data data --community community/data, fail on non-zero. It is the
pre-merge half of the release build - the cross-tree rules cannot be asked of one
root, so until now they ran only after merge, and a core PR that retired or
deleted a sidecar-keyed work went green and broke the release stream for everyone.
It should be required, not advisory.

5. Docs/prompt sweep (landed)

  • AUTHORING.md / EXTRACTION.md / EXTRACTION-AUDIO.md are now short pointer
    stubs at the community repo. Not copies - keeping the text in two places drifts,
    and core's own guide currently walks a contributor into adding a file under
    data/works-community/, which is the exact red-CI failure. The metaextract
    tool stays; the two Go doc comments citing AUTHORING.md now say where it is.
  • CONTRIBUTING.md + README.md gate blocks take --profile core with one
    sentence on why (CI runs those exact commands; ProfileAll is green precisely
    where CI is red).
  • Site: BuildTool.tsx's AUTHORING_URL and its "pull request instead"
    hand-off text, plus contribute.astro's authoring link, point at the community
    repo. The issue prefills beside them already did - the panel was telling a
    contributor to attach the file there and open a pull request here.
  • ai-verify.sh's prompt: three families in the storage-layout paragraph,
    every record must be CC0-1.0, and any data/works-community/ path or
    CC-BY-SA-4.0 record in a diff here is itself a finding
    . The sidecar length /
    spoiler / position rubric went to the community repo's reviewer, which sees
    diffs that can contain one; the two rules in that block that were never about
    sidecars are kept.
  • LICENSING.md's "moved with it" is now true.

6. release.yml nits (both fixed)

  • The composed SHAs are assigned before they are echoed. As
    echo "x=$(git ...)" the substitution's failure is discarded - echo's own status
    is what the shell sees and set -e never fires - so a broken community checkout
    would have published a release recording an empty community sha:
    unreproducible, and silently so.
  • The push paths filter gains pkg/check/** and pkg/pack/**, which now
    shape artifact content rather than only validating it (resolveSidecarKeys
    re-keys sidecars; the collision keeper decides what ships). The header comment
    carries the rationale: an extra release on a rule tweak costs a build; a
    wrong-keying fix sitting unreleased costs every consumer.

Also done

The community PR (#2) was accidentally branched on top of the intake task's
commit in a shared clone. Rebased onto that repo's main; it now carries only
notify-core.yml + its README paragraph.

Gate (this round)

go build / go vet / go test / go test -race / golangci-lint run - all
green. actionlint clean. metacheck --profile core green (277,628 works),
metafmt --check --profile core clean. Site: build, astro check 0/0/0, 413
tests. GitHub Actions still cannot be run here, so both workflow diffs remain
reviewed-only.


Rebase onto main (#2265 merged)

Clean, no conflicts - the two branches were disjoint, and even CLAUDE.md's
issueform-adjacent prose did not collide.

The equivalence proof was re-measured, not inherited. #2265 touched no
builder, compose or data path (cmd/metaissue, internal/issueform,
internal/importer; no go.mod/go.sum change), so the artifact should be
unmoved - and it is, rebuilt from the rebased branch with a freshly compiled
binary:

76ce2cd5e725db2660b7d43263e8005816c2e4088ed7c63279d7d859189a7d23  A-pinned.sqlite   (pre-cutover, one tree)
76ce2cd5e725db2660b7d43263e8005816c2e4088ed7c63279d7d859189a7d23  B-rebased.sqlite  (rebased core + --community)

Same hash as before the rebase. Gate re-run in full on the rebased branch:
go build / vet / test / test -race / golangci-lint green, actionlint
clean, metacheck --profile core green (277,628 works), metafmt --check --profile core clean, site build + astro check 0/0/0 + tests green.

@github-actions

Copy link
Copy Markdown

AI verification: passed

Claude reviewed the data changes and found nothing concerning. This is advisory; a maintainer still reviews before merge.

@github-actions github-actions Bot added the ai-verified Advisory AI verification passed label Aug 21, 2026
@github-actions

Copy link
Copy Markdown

AI verification: passed

Claude reviewed the data changes and found nothing concerning. This is advisory; a maintainer still reviews before merge.

@github-actions

Copy link
Copy Markdown

AI verification: flagged

Claude flagged the following for a maintainer to check (advisory - not a merge block):

  • Diff touches paths under data/works-community/ (deletes data/works-community/0/0.json and data/works-community/0/12-miles-below-ii-a-house-reborn.json, both carrying CC-BY-SA-4.0 records). Per repo policy this content family no longer belongs in this repository at all (it moved to KodeStar/audiosilo-meta-community) and any diff touching data/works-community/ paths here is itself a finding — CI refuses a tree with that path present in a core-profile PR diff, add or delete alike.

@github-actions github-actions Bot added ai-flagged Advisory AI verification raised findings and removed ai-verified Advisory AI verification passed labels Aug 21, 2026
The core half of the community-repo split. data/works-community/ - 3,049
works, 154 packs, the whole CC BY-SA layer - now lives in
KodeStar/audiosilo-meta-community, and this repository is the CC0 core:
works, people, series and redirects.json.

The layer is DELETED here, not copied: the community repo's tree was
verified byte-identical to this one's (diff -r against a fresh clone at
88ddce2, no differences) before a single file was removed, and the release
build composes the two back into one artifact.

CORE IS ProfileCore WHEREVER IT NAMES A PROFILE. check.yml, the intake
bot's re-render pass and release.yml's core-side validation all pass
--profile core explicitly; the CLI DEFAULTS stay `all`, because "a data
root" with nothing said must keep meaning the whole database (which is
what every fixture is). The teeth are the file accounting: under `core` a
works-community/ directory that reappears here - a bad merge, a stray
revert, a misdirected contribution - is an unrecognized location and a red
check, rather than a family silently re-adopted.

release.yml composes the pair: it checks out audiosilo-meta-community at
main beside this tree (public, shallow, no secret) and builds with
`metabuild -data data --community community/data`. Everything downstream is
untouched - same asset contract, same patch chain, same data-release
concurrency group, same webhook - and the notes now name both composed
SHAs, since one sha no longer identifies an artifact. A `community-data`
repository_dispatch joins the triggers, because a merge in the other
repository cannot push to this one's main.

The two sidecar issue forms go with the layer. Their COMPOSER stays
(internal/issueform, TemplateFromLabels, compose_sidecar.go): the community
repo runs this very cmd/metaissue under the community profile, so deleting
the code would break the tree the forms moved to. ISSUE_TEMPLATE/config.yml
carries a contact link pointing a sidecar contributor there.

internal/repair and internal/audit gain an Options.Profile (zero value
ProfileAll, so every existing caller is unchanged) threaded through the
store, the post-write format pass and the post-write validation, with
--profile flags on both CLIs. writeFamiliesIn narrows the repair's write
list to the families the root holds, which is not tidiness:
pack.OpenForProfile REFUSES a named family the profile disclaims, and under
`core` works-community is one. Nothing else about a wave moves - no sidecar
loads from a core tree, so the sidecar-moving arm of a merge has nothing to
move.

EQUIVALENCE PROOF, at the same content: metabuild over pre-cutover main as
one tree, and metabuild over this branch's core tree plus a clone of the
community repo via --community, with --built-at pinned so the wall-clock
stamp is not the only difference. Both artifacts are
76ce2cd5e725db2660b7d43263e8005816c2e4088ed7c63279d7d859189a7d23. Without
the pin the two differ in exactly one row, meta.built_at, and in nothing
else (every other table hashes equal).
addCharactersIssueUrl and addRecapsIssueUrl built new-issue URLs against
ISSUE_BASE - this repository - naming templates that moved out with the
layer they contribute to. Left alone, every "add characters" and "add
recaps" button on meta.audiosilo.app would land a contributor on a
template chooser with no such form on it.

They now build against COMMUNITY_ISSUE_BASE. Nothing else about them
moves: the same template names, the same work_ref carrying a
meta.audiosilo.app work URL, read by the same internal/issueform
resolveWorkRef - which the community repo's intake bot runs, since it runs
this repo's cmd/metaissue.

These two are the only issue URLs on the site that do not point at core,
so the existing host/path assertion is the guard and now pins the
community path explicitly.
Found by smoke-testing `metarepair --profile core` over the real
post-cutover tree: every merge died with

  read works-community entry "1984": family "works-community" is not in
  the core tree profile (this root holds people, series, works)

The merge planner asks every loser whether it carries a works-community
member, unconditionally. Under `core` that family is not in the root's
profile, so pack.Store.Get refuses it by name - correctly, since a
misdirected read or write must be loud - and narrowing the WRITE list was
not enough, because the read happens first.

The plan's view now marks such a family out-of-profile and reads EMPTY.
That is not a suppression: "this tree holds no such family" and "this
tree holds no such entry" are the same answer to a merge asking whether
the loser carries a sidecar, and it is the answer a whole-database store
already gives for an absent family. Only the READ is gated - a put or a
del still goes to the store and is still refused by name, so a write that
somehow found something to move stays loud.

Pinned as PARITY: the same fixture merged under both profiles must produce
the same report and the same tree, because over a core tree they are two
readings of one catalogue. Confirmed to fail without the fix. The two
profiles also agree over the real tree - both plan 5 applied / 0 refused /
6 tombstones on the same wave.
The internal/repair entry described writeFamiliesIn but not the half that
turned out to be load-bearing: the plan's view reads an out-of-profile
family EMPTY, because the merge planner asks every loser about a sidecar
unconditionally and a refused read killed every merge over the real core
tree.
The read-empty fix made merges plan again over a core tree, and in doing so
made the sidecar-member-collision refusal STRUCTURALLY BLIND. That refusal is
the human-decision guard over the most expensive data in the project - both
halves of a duplicate carrying the same characters or recaps member - and it
is a question about a family this repository no longer holds. Answering it
"no sidecars" is not a quiet degradation: a wave folds two sidecar-carrying
works together, the loss surfaces much later as a release-blocking
LoadComposed collision, and nothing connects it to the wave that caused it.

Measured over the real pair, the first 30 non-advisory merge proposals are 25
sidecar collisions. A blind wave would have destroyed 25 CC BY-SA sidecars.

The guard now has three modes and never infers safety (sidecarSource):

  in-tree     the root holds works-community - the pre-split shape, read and
              written exactly as before.
  read-only   --community names the community checkout's data/. The question
              is answered from there and the refusal works exactly as it did
              pre-split, but nothing is written to another repository: the
              members keep their retired keys and ride the slug tombstone to
              the survivor (the compose-time re-key) until the community
              re-key sweep lands them. The applied record says so.
  unknown     neither - every merge-works proposal is refused as
              community-data-required, naming the flag. A refused merge is
              recoverable; a folded sidecar pair is not.

Staging is identical in all three, deliberately: a read-only run still stages
the merged entry, which is the only thing that catches two clusters folding
two characters-carrying works onto one target. view.queue is the single place
the modes part.

openCommunity refuses at the door a --community root the tree already holds
the family for, and one carrying no works-community packs - metabuild's own
rule, because pointing the flag at the community repo's top level rather than
its data/ would answer "no sidecars anywhere" for every cluster, which is the
original blindness wearing the flag that was meant to end it.

metarepair and metaaudit now DEFAULT --profile to core, unlike metacheck and
metafmt, and the axis is the cost of being wrong. Those two are read by CI
with the flag spelled out and a wrong reading is a red check. These are
pointed at this tree by a human typing a command, one of them deletes
records, and under `all` over a core tree the family is present-but-empty -
the blind case again, reached through the default. The pair must also always
agree, since metarepair re-runs metaaudit's detectors.

Verification: the split pair and the whole pre-cutover tree plan the same
wave - 5 applied / 25 refused / 5 tombstones either way - and the fixture
test pins the same decision AND the same database, read through
check.LoadComposed so the tombstone and the compose-time re-key are what make
it pass. Every new refusal was confirmed to fail without its guard.
Three fixes to the two-repository build.

TAG COLLISION. The tag was date + the core short sha, which stopped being
unique the moment a community merge could trigger a release: a community-data
dispatch on a day that already released leaves core's HEAD unmoved, so the
second run composes a genuinely different artifact under a tag that already
exists and gh release create fails - and two community merges in one day fail
the same way. The tag now carries both short SHAs. Consumers are unaffected:
nothing reads the tag's shape, since metaserve's latestDataRelease and this
workflow's own prev lookup both select by asset presence at the maximum
published_at, so older single-sha releases stay valid and selectable. Build
identity is computed up front and every step is guarded on it, so a tag that
already exists - the same day, the same pair, genuinely nothing new - skips
for one API call instead of a full compose.

PR-TIME CROSS-TREE SIGNAL. check.yml gains a compose job running the exact
build release.yml performs. The cross-tree rules cannot be asked of one root,
so until now they ran only after merge: a core PR that retired or deleted a
sidecar-keyed work went green and broke the release stream for everyone, with
the fix needing a second PR. It should be required, not advisory - being the
thing that stops the merge is its whole purpose.

TWO CONFIRMED NITS. The composed SHAs are assigned before they are echoed: as
echo "x=$(git ...)", the substitution's failure is discarded because echo's
own status is what the shell sees, so a broken community checkout would have
published a release recording an empty community sha - unreproducible, and
silently so. And the push paths filter gains pkg/check/** and pkg/pack/**,
which now shape artifact CONTENT rather than only validating it
(resolveSidecarKeys re-keys sidecars; the collision keeper decides what
ships). An extra release on a rule tweak costs a build; a wrong-keying fix
sitting unreleased costs every consumer.

actionlint clean. GitHub Actions cannot be run here, so both workflows are
reviewed-only and the diffs are kept minimal and commented.
The review found this class dangerous rather than untidy, and it is: core's
AUTHORING.md walked a contributor through adding a file under
data/works-community/, which is now an unrecognized location here. Following
our own guide produced a pull request our own CI refuses.

AUTHORING.md, EXTRACTION.md and EXTRACTION-AUDIO.md become short pointer
stubs at the community repository, which is where they live. Not copies:
keeping the text in two places drifts, and the drift is exactly what walks
somebody into the red check. The metaextract TOOL stays here; only the
process docs moved, and the two Go doc comments that cited AUTHORING.md now
say where it is.

The site must not name two repositories in one panel. BuildTool's authoring
link and its "pull request instead" hand-off, and contribute.astro's
authoring link, now point at the community repo - the issue prefills beside
them already did, so the panel was telling a contributor to attach the file
there and open a pull request here.

ai-verify.sh's prompt described a four-family tree and carried the whole
sidecar review rubric. It now describes three families, requires CC0-1.0 of
every record, and treats any data/works-community/ path or CC-BY-SA-4.0
record in a diff HERE as a finding in itself. The sidecar length, spoiler and
position rules went with the layer, to the reviewer that sees diffs which can
actually contain one; the two rules in that block that were never about
sidecars are kept.

CONTRIBUTING.md and README.md's gate blocks pass --profile core with one
sentence on why: CI runs those exact commands, and ProfileAll is green
precisely where CI is red. LICENSING.md's "moved with it" is now true.
Completes the cutover's intake half on top of #2265's profile gating.

The two sidecar forms left with the layer, but a data:characters or
data:recaps LABEL can still arrive - a maintainer applying one by hand, a
bookmarked prefill URL, the sibling contributor tool - and routing is by
label, not by which forms this repository ships. Under the default profile
the composer would then write a works-community entry into a family this
root does not hold: a green-looking bot pull request putting CC BY-SA
content in the CC0 repo, and permanently unmergeable, since CI validates
--profile core.

intake.yml now passes --profile core to metaissue, so #2265's
templateBelongsElsewhere refuses the submission as needs-human and the
comment names the community repository's issue chooser - the mirror of the
message that sends a core form the other way. Verified end to end against
the real tree: a data:recaps issue returns needs-human naming
audiosilo-meta-community, where the same run without the flag walks past
the gate entirely.

Nothing was missing on the Go side: #2265 derives the rule from each
template's writeFamilies (so it cannot drift from what the composer would
write), its message already names the community repo for this direction,
and TestCoreProfileSendsASidecarFormToTheCommunityRepo already pins it. The
rebase sweep runs no metaissue - only metafmt and metacheck, both already
core-profile.

CLAUDE.md records the intake gate and adds metaissue to the tools whose
default stays 'all' with CI naming the profile, beside the approved
metaaudit/metarepair exception.
@KodeStar
KodeStar force-pushed the split/core-cutover branch from d073618 to b5f37f6 Compare August 21, 2026 18:21
@github-actions

Copy link
Copy Markdown

AI verification: flagged

Claude flagged the following for a maintainer to check (advisory - not a merge block):

  • Diff touches paths under data/works-community/ (data/works-community/0/0.json, data/works-community/0/12-miles-below-ii-a-house-reborn.json, and others), which per repository policy does not belong in this core repository at all — that family (and any CC-BY-SA-4.0 content) now lives solely in KodeStar/audiosilo-meta-community, and any diff touching this path in this repo is itself a finding regardless of whether it adds or removes entries.

…t cannot move, finish the tense sweep

Simplify pass over the cutover. view.get's nil-read branch answered EMPTY -
the superseded read-empty behavior surviving as a silent fallback one
forgotten sidecarUnknown check away from regaining the blindness the three
modes ended - so it errors now, and the field doc states the contract. The
compose CI job runs on pull requests only (push-to-main composes again
minutes later as the release build, the authoritative gate) and self-skips
unless the PR touches a path that can move its answer, probed via the PR
files API rather than a history-deep clone. Category declaration order
matches Categories()'s triage order; the red-tree advice names the run's own
profile; openCommunity's door comment owns the packs-vs-loadable-entries gap
against metabuild's rule; the staging rationale has one home (view.queue).
The tense sweep the cutover itself demanded: pkg/pack/profile.go's canonical
header, sources.go, metabuild's and compose.go's headers all describe the
POST-split world (and profile.go now carries the CLI default asymmetry by
the cost-of-being-wrong axis); README's metabuild snippets say core-only and
name --community; CLAUDE.md's metabuild entry credits the compose job beside
the release build, the repair entry scopes the modes-part claim to the WRITE
paths and drops the phantom title-carries-no-identity category (that veto is
internal/audit's, inherited through the fresh-audit gate).
@github-actions

Copy link
Copy Markdown

AI verification: flagged

Claude flagged the following for a maintainer to check (advisory - not a merge block):

  • Diff touches paths under data/works-community/ (data/works-community/0/0.json, data/works-community/0/12-miles-below-ii-a-house-reborn.json) — per repo policy this repository (tree profile 'core') no longer holds the works-community family at all; that content lives in the separate KodeStar/audiosilo-meta-community repository. Any diff touching this path, including a deletion, does not belong in this repository's PR flow and should be flagged/rejected rather than merged here.

@KodeStar
KodeStar merged commit 7d1a3e7 into main Aug 21, 2026
4 checks passed
@KodeStar
KodeStar deleted the split/core-cutover branch August 21, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-flagged Advisory AI verification raised findings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant