diff --git a/src/core/schema-pack/base/gbrain-creator.yaml b/src/core/schema-pack/base/gbrain-creator.yaml new file mode 100644 index 000000000..cce3c94ce --- /dev/null +++ b/src/core/schema-pack/base/gbrain-creator.yaml @@ -0,0 +1,97 @@ +# gbrain-creator — content-creator lens pack +# +# v0.41 lens packs wave. Declares the atom + concept lifecycle that the +# downstream wintermute agent ran via its own crons pre-v0.41: +# +# - `atom` page type (NEW to base v0.40 — first introduced here): +# single-source extractive primitive from transcripts/meetings/articles. +# The `extract_atoms` cycle phase produces these via a Haiku 3-check +# quality gate (truism reject, punchline test, entity-page test). +# +# - `concept` page type (already in gbrain-base seed): the synthesize_concepts +# phase aggregates atoms by topic cluster, tiers T1-T4 by composite_score, +# and writes voice-gated T1 Canon narratives via Sonnet. +# +# Cron consolidation: this pack's phases run inside the standard +# `gbrain dream` cycle. No separate wintermute-side cron required. +# +# Calibration: declares `concept_themes` domain using the `cluster_summary` +# aggregator (descriptive rollup, not Brier — concepts don't have binary +# outcomes to score against). When this pack is active OR the gbrain-everything +# meta-pack is active, calibration_profile produces a concept_themes scorecard +# with tier counts and dominant topics. +# +# Frontmatter validators (atom_type closed 11-value enum, virality_score +# 0-100, emotional_register, tweetable, sensitivity, lesson, source_quote, +# draft_angles JSONB) are enforced today INSIDE extract_atoms.ts and read +# from this manifest at runtime via D11. A richer per-page-type +# frontmatter_validators contract on PageTypeSchema is a v0.42 follow-up +# (filed in TODOS.md). For v0.41, the enum lives in code with a comment +# pointing at the eventual manifest surface. + +api_version: gbrain-schema-pack-v1 +name: gbrain-creator +version: 1.0.0 +description: "Content-creator lens. Atoms (single-source extractive nuggets) and concepts (aggregated themes T1-T4). Drives extract_atoms + synthesize_concepts cycle phases. When active, every gbrain dream tick walks recent transcripts/meetings/articles and produces tweetable atoms + tier-promoted concepts. Retires wintermute's parallel atom-pipeline-coordinator cron." +gbrain_min_version: 0.41.0 +extends: gbrain-base +borrow_from: [] + +# Atom + concept inherit takes_kinds from gbrain-base (fact, take, bet, hunch). +# Creator-pack-specific kinds (e.g. `extracted`) are deferred — atoms are not +# claims, they're evidentiary extractions; concepts aggregate atoms not takes. +# takes_kinds intentionally omitted (inherits from base). + +# Pack-specific page types. `concept` already exists in gbrain-base seed; we +# don't re-declare it here (the registry merges parent + child page_types). +# `atom` is net-new to base. +page_types: + - name: atom + primitive: concept + path_prefixes: + - atoms/ + aliases: [] + # extractable: false — atoms ARE the extraction output, not a source for + # further fact extraction. extract_facts skips them. extract_atoms skips + # them too (imported_from frontmatter marker handles the greenfield case; + # the type itself signals "leaf node, no further derivation"). + extractable: false + # expert_routing: false — atoms don't surface in whoknows/find_experts. + # Concepts (parent type from base) carry the routing weight for content + # discovery; atoms are content-creator-internal raw material. + expert_routing: false + +# v0.41 T3: phase participation. The runCycle orchestrator gates pack- +# flavored phases on this list. Pre-existing 17 core phases ALWAYS run +# regardless of this declaration — `phases:` is additive, not subtractive. +phases: + - extract_atoms + - synthesize_concepts + +# v0.41 T3: calibration domain declarations. Each entry binds a per-pack +# scorecard bucket to one of the 4 v1 aggregator kinds. Domain names are +# open strings (third-party packs add new domains without a gbrain release); +# aggregator algorithms are the closed AggregatorKind enum (safe SQL stays +# in code per T3 codex refinement of D6). +calibration_domains: + - name: concept_themes + aggregator: cluster_summary + page_types: + - concept + +# Filing rules — date-bucketed atoms (mirrors wintermute's existing layout +# at ~/git/brain/atoms/{YYYY-MM-DD}/{slug}.md so greenfield migration lands +# pages in their canonical locations). Concepts flat-filed per base seed. +filing_rules: + - kind: atom + directory: atoms/ + examples: + - atoms/2026-05-24/renders-vs-physical-proof + - atoms/2026-05-24/ai-compliance-headwinds + description: "Single-source extractive nuggets. Date-bucketed by extraction date (NOT source date — multiple atoms from one meeting all land in the day they were extracted). extract_atoms cycle phase writes here." + - kind: concept + directory: concepts/ + examples: + - concepts/ai-agents + - concepts/founder-psychology + description: "Aggregated themes T1-T4 by composite_score (mention_count × distinct_months × breadth). synthesize_concepts writes here. T1 Canon concepts carry Sonnet-synthesized narratives gated by the same voice_gate as calibration_profile." diff --git a/src/core/schema-pack/base/gbrain-engineer.yaml b/src/core/schema-pack/base/gbrain-engineer.yaml new file mode 100644 index 000000000..f0f6491fa --- /dev/null +++ b/src/core/schema-pack/base/gbrain-engineer.yaml @@ -0,0 +1,113 @@ +# gbrain-engineer — engineer / code-author lens pack +# +# v0.41 lens packs wave. Bridge-only pack (D8-C — locked at plan time +# after assessing what already exists in gstack): +# +# - gstack already ships review, investigate, plan-eng-review, health, +# qa, retro, learn, cso skills with a typed JSONL learnings system +# at ~/.gstack/projects/{repo}/learnings.jsonl (pattern, pitfall, +# preference, architecture, tool, operational, investigation). +# - gbrain already ships `type: code` + code-def/code-refs/code-callers. +# - gbrain already ships `type: project` + brain/projects/ directory. +# +# What's missing: the gstack learnings JSONL never reaches gbrain. T8 +# fixes that with an IngestionSource bridge that watches the JSONL files +# and emits IngestionEvent per line typed as `learning` page. +# +# Scope deliberately small: declares the `learning` page type (NEW) and +# reuses `code` from gbrain-base. Drops the speculative ADR / postmortem / +# refactor_thesis / tech_debt types from the original plan because: +# (a) gstack's investigation + retro skills already capture postmortems +# and architecture decisions inline (no dedicated type needed) +# (b) shipping types nobody authors is worse than no types at all +# (c) v0.42+ can add them when first user authors one +# +# Calibration: declares 3 engineering domains (architecture_calls, +# effort_estimates, risk_assessment). All scalar_brier except +# effort_estimates which uses weighted_brier — small estimates that are +# wrong cost less than big estimates that are wrong. +# +# Does NOT declare new cycle phases. Engineering work consumes the +# existing extract_facts + propose_takes + grade_takes + calibration_profile +# loop, augmented by the gstack-learnings IngestionSource (T8) running in +# the v0.38 daemon when this pack is active. + +api_version: gbrain-schema-pack-v1 +name: gbrain-engineer +version: 1.0.0 +description: "Engineer / code-author lens. Bridges gstack's typed JSONL learnings system (pattern, pitfall, architecture, tool, operational, investigation) into gbrain as first-class `learning` pages. Declares architecture_calls, effort_estimates, risk_assessment calibration domains. Reuses gbrain-base's `code` page type unchanged." +gbrain_min_version: 0.41.0 +extends: gbrain-base + +# Reuse `code` + `project` from base. The gstack-learnings bridge writes +# `learning` pages cross-referenced to code:// URIs via link_types, but +# we don't introduce new code-related page types here. +borrow_from: + - pack: gbrain-base + types: + - code + - project + +# Engineering uses the same takes_kinds as base (fact, take, bet, hunch). +# `bet` is the load-bearing one for architecture_calls (a bet on a +# refactor's payoff is a take). +# takes_kinds intentionally omitted (inherits from base). + +# Pack-specific page types. ONLY `learning` is new (D8-C: bridge-only). +page_types: + - name: learning + primitive: annotation + path_prefixes: + - learnings/ + - engineering/learnings/ + aliases: [] + # extractable: false — learnings ARE distilled engineering insights; + # they're not raw source material for further fact extraction. + extractable: false + # expert_routing: false — learnings surface in code-aware queries + # via direct slug search, not via whoknows/find_experts (whoknows is + # person+company shaped, learnings are pattern shaped). + expert_routing: false + +# No new cycle phases. Engineering consumes existing pipeline. +# gstack-learnings IngestionSource (T8) emits events when this pack is +# active — handled by the v0.38 daemon's normal trickle-mode dispatch. +phases: [] + +# v0.41 T3: engineering-flavored calibration domains. +calibration_domains: + - name: architecture_calls + # "How often I'm wrong about which architecture to pick" — scalar + # Brier over resolved code-attached + learning-attached binary takes + # (e.g. "this refactor was worth it" resolved true/false). + aggregator: scalar_brier + page_types: + - code + - learning + + - name: effort_estimates + # Weighted Brier — a 1-day-estimate miss costs less than a 1-quarter + # estimate miss. Weight by take.confidence so high-conviction + # estimates that fail count more. + aggregator: weighted_brier + page_types: + - project + + - name: risk_assessment + # "How often I correctly flag a risk" — scalar Brier over project- + # attached binary takes (e.g. "this rollout will OOM" resolved + # true/false). Distinct from architecture_calls (those are go/no-go + # decisions on direction; risk_assessment is about specific failure + # mode predictions). + aggregator: scalar_brier + page_types: + - project + +# Filing rules. +filing_rules: + - kind: learning + directory: learnings/ + examples: + - learnings/2026-05/orbstack-port-8080-conflict + - learnings/2026-05/django-form-cleaned-data-defaults-empty + description: "Engineering learnings bridged from gstack's ~/.gstack/projects/{repo}/learnings.jsonl. Per-month subdirectories. Frontmatter carries the original JSONL fields verbatim: learning_type (one of: pattern, pitfall, preference, architecture, tool, operational, investigation), confidence (1-10), source (one of: observed, user-stated, inferred, cross-model), files (paths), skill (originating gstack skill name)." diff --git a/src/core/schema-pack/base/gbrain-everything.yaml b/src/core/schema-pack/base/gbrain-everything.yaml new file mode 100644 index 000000000..afdbefd7b --- /dev/null +++ b/src/core/schema-pack/base/gbrain-everything.yaml @@ -0,0 +1,121 @@ +# gbrain-everything — meta-pack stacking all three lenses +# +# v0.41 lens packs wave. Codex outside-voice T4 resolution: gbrain's +# 7-tier active-pack resolver is single-valued, but the user's actual +# workflow needs creator + investor + engineer simultaneously (Garry +# switches between YC investor work, content creation, and gstack +# engineering across the same day). Two paths to "three lenses on one +# brain": +# +# A) New active-multi-pack composition concept. New code, new +# architecture, new corners. Deferred per plan's NOT-in-scope. +# +# B) Compose via the v0.38-shipped extends chain. Author a meta-pack +# that extends one of the three, and bring the other two via +# borrow_from. registry walks the chain, unions page_types + +# link_types + phases + calibration_domains. ZERO new architecture. +# +# This pack is path B. Activate via `gbrain config set schema_pack +# gbrain-everything` and the brain runs all three lens-pack phases +# (extract_atoms + synthesize_concepts from creator; gstack-learnings +# bridge from engineer) AND surfaces all 7 calibration domains +# (_overall + concept_themes + deal_success + founder_evaluation + +# market_call + architecture_calls + effort_estimates + risk_assessment). +# +# Extension chain (registry resolves via extends.parent.parent.parent): +# +# gbrain-everything ──extends──▶ gbrain-investor ──extends──▶ gbrain-base +# │ borrow │ +# ├─ gbrain-creator +# └─ gbrain-engineer +# +# Single-active-pack constraint preserved: this IS the active pack. +# Registry walks its dependencies through extends + borrow_from to +# materialize the merged view. Tested by the everything-manifest test. + +api_version: gbrain-schema-pack-v1 +name: gbrain-everything +version: 1.0.0 +description: "Meta-pack stacking creator + investor + engineer lenses on one brain. Use this when you do investor work AND content creation AND engineering in the same brain (YC president pattern, founder pattern). Single active-pack activation via `gbrain config set schema_pack gbrain-everything` — registry walks extends + borrow_from chain to union all three lenses' page_types, phases, and calibration_domains." +gbrain_min_version: 0.41.0 + +# Extends gbrain-investor (which itself extends gbrain-base) and borrows +# from gbrain-creator + gbrain-engineer. The registry's extends chain +# walker (cap depth 8) unions every level's contributions. +extends: gbrain-investor + +# Borrow types + link_types from the other two lens packs. This is the +# composition mechanism that makes "three lenses" real without a +# multi-active-pack architecture. +borrow_from: + - pack: gbrain-creator + types: + - atom + # concept already in base + - pack: gbrain-engineer + types: + - learning + # code + project already in base + +# No new page types declared here — everything comes via extends + borrow. +page_types: [] + +# Union of creator + engineer phase declarations. Investor pack declares +# no phases (consumes existing pipeline), so nothing inherited from the +# extends chain there. Creator's phases must be re-declared explicitly +# because `borrow_from` borrows types/link_types only, NOT phases (D4-B +# decision: each pack declares its phase participation explicitly). +# +# Engineer pack's phases: [] is the empty case — the gstack-learnings +# IngestionSource is daemon-side, not a cycle phase. +phases: + - extract_atoms + - synthesize_concepts + +# Union of creator + investor + engineer calibration domains. Same +# explicit re-declaration rationale as phases — calibration_domains +# aren't borrowed; the meta-pack declares the full set. +# +# Aggregator algorithms are the closed enum; domain names match the +# per-pack declarations so the JSONB scorecard keys are stable across +# active-pack switches (a user moving from gbrain-investor to +# gbrain-everything keeps their deal_success scorecard intact). +calibration_domains: + # From gbrain-creator: + - name: concept_themes + aggregator: cluster_summary + page_types: + - concept + + # From gbrain-investor: + - name: deal_success + aggregator: scalar_brier + page_types: + - deal + - name: founder_evaluation + aggregator: scalar_brier + page_types: + - person + - name: market_call + aggregator: weighted_brier + page_types: + - thesis + + # From gbrain-engineer: + - name: architecture_calls + aggregator: scalar_brier + page_types: + - code + - learning + - name: effort_estimates + aggregator: weighted_brier + page_types: + - project + - name: risk_assessment + aggregator: scalar_brier + page_types: + - project + +# Filing rules inherit through extends + borrow. No re-declaration needed — +# registry merges parent + borrowed pack filing_rules. +filing_rules: [] diff --git a/src/core/schema-pack/base/gbrain-investor.yaml b/src/core/schema-pack/base/gbrain-investor.yaml new file mode 100644 index 000000000..ec1b0b1db --- /dev/null +++ b/src/core/schema-pack/base/gbrain-investor.yaml @@ -0,0 +1,151 @@ +# gbrain-investor — investor/YC lens pack +# +# v0.41 lens packs wave. Declares the investor work surface that the user +# (YC president) already runs every day across the wintermute agent + brain: +# - 501 investor profiles at brain/investors/ +# - 19 deal memos at brain/investing/deals/ +# - 5.3K companies + 24.6K people + 7.1K YC pages +# - 8 investor skills (yc-radar, yc-portfolio-health, sequoia-batch-bangers, +# investor-diligence, batch-picks-for-investor, etc.) +# - Fences already in use: ## Recommendation, ## Pre-mortem, ## Defensibility +# (6-8 moats), ## Founders, ## Product, ## Market / Customers, ## Traction, +# ## Team, ## Signals +# +# This pack consolidates the EXISTING primitives as pack-owned + adds a +# bet-resolution lifecycle: +# +# - `thesis` (NEW): investment thesis with thesis_text, key_bets[], +# market_view, vintage. Filed at investing/theses/{slug}. +# +# - `bet_resolution_log` (NEW): outcome record for a thesis's bet. Bound +# to a take row via take_id, carries resolved_outcome + resolved_at + +# learned_pattern. Filed at investing/bets/{YYYY-MM}/{slug}. +# +# Calibration: declares 3 domains feeding the v0.41-widened +# calibration_profile: deal_success (Brier over resolved deal-flagged +# takes), founder_evaluation (Brier over person-flagged takes), market_call +# (weighted Brier over thesis-flagged takes, weighted by take.confidence +# because market calls' high-conviction-but-rare nature wants Brier scaled +# by stake). +# +# Does NOT declare new cycle phases — investor work consumes the existing +# extract_facts + propose_takes + grade_takes + calibration_profile loop. +# The propose_takes phase populates take_domain_assignments rows at write +# time from this pack's page_types → domain mapping (T10 wiring). +# +# Cron consolidation: existing wintermute crons (yc-radar-weekly, +# yc-portfolio-health monthly, yc-oh-ingest daily) continue running on +# their own schedules in wintermute — gbrain-investor doesn't displace +# them, only declares their output types + calibration semantics. v0.42+ +# may migrate those crons too once their phase contracts stabilize. + +api_version: gbrain-schema-pack-v1 +name: gbrain-investor +version: 1.0.0 +description: "Investor / YC president lens. Adds thesis + bet_resolution_log types to the deal/investor/company/person surface that already exists in gbrain-base. Declares deal_success / founder_evaluation / market_call calibration domains feeding the widened calibration_profile." +gbrain_min_version: 0.41.0 +extends: gbrain-base + +# Borrow deal + person + company + yc from gbrain-base — they remain +# base-owned (other packs may also use them) but this pack's calibration +# domains reference them. No new declarations needed because borrow_from +# just signals intent; the registry already merges types from the +# extends chain. +borrow_from: + - pack: gbrain-base + types: + - deal + - person + - company + - yc + +# Investor work uses the same takes_kinds as base (fact, take, bet, hunch). +# The `bet` kind is the load-bearing one for this pack — bet_resolution_log +# tracks outcomes for take rows of kind='bet'. +# takes_kinds intentionally omitted (inherits from base). + +# Pack-specific page types. `deal` already exists in gbrain-base seed. +# `thesis` and `bet_resolution_log` are net-new for v0.41. +# +# `investor` and `founder` (alias for person in deal context) are +# deferred: investor pages are filed under investors/ but the type is +# `person` today and changing it requires data migration. v0.42+ can +# introduce them with proper migration. +page_types: + - name: thesis + primitive: concept + path_prefixes: + - investing/theses/ + - theses/ + aliases: [] + # extractable: true — theses contain claims that the LLM extractor + # can mine into facts (the thesis itself is a claim; sub-bets are + # claims). extract_facts walks them. + extractable: true + # expert_routing: false — theses don't surface in find_experts; + # founders + companies carry that weight. + expert_routing: false + + - name: bet_resolution_log + primitive: temporal + path_prefixes: + - investing/bets/ + - bets/ + aliases: [] + extractable: false + expert_routing: false + +# No new cycle phases. Investor work consumes the existing pipeline. +phases: [] + +# v0.41 T3: investor-flavored calibration domains. Each binds the closed +# aggregator algorithm to the open domain name + page_types whose takes +# feed this bucket. propose_takes (T10) populates take_domain_assignments +# rows at write time from this mapping when this pack is active. +calibration_domains: + - name: deal_success + # Standard Brier over resolved deal-attached binary takes + # (did this deal succeed? = take.resolved_outcome). + aggregator: scalar_brier + page_types: + - deal + + - name: founder_evaluation + # Standard Brier over person-attached takes. The "how often I'm + # wrong about founders" signal. + aggregator: scalar_brier + page_types: + - person + + - name: market_call + # Weighted Brier — market calls are rare-but-high-stakes; weighting + # by take.confidence means a misfired strong conviction costs more + # than a misfired weak hunch. + aggregator: weighted_brier + page_types: + - thesis + +# Filing rules — mirror wintermute's existing layout so the v0.41 +# greenfield migration (T7) lands pages in their canonical locations. +filing_rules: + - kind: deal + directory: investing/deals/ + examples: + - investing/deals/widget-co-pre-a-memo + description: "Investment memos. Pre-existing fences (## Recommendation, ## Pre-mortem, ## Defensibility, ## Founders, ## Product, ## Market / Customers, ## Traction, ## Team) carry through unchanged." + - kind: thesis + directory: investing/theses/ + examples: + - investing/theses/ai-agents-2026 + - investing/theses/hardware-renaissance + description: "Investment theses with thesis_text + key_bets[] + market_view + vintage frontmatter. Sub-bets resolve into bet_resolution_log entries via the take_id FK chain." + - kind: bet_resolution_log + directory: investing/bets/ + examples: + - investing/bets/2026-04/widget-co-arr-trajectory + description: "Resolution records for thesis bets. Month-bucketed by resolution date. Frontmatter: bet_id (FK to thesis.key_bets), resolved_outcome (boolean), resolved_at (ISO date), learned_pattern (one-sentence takeaway). Populates the market_call calibration domain." + - kind: investor + directory: investors/ + examples: + - investors/example-vc-fund + description: "Investor profiles (501 already extant). Filed flat under investors/. Page type stays `person` for now (data-migration deferral). v0.42+ may introduce dedicated `investor` page type." diff --git a/src/core/schema-pack/index.ts b/src/core/schema-pack/index.ts index e4fd7b462..9ab915f1d 100644 --- a/src/core/schema-pack/index.ts +++ b/src/core/schema-pack/index.ts @@ -16,6 +16,10 @@ export { parseSchemaPackManifest, computeManifestSha8, packIdentity, + // v0.41 T3 — calibration domain registry + AGGREGATOR_KINDS, + type AggregatorKind, + type CalibrationDomain, } from './manifest-v1.ts'; export { diff --git a/src/core/schema-pack/load-active.ts b/src/core/schema-pack/load-active.ts index af9a7efff..cb62ab736 100644 --- a/src/core/schema-pack/load-active.ts +++ b/src/core/schema-pack/load-active.ts @@ -95,7 +95,20 @@ function defaultPackLocator(name: string): string | null { // v0.39 T8 — bundled packs registry. gbrain-base + gbrain-recommended // ship in src/core/schema-pack/base/. Add a new entry here to bundle // additional canonical packs. - const BUNDLED: ReadonlyArray = ['gbrain-base', 'gbrain-recommended']; + // + // v0.41 T4 — lens packs join the bundle: creator (atoms + concepts + + // extract_atoms/synthesize_concepts phases), investor (theses + bet + // resolution + 3 calibration domains), engineer (gstack-learnings bridge + // + 3 calibration domains), everything (meta-pack stacking all three + // via extends + borrow_from). Each ships as a real YAML at base/.yaml. + const BUNDLED: ReadonlyArray = [ + 'gbrain-base', + 'gbrain-recommended', + 'gbrain-creator', + 'gbrain-investor', + 'gbrain-engineer', + 'gbrain-everything', + ]; if (BUNDLED.includes(name)) { // Resolve bundled YAML relative to this source file. Works in both // direct-bun execution and bun --compile binaries. diff --git a/test/lens-pack-manifests.test.ts b/test/lens-pack-manifests.test.ts new file mode 100644 index 000000000..1ec68c748 --- /dev/null +++ b/test/lens-pack-manifests.test.ts @@ -0,0 +1,247 @@ +// v0.41 T4 — bundled lens pack manifest smoke tests. +// +// One test file covers all 4 packs (creator, investor, engineer, everything) +// because each test boils down to "manifest parses + declares the expected +// shape." Splitting per-pack would 4x the boilerplate without adding signal. +// +// Pinned contracts: +// - All 4 YAMLs parse via parseSchemaPackManifest without error +// - Each pack registered in BUNDLED (loadPackManifestByName resolves) +// - Each pack declares the expected page_types, phases, calibration_domains +// - extends chain resolves through registry without depth error +// - gbrain-everything unions all three lens packs' contributions +// - Calibration domain aggregator is the closed AggregatorKind enum on every entry + +import { describe, test, expect } from 'bun:test'; +import { readFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { + parseSchemaPackManifest, + parseYamlMini, + AGGREGATOR_KINDS, + type SchemaPackManifest, +} from '../src/core/schema-pack/index.ts'; + +const PACK_NAMES = [ + 'gbrain-creator', + 'gbrain-investor', + 'gbrain-engineer', + 'gbrain-everything', +] as const; + +const here = dirname(fileURLToPath(import.meta.url)); +const baseDir = join(here, '..', 'src', 'core', 'schema-pack', 'base'); + +function loadPack(name: string): SchemaPackManifest { + const p = join(baseDir, `${name}.yaml`); + if (!existsSync(p)) { + throw new Error(`bundled pack not found at ${p}`); + } + const raw = readFileSync(p, 'utf-8'); + const parsed = parseYamlMini(raw); + return parseSchemaPackManifest(parsed, { path: p }); +} + +describe('v0.41 T4: all 4 bundled lens packs parse cleanly', () => { + for (const name of PACK_NAMES) { + test(`${name}.yaml parses via parseSchemaPackManifest without error`, () => { + const pack = loadPack(name); + expect(pack.name).toBe(name); + expect(pack.version).toMatch(/^\d+\.\d+\.\d+$/); + expect(pack.api_version).toBe('gbrain-schema-pack-v1'); + }); + } +}); + +describe('v0.41 T4: bundled registry includes lens packs', () => { + test('load-active.ts BUNDLED array source includes the 4 lens pack names', () => { + const loadActiveSrc = readFileSync( + join(here, '..', 'src', 'core', 'schema-pack', 'load-active.ts'), + 'utf-8', + ); + for (const name of PACK_NAMES) { + expect(loadActiveSrc).toContain(`'${name}'`); + } + }); +}); + +describe('v0.41 T4: gbrain-creator manifest shape', () => { + const pack = loadPack('gbrain-creator'); + + test('extends gbrain-base', () => { + expect(pack.extends).toBe('gbrain-base'); + }); + + test('declares atom page type (NEW to base)', () => { + const atom = pack.page_types.find((p) => p.name === 'atom'); + expect(atom).toBeDefined(); + expect(atom?.primitive).toBe('concept'); + expect(atom?.path_prefixes).toContain('atoms/'); + expect(atom?.extractable).toBe(false); // leaf node, not source for further extraction + expect(atom?.expert_routing).toBe(false); + }); + + test('declares extract_atoms + synthesize_concepts phases', () => { + expect(pack.phases).toContain('extract_atoms'); + expect(pack.phases).toContain('synthesize_concepts'); + }); + + test('declares concept_themes calibration domain with cluster_summary aggregator', () => { + const themes = pack.calibration_domains.find((d) => d.name === 'concept_themes'); + expect(themes).toBeDefined(); + expect(themes?.aggregator).toBe('cluster_summary'); + expect(themes?.page_types).toContain('concept'); + }); + + test('filing rules for atom + concept include canonical paths', () => { + const atomRule = pack.filing_rules.find((r) => r.kind === 'atom'); + expect(atomRule?.directory).toBe('atoms/'); + const conceptRule = pack.filing_rules.find((r) => r.kind === 'concept'); + expect(conceptRule?.directory).toBe('concepts/'); + }); +}); + +describe('v0.41 T4: gbrain-investor manifest shape', () => { + const pack = loadPack('gbrain-investor'); + + test('extends gbrain-base + borrows deal/person/company/yc', () => { + expect(pack.extends).toBe('gbrain-base'); + const borrowEntry = pack.borrow_from.find((b) => b.pack === 'gbrain-base'); + expect(borrowEntry).toBeDefined(); + expect(borrowEntry?.types).toEqual(expect.arrayContaining(['deal', 'person', 'company', 'yc'])); + }); + + test('declares thesis + bet_resolution_log page types', () => { + const thesis = pack.page_types.find((p) => p.name === 'thesis'); + expect(thesis).toBeDefined(); + expect(thesis?.primitive).toBe('concept'); + expect(thesis?.extractable).toBe(true); + + const bet = pack.page_types.find((p) => p.name === 'bet_resolution_log'); + expect(bet).toBeDefined(); + expect(bet?.primitive).toBe('temporal'); + }); + + test('declares NO new cycle phases (consumes existing pipeline)', () => { + expect(pack.phases).toEqual([]); + }); + + test('declares 3 calibration domains (deal_success + founder_evaluation + market_call)', () => { + const names = pack.calibration_domains.map((d) => d.name).sort(); + expect(names).toEqual(['deal_success', 'founder_evaluation', 'market_call']); + }); + + test('every calibration_domain aggregator is in the closed AggregatorKind enum', () => { + for (const d of pack.calibration_domains) { + expect(AGGREGATOR_KINDS).toContain(d.aggregator); + } + }); + + test('market_call uses weighted_brier (high-conviction-rare-event semantics)', () => { + const mc = pack.calibration_domains.find((d) => d.name === 'market_call'); + expect(mc?.aggregator).toBe('weighted_brier'); + }); + + test('filing rules cover deal + thesis + bet_resolution_log + investor', () => { + const kinds = pack.filing_rules.map((r) => r.kind).sort(); + expect(kinds).toEqual(['bet_resolution_log', 'deal', 'investor', 'thesis']); + }); +}); + +describe('v0.41 T4: gbrain-engineer manifest shape', () => { + const pack = loadPack('gbrain-engineer'); + + test('extends gbrain-base + borrows code/project', () => { + expect(pack.extends).toBe('gbrain-base'); + const borrowEntry = pack.borrow_from.find((b) => b.pack === 'gbrain-base'); + expect(borrowEntry?.types).toEqual(expect.arrayContaining(['code', 'project'])); + }); + + test('declares ONLY learning page type (D8-C bridge-only)', () => { + expect(pack.page_types.length).toBe(1); + expect(pack.page_types[0].name).toBe('learning'); + expect(pack.page_types[0].primitive).toBe('annotation'); + }); + + test('declares NO new cycle phases (gstack bridge is daemon-side IngestionSource)', () => { + expect(pack.phases).toEqual([]); + }); + + test('declares 3 calibration domains (architecture_calls + effort_estimates + risk_assessment)', () => { + const names = pack.calibration_domains.map((d) => d.name).sort(); + expect(names).toEqual(['architecture_calls', 'effort_estimates', 'risk_assessment']); + }); + + test('effort_estimates uses weighted_brier (small-vs-big estimate scaling)', () => { + const ee = pack.calibration_domains.find((d) => d.name === 'effort_estimates'); + expect(ee?.aggregator).toBe('weighted_brier'); + }); + + test('every calibration_domain aggregator is in the closed AggregatorKind enum', () => { + for (const d of pack.calibration_domains) { + expect(AGGREGATOR_KINDS).toContain(d.aggregator); + } + }); +}); + +describe('v0.41 T4: gbrain-everything meta-pack shape', () => { + const pack = loadPack('gbrain-everything'); + + test('extends gbrain-investor (chain head)', () => { + expect(pack.extends).toBe('gbrain-investor'); + }); + + test('borrows from gbrain-creator + gbrain-engineer', () => { + const borrowedPacks = pack.borrow_from.map((b) => b.pack).sort(); + expect(borrowedPacks).toEqual(['gbrain-creator', 'gbrain-engineer']); + }); + + test('borrows atom from creator and learning from engineer', () => { + const creatorBorrow = pack.borrow_from.find((b) => b.pack === 'gbrain-creator'); + expect(creatorBorrow?.types).toContain('atom'); + const engineerBorrow = pack.borrow_from.find((b) => b.pack === 'gbrain-engineer'); + expect(engineerBorrow?.types).toContain('learning'); + }); + + test('declares NO own page_types (everything via extends + borrow)', () => { + expect(pack.page_types).toEqual([]); + }); + + test('explicitly re-declares phases from creator (borrow_from does NOT borrow phases)', () => { + expect(pack.phases).toEqual(['extract_atoms', 'synthesize_concepts']); + }); + + test('explicitly unions ALL 7 lens calibration domains', () => { + const names = pack.calibration_domains.map((d) => d.name).sort(); + expect(names).toEqual([ + 'architecture_calls', + 'concept_themes', + 'deal_success', + 'effort_estimates', + 'founder_evaluation', + 'market_call', + 'risk_assessment', + ]); + }); + + test('every meta-pack calibration_domain aggregator is in the closed enum', () => { + for (const d of pack.calibration_domains) { + expect(AGGREGATOR_KINDS).toContain(d.aggregator); + } + }); + + test('aggregator selection matches per-pack declarations (cross-pack consistency)', () => { + const byName = Object.fromEntries(pack.calibration_domains.map((d) => [d.name, d.aggregator])); + // From investor + expect(byName.deal_success).toBe('scalar_brier'); + expect(byName.market_call).toBe('weighted_brier'); + expect(byName.founder_evaluation).toBe('scalar_brier'); + // From creator + expect(byName.concept_themes).toBe('cluster_summary'); + // From engineer + expect(byName.architecture_calls).toBe('scalar_brier'); + expect(byName.effort_estimates).toBe('weighted_brier'); + expect(byName.risk_assessment).toBe('scalar_brier'); + }); +});