Implements #50 (capture sub-issue 49a) — the I1-prerequisite classification taxonomy + per-wing/repo tagging. Pure prerequisite, no dependency on the other sub-issues.
What landed
New package ingestion/internal/classification/:
Taxonomy — Level: public < internal < confidential, ordered so "stricter wins" (spec §4.1 model C) is a plain max via Stricter(a, b).
ParseLevel — case-insensitive; unknown token → error (no silent coercion).
Config.Load(brainDir) — reads an optional classification.yaml (wings: / repos: maps). Absent file → defaults-only, no error. An unparseable level in the file → hard error (a classification source the server can't trust fails loud).
Config.Derive(Target) — the function the use-case calls per item; Wing(name) / Repo(name) are the per-kind helpers.
Design decision: central classification.yaml, not the alternatives
The issue floated frontmatter-on-_index.md, a gitea repo topic, or a server-side map. I chose a central classification.yaml at the brain root for three reasons:
No gitea dependency — classifying a repo from a server-readable map needs no live Gitea client, so #50 has no code dependency on the tracker work (#52), and the two dependency-graph roots (#50, #51) stay buildable in parallel as the epic intends.
Auditable in one place (I2/I5) — one Flux-reconcilable file shows every classification.
Frontmatter would be clobbered — brain.BuildWingIndex regenerates a wing's _index.md; durable classification metadata there is fragile.
The file is deployed into the brain dir (runtime data), not committed here. Format is documented in the spec edit + brain note.
Fail-safe is the load-bearing property
The I1 gate (#53) is only as strong as the classification it reads. A missing/unknown tag resolving to confidential (never permissive) is what keeps the gate from being theatre. Covered explicitly: TestWingDefaulting ("" and unknown → confidential), TestRepoDefaulting, TestDeriveUnifiedTarget.
Acceptance criteria
Taxonomy levels defined + ordered; documented (brain note + referenced from the spec)
Per-wing classification tag mechanism implemented and readable by the server
Per-repo classification tag mechanism implemented and readable by the server
deriveClassification (Config.Derive/Wing/Repo) returns the level for a tagged target; untagged → confidential (fail safe)
Implements #50 (capture sub-issue 49a) — the I1-prerequisite classification taxonomy + per-wing/repo tagging. Pure prerequisite, no dependency on the other sub-issues.
## What landed
New package `ingestion/internal/classification/`:
- **Taxonomy** — `Level`: `public < internal < confidential`, ordered so "stricter wins" (spec §4.1 model C) is a plain `max` via `Stricter(a, b)`.
- **`ParseLevel`** — case-insensitive; **unknown token → error** (no silent coercion).
- **`Config.Load(brainDir)`** — reads an optional `classification.yaml` (`wings:` / `repos:` maps). **Absent file → defaults-only, no error.** An unparseable level *in* the file → hard error (a classification source the server can't trust fails loud).
- **`Config.Derive(Target)`** — the function the use-case calls per item; `Wing(name)` / `Repo(name)` are the per-kind helpers.
- **Defaulting** — explicit tag wins → `client-*` → confidential → `hyperguild`/`homelab` → internal → **everything else → confidential (fail safe to strictest)**.
## Design decision: central `classification.yaml`, not the alternatives
The issue floated frontmatter-on-`_index.md`, a gitea repo topic, or a server-side map. I chose a **central `classification.yaml` at the brain root** for three reasons:
1. **No gitea dependency** — classifying a *repo* from a server-readable map needs no live Gitea client, so #50 has **no code dependency on the tracker work (#52)**, and the two dependency-graph roots (#50, #51) stay buildable in parallel as the epic intends.
2. **Auditable in one place** (I2/I5) — one Flux-reconcilable file shows every classification.
3. **Frontmatter would be clobbered** — `brain.BuildWingIndex` regenerates a wing's `_index.md`; durable classification metadata there is fragile.
The file is deployed *into the brain dir* (runtime data), not committed here. Format is documented in the spec edit + brain note.
## Fail-safe is the load-bearing property
The I1 gate (#53) is only as strong as the classification it reads. A missing/unknown tag resolving to **confidential** (never permissive) is what keeps the gate from being theatre. Covered explicitly: `TestWingDefaulting` (`""` and unknown → confidential), `TestRepoDefaulting`, `TestDeriveUnifiedTarget`.
## Acceptance criteria
- [x] Taxonomy levels defined + ordered; documented (brain note + referenced from the spec)
- [x] Per-wing classification tag mechanism implemented and readable by the server
- [x] Per-repo classification tag mechanism implemented and readable by the server
- [x] `deriveClassification` (`Config.Derive`/`Wing`/`Repo`) returns the level for a tagged target; **untagged → confidential (fail safe)**
- [x] Defaulting rules applied (`client-*` → confidential; homelab/hyperguild → internal)
- [x] Unit tests incl. the untagged-fails-safe path (+ stricter-wins, explicit-tag-overrides, bad-level-in-file, case-insensitive)
- [x] `task check` green (lint 0 issues, vet clean, `-race` tests green)
## Docs
- Spec §4.1 updated with an "Implemented (#50)" note pointing at the package + defaults.
- Brain: `wiki/hyperguild/decisions/capture-classification-taxonomy`.
Not auto-merged — review + merge from claude.ai.
Closes #50
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements the I1-prerequisite from #49/#50: the classification taxonomy
and the per-wing / per-repo tagging the capture server reads to derive a
target's sensitivity.
- Levels public < internal < confidential, ordered so "stricter wins"
(spec §4.1 model C) is a plain max via Stricter().
- Tags read from an optional classification.yaml at the brain root
(wings:/repos: maps). Absent file → defaults-only, not an error.
- Defaulting: client-* → confidential; hyperguild/homelab → internal;
everything else → confidential. Fail-safe-to-strictest is the
load-bearing property: a missing tag never silently downgrades.
- Config.Derive(Target) is the function the use-case calls; Wing/Repo
are the per-kind helpers. ParseLevel rejects unknown tokens; a bad
level in the config file is a hard load error.
Central classification.yaml (not _index.md frontmatter, not gitea repo
topics): classifying a repo needs no live Gitea client, so #50 has no
dependency on the tracker work (#52); it's auditable in one place; and
it avoids BuildWingIndex clobbering a wing's regenerated _index.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #50 (capture sub-issue 49a) — the I1-prerequisite classification taxonomy + per-wing/repo tagging. Pure prerequisite, no dependency on the other sub-issues.
What landed
New package
ingestion/internal/classification/:Level:public < internal < confidential, ordered so "stricter wins" (spec §4.1 model C) is a plainmaxviaStricter(a, b).ParseLevel— case-insensitive; unknown token → error (no silent coercion).Config.Load(brainDir)— reads an optionalclassification.yaml(wings:/repos:maps). Absent file → defaults-only, no error. An unparseable level in the file → hard error (a classification source the server can't trust fails loud).Config.Derive(Target)— the function the use-case calls per item;Wing(name)/Repo(name)are the per-kind helpers.client-*→ confidential →hyperguild/homelab→ internal → everything else → confidential (fail safe to strictest).Design decision: central
classification.yaml, not the alternativesThe issue floated frontmatter-on-
_index.md, a gitea repo topic, or a server-side map. I chose a centralclassification.yamlat the brain root for three reasons:brain.BuildWingIndexregenerates a wing's_index.md; durable classification metadata there is fragile.The file is deployed into the brain dir (runtime data), not committed here. Format is documented in the spec edit + brain note.
Fail-safe is the load-bearing property
The I1 gate (#53) is only as strong as the classification it reads. A missing/unknown tag resolving to confidential (never permissive) is what keeps the gate from being theatre. Covered explicitly:
TestWingDefaulting(""and unknown → confidential),TestRepoDefaulting,TestDeriveUnifiedTarget.Acceptance criteria
deriveClassification(Config.Derive/Wing/Repo) returns the level for a tagged target; untagged → confidential (fail safe)client-*→ confidential; homelab/hyperguild → internal)task checkgreen (lint 0 issues, vet clean,-racetests green)Docs
wiki/hyperguild/decisions/capture-classification-taxonomy.Not auto-merged — review + merge from claude.ai.
Closes #50
🤖 Generated with Claude Code