Эх сурвалжийг харах

docs(architecture): revise 02-canonical-schema to v3 — schema accepts its corpus, 3 blockers ratified

Nine-kind dependency vocabulary + wildcard refs (all 94 registry refs
validate, incl. the 19 previously rejected); ContextSchema name/description
optional/derived; targets[] applicability on every content type; registry
profiles/categories/aliases restored; implicit-default permission rule
adopted (opposite-of-decisions-present; mixed-without-* = parse error;
8-block migration impact enumerated); blockers ratified: editable content/
= YES, model not authorable -> inference.tier, implicit default per above.
All counts re-verified from disk 2026-07-15.

Task: canonical-refactor-06
darrenhinde 3 долоо хоног өмнө
parent
commit
3146bb5f36

+ 472 - 196
docs/architecture/canonical-refactor/02-canonical-schema.md

@@ -1,19 +1,42 @@
-# 02 — Canonical IR Schema — **v2**
+# 02 — Canonical IR Schema — **v3**
 
-> **Status:** Specification only — v2 (realigned to `00-INDEX.md` v2). No implementation.
+> **Status:** Specification only — v3 (post `06-REVIEW.md` repairs). No implementation.
 > Zod blocks are *illustrative*: they define the target shape of `packages/core/src/schema/*.ts`,
 > not code to ship as-is.
-> **Read `00-INDEX.md` (v2) first.** Where this doc conflicts with the index, **the index wins**.
+> **Read `00-INDEX.md` (v2) first.** Where this doc conflicts with the index, **the index wins**
+> — except for the three blocker decisions ratified in §0.4, which follow the `06-REVIEW`
+> dispositions (precedence: 06-REVIEW dispositions > 08 > 07 > 00-INDEX, per `12-DISPATCH.md`).
 >
-> **Changes from v1:**
-> - §1.2 **Capabilities rewritten to Option A** (ordered rule list + scalar sugar) per locked
->   decision #5. The v1 flat model is withdrawn — verified provably lossy (§1.2.1).
-> - **`guards` hint deleted.** Option A puts safety globs in content natively; the v1 open
->   question about globs is resolved and removed.
-> - §4 **Context rewritten** around the MVI HTML-comment format per locked decision #6.
->   No YAML migration. Adds a precise parser spec — **the highest-risk parser requirement in
->   the project**.
-> - Census corrected (§0.1); `CapabilityMatrix` row corrections folded into §8.
+> **Changes from v2 (v3, 2026-07-15 — repairs per 06-REVIEW F2/C1/C8/L2/L4/L5/L6/L7/G2 and
+> ratification of the three blockers; every count re-verified against disk on this date):**
+> - **§0.4 NEW — the three blocking decisions are RATIFIED:** (a) user projects keep an
+>   **editable `content/`** (04 Q9 = YES); (b) **`model` is NOT authorable** — deleted from the
+>   authored schema and replaced by `inference.tier: fast | balanced | deep` (default `balanced`),
+>   preserving the 5-sonnet/2-haiku scout cost tiering; (c) the **implicit-default permission
+>   rule** is adopted into §1.2.5 (no `*` rule → opposite of the decisions present;
+>   mixed-decisions-without-`*` = parse error).
+> - **§1.5 Dependencies rewritten (fixes 06-REVIEW F2).** One nine-kind vocabulary shared with
+>   `ComponentTypeSchema` (adds `agent`, `plugin`, `config`, `hook`); targets accept `/`-joined
+>   path segments and a trailing `/*` wildcard. **All 94 unique dependency refs in
+>   `registry.json` — including the 19 the v2 grammar rejected — now validate** (verified).
+> - **§1.2.5 implicit-default rule added (fixes C8)**, with a full-corpus measurement: of the
+>   33 no-`*` capability blocks across all 34 agents, 22 are homogeneous-deny (→ implicit
+>   `allow`, covers `coder-agent`'s five-denies `edit` block), 3 homogeneous-allow (→ implicit
+>   `deny`), and **8 are mixed → parse error, requiring a one-line migration** (enumerated).
+> - **§1.3 `inference.model` deleted; `inference.tier` added** (fixes L4, closes v2 Q3).
+> - **§4.4 `ContextSchema` fixed (fixes G2):** `name`/`description` optional/derived — under v2
+>   every real context file failed validation; `function` split from the compound category.
+> - **`targets: []` applicability added to every content type** (fixes L2).
+> - **§7 Registry restored (fixes L5/L6/L7):** top-level `profiles` object (`badge`,
+>   `additionalPaths`, `components[]`), `categories` (objects with `icon`/`order`/`status`),
+>   and `aliases[]` on registry entries and `AgentSchema`.
+> - **§1.2.4 precedence rewritten** to match the current index (last-match-wins, primary
+>   verification still required before Stage 1) — v2 argued against index prose that no longer
+>   exists (fixes C1); v2 Q1 closed.
+> - **Census re-verified 2026-07-15 — the tree has moved since v2 and 06-REVIEW:** context is
+>   now **296 `.md` path entries = 293 regular files + 3 symlinks** (v2 said 297 = 294 + 3);
+>   priority distribution over the leading window is now **113/112/34/29/1**; registry now holds
+>   **248 entries** (contexts 194). All counts below are as-of-date measurements, with methods.
 
 ## 0. What this document is
 
@@ -31,20 +54,24 @@ For every field: name, type, required/optional, default, and whether it is a **n
 several places. Each section ends with a **"Too OpenCode-shaped — must change"** callout citing
 concrete lines.
 
-### 0.1 Corrected census (re-verified independently for this doc)
+### 0.1 Corrected census (re-verified from disk **2026-07-15** for v3)
 
 | Item | Count | Method |
 |---|---|---|
 | Agents | **34** `.md` | `find .opencode/agent -name '*.md'` |
 | Commands | **20** `.md` | `find .opencode/command -name '*.md'` |
-| Context | **297** `.md` | `find .opencode/context -name '*.md'` |
-
-⚠️ **Context count delta vs index.** The index states **296** (286 HTML-comment / 3 YAML /
-7 neither). My reproducible count over `.opencode/context/**/*.md` gives **297**, and the
-format breakdown is materially different from a naive reading — see §4.1, which supersedes the
-"286/3/7" summary with a verified line-position analysis. The 1-file delta does not change any
-design decision (the parser must handle every bucket regardless), but the *bucket semantics* do
-change the parser spec. Flagged for reconciliation in Open Questions.
+| Context | **296** `.md` path entries = **293** regular files + **3 symlinks** | `find .opencode/context -name '*.md'` (296) vs `… -type f` (293) |
+| Skills (OpenCode) | **6** dirs across **two** trees: `.opencode/skill/` (2) + `.opencode/skills/` (4), `task-management` duplicated | `ls -d` both trees |
+| Skills (CC plugin) | **12** dirs | `ls -d plugins/claude-code/skills/*/` |
+
+⚠️ **The tree has moved since v2 and since `06-REVIEW`.** v2 counted 297 `.md` path entries
+(294 files + 3 symlinks per 06-REVIEW F9/U1); the 2026-07-15 recount gives **296 = 293 + 3**
+— one context file has since been removed. The three symlinks
+(`core/standards/code.md → code-quality.md`, `docs.md → documentation.md`,
+`tests.md → test-coverage.md`) are the index's v2.2 symlink finding and are counted as path
+entries, not files. **Do not quote any census number without recounting with the
+leading-window rule** (`12-DISPATCH.md` repeats this warning). The marker-position buckets are
+re-derived in §4.1 as of the same date.
 
 ### 0.2 Design rules for neutrality
 
@@ -79,8 +106,51 @@ export const TagsSchema = z.array(z.string()).default([]);
 /** Semver of the authored component. NEUTRAL for the registry/update engine; most tools
  *  drop it (HINT at the tool layer). NOTE: context files use X.Y on disk → normalized (§4). */
 export const VersionSchema = z.string().regex(/^\d+\.\d+\.\d+$/).default("1.0.0");
