Files
Jonathan KirkwoodandClaude Fable 5 b1d7aed9f4 Implement BDEF v1.1 grading: scoring core, per-deck pipeline, ledger, dashboard, StartOS layer
- Deterministic scoring.py (quant 60 / qual 40 / flags -15, profitability heaviest)
- Per-company JSON ledger with forecast-target chaining deck N-1 -> N
- Single-shot sandbox agent with guided-JSON fallback ladder (no tool loop)
- Portfolio dashboard with sparklines, KPI hit rates, BDEF category bars
- 48 unit tests green; endpoints smoke-tested; npm check+build green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:15:12 -05:00

117 lines
3.9 KiB
TypeScript

import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
const { InputSpec, Value, List } = sdk
const inputSpec = InputSpec.of({
graders: Value.list(
List.obj(
{
name: 'Grading Panel',
description:
'Who grades the decks — one entry per grader. Add as many as you like. ' +
'Each grader is a model from your catalog plus a PERSONA: the lens it ' +
'grades through, so the same deck gets scored from different angles ' +
'before the deterministic composite is computed.',
default: [],
minLength: 1,
maxLength: 32,
},
{
uniqueBy: 'name',
displayAs: '{{name}} ({{model}})',
spec: InputSpec.of({
name: Value.text({
name: 'Name',
description: 'Unique grader name. Becomes its container and grade-sheet filename.',
required: true,
default: null,
placeholder: 'munger-lens',
patterns: [
{ regex: '^[A-Za-z0-9][A-Za-z0-9 _-]{0,40}$',
description: 'Letters, numbers, spaces, dashes, underscores (max 41 chars).' },
],
}),
model: Value.text({
name: 'Model Alias',
description: 'Which catalog model this grader uses (must match an alias from "Configure Models").',
required: true,
default: null,
placeholder: 'grader-a',
}),
persona: Value.textarea({
name: 'Persona / Lens',
description:
'How THIS grader should read the deck — its priorities and ' +
'weighting within the BDEF rubric. Injected into its system prompt. ' +
'e.g. "You are a Munger-style inversion skeptic: ask what would have ' +
'to be true for this deck to be hiding a deteriorating business." ' +
'Leave empty for a neutral grader.',
required: false,
default: null,
minRows: 3,
maxRows: 16,
placeholder:
'You are a Girdley-style operator. Weight unit economics, owner ' +
'accountability, and whether the KPIs the board was promised last ' +
'quarter are still being reported. Flag every silently dropped metric.',
}),
temperature: Value.number({
name: 'Sampling Temperature (optional)',
description:
'Best-effort per-grader sampling temperature for extra diversity. ' +
'Persona is the primary lever. Leave empty to use the model default.',
required: false,
default: null,
integer: false,
min: 0,
max: 2,
}),
}),
},
),
),
})
export const configureGraders = sdk.Action.withInput(
'configure-graders',
async ({ effects }) => ({
name: 'Configure Graders',
description: 'Define the grading panel: which models and which personas grade each deck.',
warning: null,
allowedStatuses: 'any',
group: 'Grading',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return { graders: cfg.graders }
},
async ({ effects, input }) => {
await configFile.merge(effects, { graders: input.graders })
return {
version: '1',
title: 'Panel Configured',
message:
'Saved a panel of ' + input.graders.length + ' grader(s). Each model ' +
'alias must exist in "Configure Models". Set the scoring knobs in ' +
'"Configure Grading", add companies in "Configure Companies", then drop ' +
'decks into inbox/<company-slug>/ and run "Grade Decks".',
result: {
type: 'single',
value: input.graders.map((g) => g.name).join(', '),
copyable: false,
qr: false,
masked: false,
},
}
},
)