+
+/** Known build targets. */
+export const TargetSchema = z.enum(["opencode", "claude", "cursor", "windsurf"]);
+
+/** Applicability (NEW in v3 — fixes 06-REVIEW L2). Empty array (the default) = universal:
+ *  the component applies to every target. Non-empty = the component is emitted ONLY for the
+ *  listed targets; `oac build --target X` SKIPS a component whose non-empty `targets` lacks X
+ *  — silently, BY DESIGN: being skipped is authored intent, not degradation, so it produces
+ *  no warning (avoids the per-agent-per-target warning noise 06-REVIEW L2 predicts).
+ *  This is what makes the union skill set (6 OpenCode + 12 CC, disjoint) representable:
+ *  Bun-shelling OpenCode skills declare ["opencode"], CC-frontmatter skills ["claude"],
+ *  OpenCode custom tools ["opencode"], session-start.sh's hook ["claude"], etc.
+ *  NEUTRAL INVARIANT — every adapter must honor it. Appears on EVERY content type. */
+export const TargetsSchema = z.array(TargetSchema).default([]);
 ```
 
+### 0.4 Ratified blockers (v3 — binding; per 06-REVIEW "Top 5" fix #4)
+
+`06-REVIEW` triaged the 60 open questions across the spec set down to **three that block
+code**. All three are hereby **ratified** for the schema; downstream docs (`03`, `04`, `05`)
+must align to these answers.
+
+1. **Do user projects hold an editable `content/`? — YES** *(closes `04` Q9 / 06-REVIEW C5).*
+   The user's project keeps an editable `content/` directory as the input to `oac build` run
+   on the user's machine. Consequence for this doc: the CLI ships the full IR — parser, these
+   schemas, and the adapters — not just pre-built outputs. The OAC repo's own `/content/` and
+   a user project's `content/` are the **same artifact class** validated by the **same
+   schemas**; `04` owns the lifecycle (init/add/update) distinctions.
+
+2. **Is `model` authorable? — NO.** *(closes v2 Q3 / 01 Q3 / 06-REVIEW C3+L4).* `model` is
+   **deleted from the authored schema and from the IR**. In its place, `inference.tier:
+   fast | balanced | deep` (default `balanced`) expresses the *intent* the hardcoded models
+   carried — verified: the shipped CC plugin is 5× `model: sonnet` + 2× `model: haiku`, and
+   the two haiku agents are the scouts (`context-scout`, `external-scout`), a deliberate
+   cost/latency tier that `model: null` alone would silently destroy. Adapters map tier → that
+   tool's fast/balanced/deep model; tools without tiers drop it **with a warning**. Concrete
+   model ids live only in a build-time user/project override layer (`04`), never in content.
+   This makes `05`'s Layer-1 test ("`model:` set → rejected") correct as written.
+
+3. **Implicit default when rules exist but no `*` rule matches — adopted.** *(closes
+   06-REVIEW C8 / `03` Q2; the live security gap, index finding #10, depends on it.)* Full
+   normative statement in **§1.2.5**: the implicit terminal default is the **opposite of the
+   decisions present**; a mixed-decision list without an explicit `*` rule is a **parse
+   error**. Verified against all 34 agents — corpus impact enumerated in §1.2.5.
+
 ---
 
 ## 1. Agent
@@ -175,63 +245,105 @@ elegance of Option A:
 | `web` | URL/domain glob | `https://internal.*` |
 | `delegate` | agent id (kebab) or `*` | `contextscout` |
 
-#### 1.2.4 Precedence — ⚠️ index prose and index example conflict; needs ratification
-
-The index v2 says *"first-match-wins ordering preserved"*, but its own worked example — and
-**100% of the real scoped agents** — author **broad-first, specific-after**:
-
-- coder-agent: `"*": deny` is **first**, allowlist entries follow.
-- openagent: `"*": ask` is **first**, specific denies follow.
-
-Under **naive first-match-wins in authored order**, `"*"` matches everything and every later rule
-is unreachable: coder-agent's `router.sh` would be **denied** (breaking the exact behavior locked
-decision #5 exists to protect), and openagent's `sudo *` would resolve to **ask**, not **deny** —
-a real security regression. So first-match-wins over the authored order is provably wrong against
-the current corpus.
-
-Two semantics produce correct results on all real data:
-
-| Semantics | Definition | Verdict on real data |
-|---|---|---|
-| **Last-match-wins** *(recommended)* | Evaluate in authored order; the **last** matching rule wins. | ✅ correct for both agents |
-| Most-specific-wins | Order-independent; the most specific matching pattern wins. | ✅ correct for both agents |
-| Naive first-match-wins | First matching rule wins. | ❌ breaks both |
-
-**Recommendation: last-match-wins.** It (a) makes the index's own worked example correct *as
-written*, with no re-authoring of 34 agents; (b) reads the way humans write policy — broad
-default, then exceptions; (c) maps 1:1 to OpenCode's YAML mapping key order (JS string keys
-preserve insertion order), so IR→OpenCode→IR round-trips order-preserving and lossless.
-Most-specific-wins would need a specificity metric (pattern length? segment count?) that is
-ambiguous for equal-specificity overlaps.
-
-**Both candidates agree on every one of the 34 current agents**, so this choice is about
-future-proofing, not migration risk. Critically, **the schema shape is identical either way** —
-only the resolver's evaluation differs. So the shape below can be locked now and the semantics
-ratified independently without blocking any workstream. Raised in Open Questions.
+#### 1.2.4 Precedence — **last-match-wins** (aligned to index v2.2; v2's conflict note retired)
+
+*(v3 note — fixes 06-REVIEW C1: v2 of this section argued against index prose that no longer
+exists. The current index adopts last-match-wins, with the honest v2.2 caveat that it is
+"STRONGLY INDICATED — NOT independently confirmed" and that primary verification against a
+real OpenCode install is REQUIRED before Stage 1. This doc adopts the same position; v2's
+Open Question 1 is closed as already-answered.)*
+
+**Semantics: evaluate rules in authored order; the last matching rule wins.** All real scoped
+agents author **broad-first, specific-after** (`coder-agent`: `"*": deny` first, allowlist
+after; `openagent`: `"*": ask` first, specific denies after). Under naive first-match-wins the
+leading `"*"` would make every later rule unreachable — denying `coder-agent`'s `router.sh`
+and downgrading `openagent`'s `sudo *` from deny to ask. Last-match-wins makes the corpus
+correct as written, needs zero reordering on either serialize or parse, and maps 1:1 to
+OpenCode's YAML key order. The schema shape is identical under any candidate semantics — only
+the resolver differs — so the shape is locked here while the Stage-1 primary verification
+(index v2.2) remains outstanding.
+
+Parse-time validations the order-as-semantics contract requires (index v2.2 residual risk):
+**integer-like scopes** (e.g. `"8080"`) are rejected in map sugar (ECMAScript integer-key
+reordering would silently move them), and **duplicate scopes** in one map are a parse error
+(YAML would silently collapse them).
 
 ```ts
 /** Resolve a request against a rule list. Illustrative signature only.
- *  Terminal fallback when NO rule matches = "allow" (§1.2.5). */
+ *  Terminal fallback when NO rule matches = the implicit default of §1.2.5. */
 declare function resolve(rules: Rule[], request: string): "allow" | "deny" | "ask";
 ```
 
-#### 1.2.5 Defaults — absent capability vs. no-rule-matched
-
-Two distinct "nothing said" cases, and getting them wrong breaks every existing agent:
-
-- **Capability absent** from `capabilities` → empty list `[]` → *no author constraint* → the
-  tool's own default applies. Rationale: `openagent` declares no `write` key and OpenCode
-  therefore **allows** write. If the IR defaulted `write: deny` (as v1 did), the build would
-  silently strip a capability the agent relies on.
-- **Rules present, none match** → terminal fallback **`allow`**.
-
-Rationale for both: OpenCode's `permission` block is a **restriction list** over an
-otherwise-permitted tool, and all 34 agents were authored against that semantics. Reinterpreting
-them as deny-by-default would break the corpus. Authors wanting deny-by-default write it
-explicitly — exactly as `coder-agent` does with a leading `{scope: "*", decision: "deny"}`.
-
-> **v1 correction:** the v1 `CapabilitiesSchema` defaulted `write/edit/bash/web` to `deny`. That
-> was wrong on this evidence and is withdrawn along with the flat model.
+#### 1.2.5 Defaults — absent capability, no-rule-matched, and the implicit default (RATIFIED v3)
+
+*(v3 — this section now owns the rule 06-REVIEW C8 found ownerless: "the highest-severity
+finding in the whole set depends on a rule no document owns." It adopts `03` §0.4's proposal
+per the 06-REVIEW disposition, ratified in §0.4 blocker 3.)*
+
+Three distinct "nothing said" cases:
+
+1. **Capability absent** from `capabilities` → empty list `[]` → *no author constraint* → the
+   tool's own default applies. Rationale: `openagent` declares no `write` key and OpenCode
+   therefore **allows** write. If the IR defaulted `write: deny` (as v1 did), the build would
+   silently strip a capability the agent relies on.
+
+2. **Rules present, one of them is a `*` (or otherwise-terminal catch-all) rule** → the rule
+   list is total; no implicit default is ever consulted. This is the recommended authoring
+   style and what migration normalizes toward.
+
+3. **Rules present, NO `*` rule, and no rule matches the request** → the **implicit default**
+   applies, computed from the authored rules:
+
+   | Authored decisions in the list | Implicit default for non-matching requests |
+   |---|---|
+   | all `deny` | **`allow`** — the list is a *restriction list* over an otherwise-permitted tool |
+   | all `allow` | **`deny`** — the list is an *allowlist*; anything unlisted is out |
+   | mixed (`allow`+`deny`, or any list containing `ask`) | **parse error** — the author's terminal intent is ambiguous; an explicit `{scope: "*", decision: …}` rule is required |
+
+   The rule in one line: **no `*` rule present → the opposite of the decisions present;
+   mixed decisions without `*` = parse error.** (`ask` has no defensible "opposite", so any
+   `ask` in a no-`*` list falls in the parse-error row; zero corpus impact — verified below.)
+
+**Why this is load-bearing:** `coder-agent.edit` is five `deny` globs (`**/*.env*`, `**/*.key`,
+`**/*.secret`, `node_modules/**`, `.git/**`) with **no `*` rule** — verified verbatim on disk.
+Under row 1 it resolves to implicit `allow` for everything else, exactly matching OpenCode's
+live behavior. Critically, the **collapse for lossy targets (§1.2.8) consumes the implicit
+default too**: on Claude the block collapses to an `Edit` grant *with the five security globs
+gone* — the live security gap (index finding #10). The implicit default is what makes that
+loss *computable*, so the adapter can warn (or block, per `03` Q3) instead of guessing.
+
+**Full-corpus verification (2026-07-15, all 34 agents, frontmatter `permission` blocks):**
+24 agents carry permission blocks containing **82** capability rule-maps; **33** of those maps
+have no `*` rule. They split:
+
+- **22 homogeneous-deny** (secret-glob restriction lists like `coder-agent.edit`) → implicit
+  `allow` = today's OpenCode behavior. ✅ no migration.
+- **3 homogeneous-allow** — all `task` (delegate) allowlists: `coder-agent`
+  (`contextscout, externalscout, TestEngineer`), `reviewer` (`contextscout`), `test-engineer`
+  (`contextscout, externalscout`) → implicit `deny` = the obvious authored intent (delegate
+  only to the named agents), and consistent with the many corpus `task` maps that spell
+  `"*": deny` explicitly. ✅ no migration.
+- ⚠️ **8 mixed → parse error under this rule.** Verified list (file → capability):
+  `core/opencoder.md` → `bash` (ask+deny); `meta/repo-manager.md` → `bash` (ask+deny);
+  `subagents/core/context-manager.md` → `edit` **and** `write` (allow+deny);
+  `subagents/core/documentation.md` → `edit` (allow+deny);
+  `subagents/core/externalscout.md` → `read` (allow+deny);
+  `subagents/development/frontend-specialist.md` → `edit` (allow+deny);
+  `subagents/planning/adr-manager.md` → `edit` (allow+deny).
+  **Consequence:** the Stage-3 seed migration must add one explicit terminal rule
+  (`{scope: "*", decision: …}`) to each of these 8 blocks, choosing the decision that
+  preserves the tool behavior the agent was authored against (for these, OpenCode's
+  tool default). This is a deliberate, *loud* disambiguation of 7 agent files — the parse
+  error exists precisely because these blocks' terminal intent is ambiguous today, and
+  silently guessing is how index finding #10 happened. 06-REVIEW measured only
+  `coder-agent`/`openagent`; this 8-block impact is new data recorded here so the migration
+  (doc `09`/Stage 3) can budget for it.
+
+> **v1 correction (retained):** the v1 `CapabilitiesSchema` defaulted `write/edit/bash/web` to
+> `deny`. That was wrong on this evidence and is withdrawn along with the flat model.
+> **v2 correction:** v2's blanket "no rule matched → `allow`" is superseded by the implicit
+> default above — it was right for restriction lists and wrong for allowlists (it made every
+> no-`*` delegate map meaningless).
 
 #### 1.2.6 Sugar ↔ full desugaring
 
@@ -321,26 +433,38 @@ Worked: `coder-agent.bash` collapses to `allow` under `permissive` (it has allow
 Claude gets `Bash` with **none of the deny-all scoping**. That is a genuine, material capability
 loss and exactly why it must warn loudly rather than degrade quietly.
 
-### 1.3 Inference block (temperature, maxSteps, model=null)
+### 1.3 Inference block (tier, temperature, maxSteps — **`model` deleted, RATIFIED v3**)
 
 ```ts
 /** Model tuning knobs, grouped so adapters process them as a unit and the degradation
- *  report is legible ("inference.temperature dropped; inference.model → tool default"). */
+ *  report is legible ("inference.temperature dropped; inference.tier → tool default"). */
 export const InferenceSchema = z.object({
-  /** null ⇒ tool default (locked decision #2 — NO hardcoded models). When set, a neutral
-   *  family id (e.g. "claude-sonnet-4"); ModelMapper resolves to the tool's dated id. */
-  model: z.string().nullable().default(null),
+  /** Semantic cost/latency tier — NOT a model name (ratified §0.4 blocker 2; locked decision
+   *  #2 means NO model is ever authorable in content). Adapters map tier → that tool's
+   *  fast/balanced/deep model (e.g. CC: fast → haiku, balanced → sonnet); tools without
+   *  tiers drop it WITH a warning. NEUTRAL intent; HINT where unmappable.
+   *  Verified rationale: the shipped CC plugin is 5× sonnet + 2× haiku, and the haiku pair
+   *  is exactly the scouts — a deliberate cost tier that `model: null` alone would destroy.
+   *  Migration: model: haiku → tier: fast; model: sonnet → tier: balanced (or omitted). */
+  tier: z.enum(["fast", "balanced", "deep"]).default("balanced"),
   /** 0.0–2.0. NEUTRAL intent; HINT at Claude (temperatureControl = none → dropped + warned),
    *  partial at cursor/windsurf. */
   temperature: z.number().min(0).max(2).optional(),
   /** HINT — only OAC/OpenCode honor it (maxSteps: claude/cursor/windsurf = none). */
   maxSteps: z.number().int().positive().optional(),
-}).default({ model: null });
+}).default({});
 ```
 
-`types.ts` puts these as flat siblings (181–184) and — **confirmed defect, index v2** —
-`ModelIdentifierSchema = z.union([z.string(), z.string()])` (line 116) is a **no-op union** with
-no null default, so it cannot enforce decision #2.
+**There is no `model` field** — not in the authored form and not in the IR. A frontmatter
+`model:` key is a **parse error** with a fix-it message pointing at `tier` and at the
+build-time override layer (`04` owns it: user/project config may pin concrete models at build
+time; content never does). This makes `05`'s Layer-1 test case 2 ("`model:` set → rejected")
+correct as written and removes `03`'s "`inference.model` set → emit `model:`" rows (06-REVIEW C3).
+
+`types.ts` puts these knobs as flat siblings (181–184) and — **confirmed defect, index v2** —
+`ModelIdentifierSchema = z.union([z.string(), z.string()])` (line 116, re-verified 2026-07-15)
+is a **no-op union** with no null default, so it cannot enforce decision #2. It is deleted,
+not repaired.
 
 ### 1.4 Context references
 
@@ -363,28 +487,70 @@ export const ContextRefSchema = z.object({
 Matches `types.ts:63-67` (already neutral). Only change: prefer `id` references over raw
 `.opencode/...` paths, which bake in the source tool.
 
-### 1.5 Dependencies (typed refs)
+### 1.5 Dependencies (typed refs) — **rewritten v3 (fixes 06-REVIEW F2)**
+
+The v2 grammar rejected **19 real refs in `registry.json`** — every `agent:`, `plugin:`, and
+`config:` ref and every wildcard — and disagreed with §7's own `ComponentTypeSchema` about the
+kind vocabulary. v3 fixes both defects: **one nine-kind vocabulary, defined once and shared**,
+and a target grammar that admits path segments and wildcards.
 
 ```ts
+/** THE component-kind vocabulary — single source, shared by dependencies (here) and by
+ *  registry entries (§7 ComponentTypeSchema === this schema). Nine kinds. */
 export const DependencyKindSchema = z.enum([
-  "subagent", "context", "command", "skill", "tool",
+  "agent", "subagent", "command", "skill", "context", "tool", "hook", "plugin", "config",
 ]);
 
+/** A dependency target: a kebab id, optionally namespaced by `/`-joined kebab segments,
+ *  optionally ending in the wildcard segment `/*`.
+ *    contextscout                      plain id
+ *    core/context-system               path-style id (context namespace)
+ *    core/*                            wildcard — expands at closure-resolution time
+ *    core/context-system/*             nested wildcard */
+export const DependencyTargetSchema = z.string().regex(
+  /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*(?:\/\*)?$/,
+  "target must be kebab segments joined by '/', optionally ending in '/*'",
+);
+
 export const DependencyRefSchema = z.object({
   kind: DependencyKindSchema,
-  id: IdSchema,
+  target: DependencyTargetSchema,        // was `id` — widened; IdSchema still governs plain ids
 });
 
-/** Also accepts the compact "kind:id" string — the REAL on-disk format in both
- *  .opencode/config/agent-metadata.json and registry.json ("subagent:contextscout"). */
+/** Also accepts the compact "kind:target" string — the REAL on-disk format in both
+ *  .opencode/config/agent-metadata.json and registry.json ("subagent:contextscout",
+ *  "context:core/*", "config:agent-metadata"). */
 export const DependencyInputSchema = z.union([
   DependencyRefSchema,
-  z.string().regex(/^(subagent|context|command|skill|tool):[a-z0-9-]+$/),
+  z.string().regex(
+    /^(agent|subagent|command|skill|context|tool|hook|plugin|config):[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*(?:\/\*)?$/,
+  ),
 ]);
 ```
 
-Rename `type` → `kind` (avoids collision with the codebase's several other `type` fields).
-Neutral because tools consume the resolved closure, not the notation.
+**Verified against the corpus (2026-07-15):** `registry.json` carries **94 unique refs**
+across all `dependencies[]` arrays and all `profiles.*.components[]` lists. The grammar above
+validates **94/94**, including the 19 the v2 regex rejected: 8 `agent:` refs (`openagent`,
+`opencoder`, `system-builder`, `copywriter`, `technical-writer`, `data-analyst`, `eval-runner`,
+`repo-manager`), 3 `config:` refs (`env-example`, `agent-metadata`, `readme`), `plugin:notify`,
+and 7 wildcards (`context:core/*`, `context:core/context-system/*`, `context:context-system/*`,
+`context:development/*`, `context:openagents-repo/*`, `context:project-intelligence/*`,
+`context:ui/*`). *(06-REVIEW F2's inline listing shows 18 of the 19; the one it omitted is
+`context:context-system/*`. Its count of 19 was correct.)*
+
+**Semantics:**
+- Rename `type` → `kind` (avoids collision with the codebase's several other `type` fields).
+  Neutral because tools consume the resolved closure, not the notation.
+- **Wildcard refs expand at closure-resolution time** to every registry entry of that kind
+  whose id/path falls under the prefix — parity with `install.sh`'s
+  `expand_context_wildcard()` (`01` §7.2, the preservation checklist's "crown-jewel"). Today
+  every corpus wildcard is a `context:` ref; the grammar permits wildcards on any kind, and
+  `doctor` warns on kinds where expansion has no defined base tree.
+- **Resolution failure is a hard error** — an unresolvable ref or a wildcard expanding to
+  zero components fails the build; never `|| echo ""` (index finding #2).
+- Alias-aware: a plain-id target matches a registry entry when `entry.id === target` **or**
+  `entry.aliases` contains it (§7; parity with `resolve_dependencies()`'s
+  `.id == id or (.aliases // []) | index(id)`).
 
 ### 1.6 Examples (few-shot)
 
@@ -413,19 +579,23 @@ Confirmed by index v2: `types.ts` `AgentCategorySchema` (line 93) means *domain*
  *  types.ts list is arbitrary and real data already exceeds it ("subagents/core",
  *  "testing", "data"…). */
 export const CategorySchema = z.string().optional();
-
-/** Distribution profile — which install tier ships this. Owned by the registry entry (§7). */
-export const ProfileSchema = z.enum([
-  "essential", "standard", "extended", "specialized", "meta",
-]);
 ```
 
+*(v3 — the v2 `ProfileSchema` tier **enum** is withdrawn. Distribution profiles are now a
+top-level registry **object** — `RegistrySchema.profiles`, §7 — owning `components[]`
+membership lists, `badge`, and `additionalPaths`, matching `registry.json` on disk and what
+`04` §2.2 reads. Membership lives in the profile, not as a tag on each entry; the registry is
+the sole owner — closes v2 Q9.)*
+
 ### 1.8 Full Agent IR
 
 ```ts
 export const AgentSchema = z.object({
   // ---- identity (folded-in metadata; NO sidecar) ----
-  id: IdSchema,
+  id: IdSchema,                              // filename == id (01 Q1, per 06-REVIEW triage)
+  /** Alternate ids this agent answers to in dependency refs (NEW v3 — fixes 06-REVIEW L7).
+   *  Back-compat for renames like tester → test-engineer. */
+  aliases: z.array(IdSchema).default([]),
   name: NameSchema,
   description: DescriptionSchema,
   role: RoleSchema,                          // was `mode`
@@ -434,10 +604,12 @@ export const AgentSchema = z.object({
   version: VersionSchema,
   author: z.string().default("oac"),
   dependencies: z.array(DependencyInputSchema).default([]),
+  /** Applicability (NEW v3, §0.3). [] = all targets. */
+  targets: TargetsSchema,
 
   // ---- behavior ----
   capabilities: CapabilitiesSchema,          // Option A; replaces tools + permission
-  inference: InferenceSchema,                // model = null default
+  inference: InferenceSchema,                // tier-based; `model` is a parse error (§1.3)
   context: z.array(ContextRefSchema).default([]),
   examples: z.array(AgentExampleSchema).default([]),
 
@@ -476,9 +648,13 @@ registry `skills` entries carry a `files: [...]` array. A Skill is a **directory
 `SKILL.md` (lean YAML frontmatter: `name`, `description`, `version`, `type`, `category`, `tags`);
 body = instructions; bundled files travel with it.
 
-⚠️ Index v2 finding #3: OpenCode has **2** skill dirs, the CC plugin has **~11**, and the sets are
-**disjoint**. Seeding `/content/` from `.opencode/` alone destroys the CC set — a merge concern
-for Agent E, but it means `SkillSchema` must model both origins.
+⚠️ Index v2.2 (corrected counts, re-verified from disk 2026-07-15): OpenCode has **6** skill
+dirs across **two** trees (`.opencode/skill/` ×2 + `.opencode/skills/` ×4, with
+`task-management` duplicated across both), the CC plugin has **12**, and the sets are
+**disjoint**. Seeding `/content/` from `.opencode/` alone destroys the CC set — a merge
+concern for Agent E (owner + conflict rules: doc `09`), but it means `SkillSchema` must model
+both origins. The union set is representable because every skill carries `targets` (§0.3):
+Bun-shelling OpenCode skills declare `["opencode"]`, CC-frontmatter skills `["claude"]`.
 
 ```ts
 /** A file shipping alongside SKILL.md. NEUTRAL as a manifest entry; whether a target can
@@ -492,6 +668,7 @@ export const SkillFileSchema = z.object({
 
 export const SkillSchema = z.object({
   id: IdSchema,
+  aliases: z.array(IdSchema).default([]),    // NEW v3
   name: NameSchema,
   description: DescriptionSchema,
   category: CategorySchema,
@@ -499,6 +676,9 @@ export const SkillSchema = z.object({
   version: VersionSchema,
   author: z.string().default("oac"),
   dependencies: z.array(DependencyInputSchema).default([]),
+  /** Applicability (NEW v3, §0.3). [] = all targets. The disjoint 6+12 skill union depends
+   *  on this field. */
+  targets: TargetsSchema,
 
   /** Directory basename rooting the skill (contains SKILL.md). NEUTRAL — every tool with
    *  skills is directory-based (.claude/skills/<dir>/SKILL.md, .opencode/skill/<dir>/SKILL.md). */
@@ -527,10 +707,14 @@ Real source: `.opencode/command/add-context.md`, `context.md`. Verified frontmat
 ```ts
 export const CommandSchema = z.object({
   id: IdSchema,
+  aliases: z.array(IdSchema).default([]),  // NEW v3
   name: NameSchema.optional(),           // often absent; derive from id
   description: DescriptionSchema,        // NEUTRAL INVARIANT — the only universal field
   tags: TagsSchema,
   dependencies: z.array(DependencyInputSchema).default([]),
+  /** Applicability (NEW v3, §0.3). [] = all targets — e.g. the 6 CC-only commands declare
+   *  ["claude"]. */
+  targets: TargetsSchema,
 
   /** OPTIONAL, HINT. OpenCode lets a slash-command pin an agent/inference/capabilities;
    *  Claude/Cursor slash-commands are plain prompt templates. Reuses Agent primitives
@@ -555,27 +739,31 @@ body }`. `types.ts` has no Command schema; this is net-new from already-neutral
 
 Locked: context metadata **stays** the compact MVI HTML-comment line on disk. **No YAML
 migration.** It is deliberate token-efficiency — the model reads it on every context load — and
-multi-line YAML would cost tokens on all ~297 files. The IR normalizes it in memory; **on-disk
+multi-line YAML would cost tokens on all ~296 files. The IR normalizes it in memory; **on-disk
 format ≠ IR shape**.
 
 ### 4.1 ⚠️ Verified format census — the highest-risk parser requirement
 
-I re-derived the breakdown by **marker line position**, which the "286/3/7" summary obscures:
+Re-derived from disk **2026-07-15** by **marker position under the leading-window rule**
+(line 1, or the first non-blank line after a closing YAML `---`). The tree has moved since v2
+(one file removed):
 
 | Bucket | Count | Detail |
 |---|---|---|
-| Marker on **line 1** (happy path) | **287** | `<!-- Context: … -->` first line |
-| Marker present but **NOT line 1** | **7** | see below — two very different kinds |
+| Marker on **line 1** (happy path) | **286** | `<!-- Context: … -->` first line |
+| **Dual-format** — YAML block + marker in the leading window | **3** | marker at line 11, after the closing `---` |
+| Marker present but **only outside** the leading window | **4** | prose about the format — must NOT parse |
 | **No marker at all** | **3** | `index.md`, `core/workflows/task-delegation.md`, `core/context-system/standards/typescript-coding.md` |
-| **Total** | **297** | |
+| **Total** | **296** | = 293 regular files + 3 symlinks |
 
-The 7 "marker not on line 1" files split into two kinds that must be handled **oppositely**:
+The non-happy-path files, by kind (handled **oppositely**):
 
 1. **Dual-format (3)** — YAML frontmatter on line 1 *and* an MVI marker at line 11:
    `core/standards/csharp.md`, `core/standards/csharp-project-structure.md`,
    `openagents-repo/quality/registry-dependencies.md`. These are the index's "3 YAML" files —
    but note they carry **both**, so a YAML-only parser still loses nothing here while an
-   MVI-only parser would need to look past the YAML block.
+   MVI-only parser would need to look past the YAML block. Their markers ARE metadata (they
+   sit in the leading window) and parse normally.
 2. **Marker-as-prose (4)** — the marker appears *deep in the body as documentation*, not as
    metadata: `openagents-repo/core-concepts/agents.md` (**line 232**),
    `openagents-repo/core-concepts/categories.md` (**line 301**),
@@ -585,7 +773,7 @@ The 7 "marker not on line 1" files split into two kinds that must be handled **o
 > ### 🚨 Two parser traps, both verified
 >
 > **Trap 1 — `gray-matter` silently drops everything.** A generic frontmatter parser finds **no
-> frontmatter** in 287/297 files, yielding `{}` metadata with **no error**. Priority drives
+> frontmatter** in 293 of 296 files (all but the 3 dual-format), yielding `{}` metadata with **no error**. Priority drives
 > context ordering, so this silently degrades every agent's context loading. This is the
 > single highest-risk parser requirement in the project.
 >
@@ -601,21 +789,28 @@ The 7 "marker not on line 1" files split into two kinds that must be handled **o
 
 ```
 parseContext(file):
+  0. If line 1 is exactly "<!-- oac:no-parse -->" → NO metadata parsing at all; whole file
+     is body (explicit opt-out for exemplar/template files).       [0 files today]
   1. If line 1 starts with "---" → parse the YAML block; remember where it closes.
      Set cursor = first non-blank line after the closing "---".  [3 files]
-     Else cursor = line 1.                                        [294 files]
+     Else cursor = line 1.                                        [293 files]
   2. If the line at `cursor` matches /^<!--\s*Context:\s*(.*?)\s*-->$/ →
-     parse it as MVI metadata (§4.3). Consume it; it is NOT body.  [287 + 3 files]
+     parse it as MVI metadata (§4.3). Consume it; it is NOT body.  [286 + 3 files]
      Else → NO metadata in the leading window.                     [7 files]
   3. NEVER scan beyond the leading window for a marker. Later markers are body. [Trap 2]
   4. Merge precedence when both YAML and MVI are present: MVI wins for the four MVI
      fields (it is the maintained convention); YAML supplies any extra keys.
   5. No metadata found → derive: id/name from path, priority = "medium" (schema default),
      and record a `doctor` finding. NEVER silently succeed with {} metadata.  [Trap 1]
-  6. Exclude from metadata parsing (template/standard exemplars, by path allowlist):
-     core/context-system/standards/templates.md, .../frontmatter.md.
 ```
 
+*(v3 — step 6's hardcoded path allowlist is deleted; 06-REVIEW G2 showed it was both brittle
+and redundant: all 4 marker-as-prose files — `templates.md:25`, `frontmatter.md:43`,
+`agents.md:232`, `categories.md:301` — carry their markers OUTSIDE the leading window, so
+steps 2–3 already treat them as body. The `<!-- oac:no-parse -->` first-line opt-out (step 0,
+per the 06-REVIEW Q5 disposition) is the forward mechanism for any future exemplar whose
+example marker would land in the leading window.)*
+
 The MVI line grammar is pipe-delimited `Key: Value`, tolerant of missing trailing fields:
 
 ```
@@ -637,44 +832,68 @@ demanding all four fails on real content.
 ### 4.3 ⚠️ Priority vocabulary — real data escapes the documented enum
 
 `.opencode/context/core/context-system/standards/frontmatter.md` documents
-`critical|high|medium|low`. Verified actual distribution across the parseable markers:
+`critical|high|medium|low`. Verified distribution across the **leading-window** markers
+(re-counted 2026-07-15, including the 3 dual-format files; 289 markers total):
 
 | Value | Count |
 |---|---|
 | `high` | 113 |
-| `critical` | 111 |
+| `critical` | 112 |
 | `low` | 34 |
 | `medium` | 29 |
 | **`reference`** | **1** ← outside the enum |
 
-The outlier is `core/workflows/lightweight-context-handoff-example.md`
-(`Priority: reference`). A strict `z.enum` **rejects a real file**. Options: (a) coerce
-`reference` → `low` on parse with a `doctor` warning; (b) fix the one file; (c) widen the enum.
-Recommend (b) + keep the strict enum — one-file fix, keeps the vocabulary closed. Raised in Open
-Questions.
+*(The index's v2.2 numbers — 112/111 — counted line-1 markers only; the two dual-format
+`critical` files and one dual-format `high` file sit at line 11. Any future recount must state
+which window it used.)*
 
-`Version` on disk is **X.Y**, not semver (verified: `1.0` ×271, `2.0` ×10, `1.1` ×4, plus `3.1`,
-`2.1`, `1.3`) → normalize `X.Y` → `X.Y.0` for `VersionSchema`.
+The outlier is `core/workflows/lightweight-context-handoff-example.md`
+(`Priority: reference` — still on disk as of 2026-07-15). A strict `z.enum` **rejects a real
+file**. **Decided (06-REVIEW disposition, v2 Q2 closed):** fix the one file → `low` at
+migration and keep the enum closed. Until that commit lands, the parser coerces `reference` →
+`low` with a `doctor` warning rather than failing the corpus.
+
+`Version` on disk is **X.Y**, not semver (re-verified 2026-07-15: `1.0` ×269, `2.0` ×13,
+`1.1` ×4, plus `1.3`, `2.1`, `3.1` — one each) → normalize `X.Y` → `X.Y.0` for `VersionSchema`.
+**Re-emit rule (v3 — fixes 06-REVIEW L9):** adapters serializing the MVI marker MUST re-emit
+the version in its **original on-disk form** (`2.0`, not `2.0.0`) — the IR keeps the raw
+string alongside the normalized semver — so a no-op build does not churn ~289 markers and
+`oac build && oac build` stays a no-op.
 
 ### 4.4 The Context IR
 
 ```ts
 export const ContextSchema = z.object({
   id: IdSchema,
-  name: NameSchema,
-  description: DescriptionSchema,
-  /** "{category}/{function}" from the MVI marker, e.g. "core/standards". Open string. */
+  aliases: z.array(IdSchema).default([]),    // NEW v3 — the 3 symlink/dup-id files land here
+  /** OPTIONAL/DERIVED (v3 — fixes 06-REVIEW G2 gap 1: v2 required both, but the MVI marker
+   *  carries NEITHER a name NOR a description, so every real context file failed
+   *  validation). When absent, derived: name = title-cased path basename; description =
+   *  first H1 or first non-empty body paragraph (truncated), else "{category} context:
+   *  {name}". Derivation happens at parse time so the IR object is always complete;
+   *  the FRONTMATTER fields stay optional. */
+  name: NameSchema.optional(),
+  description: DescriptionSchema.optional(),
+  /** SPLIT in v3 (fixes G2 gap 2, per 01 §"dual taxonomy" + checklist 01:1209): the marker's
+   *  compound "{category}/{function}" — e.g. "core/standards", "standards/code" — parses
+   *  into category = first segment, function = the rest. category stays an open string;
+   *  function carries the concepts/examples/guides/lookup/errors-style taxonomy instead of
+   *  being flattened into one string as v2 did. */
   category: CategorySchema,
+  function: z.string().optional(),
   tags: TagsSchema,
   /** From the MVI marker. NEUTRAL intent; HINT at every non-OAC target
    *  (contextPriority = none for claude/cursor/windsurf). Drives context ORDERING —
    *  losing it is the Trap 1 failure mode. */
   priority: PrioritySchema.default("medium"),
-  /** X.Y on disk → normalized to semver in memory (§4.3). */
+  /** X.Y on disk → normalized to semver in memory; raw form retained for re-emit (§4.3). */
   version: VersionSchema,
+  versionRaw: z.string().optional(),         // NEW v3 — original "X.Y" for the re-emit rule
   /** ISO date from "Updated:". Optional — real files omit it. HINT at tool layer. */
   updated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
   dependencies: z.array(DependencyInputSchema).default([]),
+  /** Applicability (NEW v3, §0.3). [] = all targets. */
+  targets: TargetsSchema,
 
   /** Markdown body, marker consumed. NEUTRAL INVARIANT. */
   body: z.string().min(1),
@@ -712,11 +931,15 @@ export const ToolParamSchema = z.object({
 
 export const ToolSchema = z.object({
   id: IdSchema,
+  aliases: z.array(IdSchema).default([]),  // NEW v3
   name: NameSchema,
   description: DescriptionSchema,        // NEUTRAL — the model needs to know what it does
   tags: TagsSchema,
   version: VersionSchema,
   dependencies: z.array(DependencyInputSchema).default([]),  // e.g. tool:env
+  /** Applicability (NEW v3, §0.3). Custom tools implement the OpenCode tool API and have no
+   *  CC primitive (01 Q18) — real tools declare ["opencode"]. */
+  targets: TargetsSchema,
 
   /** Entry module relative to the tool dir, e.g. "index.ts". NEUTRAL as a locator; whether a
    *  target can LOAD a custom tool is a HINT (OpenCode plugins vs Claude MCP). */
@@ -775,6 +998,9 @@ export const HookSchema = z.object({
   /** Optional filters (tool names/globs). HINT — matcher semantics vary; empty = always. */
   matchers: z.array(z.string()).default([]),
   actions: z.array(HookActionSchema).min(1),   // was `commands`
+  /** Applicability (NEW v3, §0.3). session-start.sh has no OpenCode equivalent — its hook
+   *  declares ["claude"] rather than emitting a warning on every other target. */
+  targets: TargetsSchema,
 });
 ```
 
@@ -784,61 +1010,119 @@ export const HookSchema = z.object({
 
 ---
 
-## 7. Registry entry
-
-Real source: `registry.json` v2.0.0 — `components: { agents, subagents, commands, tools, plugins,
-skills, contexts, config }`, each an array of `{ id, name, type, path, description, tags,
-dependencies, category, files? }`. This is the neutral catalog the CLI and build closure walk.
-
-Adds what the current registry lacks: **profiles** and **checksums** — the latter directly
-addresses index v2 finding #2 (dependency resolution is broken; `install.sh`'s resolver swallows
-`jq` errors via `|| echo ""`, so unknown refs yield **zero deps, no error**) and the
-bidirectional drift in finding #3.
+## 7. Registry entry — **rewritten v3 (fixes 06-REVIEW L5/L6/L7/G3)**
+
+Real source: `registry.json` — re-verified from disk **2026-07-15**: top-level keys
+`{ version: "2.0.0", schema_version: "2.0.0", repository, metadata, categories, components,
+profiles, subagents }`; `components: { agents 8, subagents 19, commands 17, tools 2, plugins 1,
+skills 4, contexts 194, config 3 }` = **248 entries** (the tree has moved since 06-REVIEW's
+245/191); each entry `{ id, name, type, path, description, tags, dependencies, category,
+files?, aliases? }` (3 context entries carry `aliases` today); `profiles` holds **5 named
+objects** — `essential` (25 components), `developer` (41, `badge: "RECOMMENDED"`), `business`
+(25), `full` (50), `advanced` (68, `additionalPaths: [".Building/", ".github/workflows/"]`).
+This is the neutral catalog the CLI and build closure walk.
+
+v2 deleted three things the corpus and `04` both depend on; v3 restores them:
+**`profiles` as a top-level object** (v2 kept only a per-entry tier enum, leaving nowhere for
+`additionalPaths`, `badge`, or a profile description — L5), **`categories`** (the OpenCode
+adapter is specified to emit `0-category.json`, whose `icon`/`order`/`status` source data the
+v2 IR could not hold — L6), and **`aliases[]`** (three live registry entries use it and
+`resolve_dependencies()` matches on it — L7).
 
 ```ts
-export const ComponentTypeSchema = z.enum([
-  "agent", "subagent", "command", "skill", "context", "tool", "hook", "plugin", "config",
-]);
+/** ONE kind vocabulary (fixes F2's five-vs-nine self-contradiction): the registry's component
+ *  types ARE the dependency kinds. */
+export const ComponentTypeSchema = DependencyKindSchema;   // §1.5 — nine kinds
+
+/** Section-key mapping (fixes G3 — "three docs assume someone else did"): the `components`
+ *  record is keyed by the PLURAL of the kind — agents, subagents, commands, skills, contexts,
+ *  tools, hooks, plugins — with ONE special case: `config` stays singular (matches disk;
+ *  parity with install.sh get_registry_key()). kind → section key is a total function:
+ *  k === "config" ? "config" : k + "s". */
 
 export const RegistryEntrySchema = z.object({
   id: IdSchema,
+  /** Alternate ids resolvable in dependency refs (RESTORED v3 — 3 live entries use it;
+   *  resolution rule in §1.5). Also the landing place for the 3 symlinked standards files
+   *  if the symlink→alias collapse is chosen (index v2.2 symlink finding). */
+  aliases: z.array(IdSchema).default([]),
   name: NameSchema,
   type: ComponentTypeSchema,
   /** Source path under /content/ (NOT .opencode/). NEUTRAL INVARIANT — catalog→source join. */
   path: z.string().min(1),
   description: DescriptionSchema,
   tags: TagsSchema,
-  category: CategorySchema,                                  // domain
-  /** Compact "kind:id" refs. Drives the build closure. An unresolvable ref MUST be a hard
-   *  error — never `|| echo ""` (index v2 finding #2). */
+  category: CategorySchema,                                  // domain ONLY (see callout)
+  /** Compact "kind:target" refs incl. wildcards (§1.5). Drives the build closure. An
+   *  unresolvable ref MUST be a hard error — never `|| echo ""` (index v2 finding #2). */
   dependencies: z.array(DependencyInputSchema).default([]),
   files: z.array(z.string()).default([]),                    // multi-file components
+  /** Applicability (NEW v3, §0.3). [] = all targets. */
+  targets: TargetsSchema,
 
   // ---- new in the refactor ----
-  /** Install tiers including this component. Replaces the overloaded registry `category`
-   *  tier with an explicit, multi-valued set. */
-  profiles: z.array(ProfileSchema).default(["standard"]),
   /** Content hash at publish time — drift/update detection. */
   checksum: z.string().optional(),
   version: VersionSchema,
 });
+// NOTE: v2's per-entry `profiles: [tier]` tag is WITHDRAWN — membership lives in
+// RegistrySchema.profiles.*.components[] (single owner; closes v2 Q9). The registry, not
+// component frontmatter, owns distribution.
+
+/** Distribution profile (RESTORED v3 as an object — matches registry.json on disk and the
+ *  shape 04 §2.2 reads: profiles.*.components[] and additionalPaths). */
+export const ProfileSchema = z.object({
+  name: NameSchema,
+  description: DescriptionSchema,
+  /** e.g. "RECOMMENDED" on `developer` (verified). Free string; HINT (install-UX only). */
+  badge: z.string().optional(),
+  /** Compact refs incl. wildcards — e.g. "agent:openagent", "context:core/*",
+   *  "config:agent-metadata" (all verified live in registry.json profiles). */
+  components: z.array(DependencyInputSchema),
+  /** Extra repo paths the profile installs. Verified: advanced = [".Building/",
+   *  ".github/workflows/"]. install.sh only PRINTED these; the CLI must copy them (04 §2.2). */
+  additionalPaths: z.array(z.string()).default([]),
+});
+
+/** Category descriptor (RESTORED v3, upgraded to objects per 01 §1.6 — the source data for
+ *  the OpenCode adapter's 0-category.json emit and for the current registry `categories`
+ *  tier descriptions). */
+export const CategoryInfoSchema = z.object({
+  name: NameSchema.optional(),           // absent → derive from key
+  description: DescriptionSchema,
+  icon: z.string().optional(),           // from 0-category.json (verified: "⚙️" etc.)
+  order: z.number().int().optional(),
+  status: z.string().optional(),
+});
 
 export const RegistrySchema = z.object({
   version: VersionSchema,
   schema_version: VersionSchema,
   repository: z.string().url().optional(),
-  targets: z.array(z.enum(["opencode", "claude", "cursor", "windsurf"])).default([
-    "opencode", "claude", "cursor", "windsurf",
-  ]),
+  targets: z.array(TargetSchema).default(["opencode", "claude", "cursor", "windsurf"]),
+  /** Keyed per the section-key mapping above (plural; `config` singular). */
   components: z.record(z.string(), z.array(RegistryEntrySchema)),
+  /** RESTORED v3. Keyed by profile id (essential | developer | business | full | advanced
+   *  today; open set). */
+  profiles: z.record(IdSchema, ProfileSchema).default({}),
+  /** RESTORED v3. Keyed by category id (open set — holds both today's tier descriptions and
+   *  the promoted 0-category.json domain entries). */
+  categories: z.record(z.string(), CategoryInfoSchema).default({}),
 });
 ```
 
 > **Too OpenCode-shaped — must change (Registry).**
 > - Every `path` starts with `.opencode/...` → rewrite to `/content/...`. `.opencode/` is a build
 >   target; it cannot also be the catalog's source of truth.
-> - `category` overloaded as *tier* (`essential|standard|…`) vs agent metadata's *domain*
->   (`core|development|…`) → split into `category` + `profiles` (index v2, confirmed).
+> - `category` overloaded as *tier* (`essential|standard|…` — the current on-disk `categories`
+>   map documents exactly that tier vocabulary) vs agent metadata's *domain*
+>   (`core|development|…`) → in v3 the entry `category` is **domain only**; tier membership is
+>   expressed solely by `profiles.*.components[]`. Migration maps old tier categories into
+>   profile membership.
+> - The orphan top-level `subagents` key (1 object, verified) → dissolved into
+>   `components.subagents` at migration.
+> - Version triplet on disk: `version` 2.0.0, `schema_version` 2.0.0, `metadata.schemaVersion`
+>   1.0.0 (re-verified) → one `schema_version` + one content `version` (01 Q21 default).
 > - Add `checksum` — without it `oac update` cannot detect the drift the index calls out as the
 >   core failure of the `cp`-based bridge.
 
@@ -856,8 +1140,10 @@ Every droppable field must be reported via `CapabilityMatrix` — never silently
 | **rule ordering** | **hint** | all but OpenCode (collapse discards order) | `granularPermissions` |
 | `decision: ask` | hint | claude/cursor/windsurf | `askPermissions` = none |
 | `capabilities.delegate` | invariant\* | cursor (none) | `taskDelegation` |
-| `inference.model` (null) | invariant | — (null ⇒ default) | `modelSelection` |
+| `inference.tier` (v3) | hint | any target without a fast/deep model mapping — drop + **warn** | `modelSelection` |
 | `inference.temperature` | hint | claude (none); cursor/windsurf (partial) | `temperatureControl` |
+| `targets` (v3) | **invariant** | — (skip-by-design produces no warning, §0.3) | — |
+| `aliases` (v3) | invariant (registry/build-closure); dropped at every tool layer silently (resolution artifact, not content) | — | — |
 | `inference.maxSteps` | hint | claude/cursor/windsurf | `maxSteps` |
 | `context[].path` | invariant | cursor (inline-only) | `externalContext` |
 | `context[].priority` | hint | claude/cursor/windsurf | `contextPriority` |
@@ -876,52 +1162,42 @@ not a soft warning.
 
 ---
 
+## Resolved in v3 (was: Open Questions — dispositions per 06-REVIEW triage)
+
+- ~~Q1 Precedence semantics~~ — **closed, already-answered** (06-REVIEW C1): the current index
+  adopts **last-match-wins** (§1.2.4). The index's own v2.2 caveat stands: primary verification
+  against a real OpenCode install is required before Stage 1 (an index/Stage-1 action item, not
+  a schema question — the shape is identical either way).
+- ~~Q2 `Priority: reference`~~ — **closed by default**: fix the one file → `low` at migration;
+  enum stays closed; parser coerces with a `doctor` warning until then (§4.3).
+- ~~Q3 `model` authorable?~~ — **RATIFIED: NO** (§0.4 blocker 2). `inference.tier` replaces it.
+- ~~Q4 census 296 vs 297~~ — **closed by measurement**: as of 2026-07-15 the tree holds
+  **296 `.md` path entries = 293 regular files + 3 symlinks** (§0.1; earlier 297 = 294 + 3 was
+  true of an earlier tree — it has moved).
+- ~~Q5 exemplar exclusion~~ — **closed by default**: the path allowlist is deleted; the
+  leading-window rule already covers all 4 current prose-marker files, and
+  `<!-- oac:no-parse -->` is the forward opt-out (§4.2).
+- ~~Q6 `web` capability~~ — **closed by default**: keep it in the closed set.
+- ~~Q8 emitted-file envelope~~ — **closed by default**: `ToolConfigSchema`/`ConversionResult`
+  (types.ts 253–316) move wholesale into the adapter contract (`03`/Agent C), out of
+  `packages/core`.
+- ~~Q9 profiles source of truth~~ — **closed**: the registry's `profiles.*.components[]` is the
+  sole owner (§7); no frontmatter self-declaration.
+- ~~Q10 two roles~~ — **closed by default**: `primary | subagent` locks at two.
+
 ## Open Questions
 
-1. **Precedence semantics — index prose vs. index example (§1.2.4). ⚠️ Needs ratification.**
-   The index says *"first-match-wins"*, but its own worked example and **all 34 real agents**
-   author broad-first/specific-after, under which naive first-match-wins denies coder-agent's
-   `router.sh` and downgrades openagent's `sudo *` deny to `ask`. **Recommend last-match-wins**
-   (makes the index example correct as written; maps 1:1 to OpenCode key order). Most-specific-wins
-   also works and agrees on 100% of current data. **The schema shape is identical under all three**
-   — only the resolver differs — so this can be ratified without blocking any workstream.
-
-2. **`Priority: reference` escapes the documented enum (§4.3).** One real file
-   (`core/workflows/lightweight-context-handoff-example.md`) uses a value the standard doesn't
-   define, so a strict `z.enum` rejects real content. Coerce→`low` with a warning, fix the one
-   file, or widen the enum? *Recommend fixing the file and keeping the enum closed.*
-
-3. **Should `model` be authorable in `/content/` at all?** (Carried from v1; still open per index
-   decision #2.) Since content must never hardcode a model, `inference.model` could be dropped
-   from the *authored* schema entirely and exist only as an always-null IR field plus a build-time
-   user/project override layer. Keeping it authorable invites the exact violation #2 forbids.
-
-4. **Context census delta: 296 (index) vs 297 (my count) (§0.1).** My reproducible count over
-   `.opencode/context/**/*.md` gives 297 (287 marker-on-line-1 / 7 marker-elsewhere / 3 no-marker).
-   Likely a scope difference in what was enumerated. No design impact — but the numbers should be
-   reconciled before they're quoted in shipping docs.
-
-5. **Template/standard exemplar exclusion (§4.2 step 6).** `templates.md` and `frontmatter.md`
-   contain placeholder/example markers that must not be parsed as metadata. Is a hardcoded path
-   allowlist acceptable, or should such files carry an explicit opt-out marker (e.g.
-   `<!-- oac:no-parse -->`)? A path allowlist is brittle as content grows.
-
-6. **`web` as a first-class capability?** No current agent uses a `web` permission; included
-   because Claude has WebFetch/WebSearch and it is a real intent. Confirm it belongs in the closed
-   capability set, or defer until a target needs it.
-
-7. **`id` vs `path` in `ContextRefSchema` and dependency joins.** Require registry `id`s (fully
-   neutral, but forces every one of ~297 contexts into the registry) or keep allowing relative
-   paths (flexible, weaker neutral anchor)? Affects how strict the build closure can be — and
-   interacts with index v2 finding #2 (unresolvable refs must hard-error).
-
-8. **Where does the emitted-file envelope live?** `types.ts` `ToolConfigSchema`/`ConversionResult`
-   (253–316) are adapter outputs, not IR. Confirm they move wholesale into Agent C's adapter
-   contract and out of `packages/core`.
-
-9. **Profiles source of truth.** Proposed on the registry entry (§7). Should agents/skills also
-   self-declare a profile in frontmatter (author-controlled but duplicated), or is the registry
-   sole owner (single source, but authors can't express intent locally)?
-
-10. **`primary|subagent` — are two roles enough?** Locking to two matches Claude + the worked
-    example. Flag before the enum freezes if any target needs a third (e.g. background/always-on).
+1. **`id` vs `path` in `ContextRefSchema` and dependency joins.** *(was Q7 — still open.)*
+   Require registry `id`s (fully neutral, but forces every one of ~296 contexts into the
+   registry — 194 are registered today) or keep allowing relative paths (flexible, weaker
+   neutral anchor)? Affects how strict the build closure can be — and interacts with index v2
+   finding #2 (unresolvable refs must hard-error).
+
+2. **Wildcard refs on non-`context` kinds (§1.5).** The grammar permits `agent:core/*` etc.;
+   only `context:` wildcards exist in the corpus (7, all verified). Should `doctor` hard-limit
+   wildcards to `context:` until another kind defines an expansion base tree, or is the
+   general rule fine with a warning?
+
+3. **Handoff — the 8 mixed-without-`*` permission blocks (§1.2.5).** Not a schema question
+   (the rule is ratified), but the one-line-per-block disambiguation migration needs an owner
+   in the Stage-3 seed/merge spec (doc `09`); recorded here so it is not lost.