import { sdk } from '../sdk' import { configFile } from '../file-models/config' const { InputSpec, Value } = sdk const inputSpec = InputSpec.of({ bdefOverride: Value.textarea({ name: 'BDEF Rubric Override (optional)', description: 'Leave EMPTY to grade against the baked-in BDEF v1.1 framework ' + '(Girdley + Munger/Buffett). Non-empty text replaces the rubric wholesale, ' + 'so include the qualitative categories A-H if you customize it.', required: false, default: null, minRows: 5, maxRows: 24, placeholder: '(empty = the built-in BDEF v1.1 rubric)', }), extractorModel: Value.text({ name: 'Extractor Model (optional)', description: 'Catalog alias of the model that runs the stage-1 structured KPI ' + 'extraction over each deck. Empty = use the first model.', required: false, default: null, placeholder: 'grader-a', }), networkMode: Value.select({ name: 'Network Mode', description: 'Air-gapped: graders reach ONLY the on-Spark model proxy — zero internet, ' + 'board decks never leave your hardware (models must be pre-pulled into ' + 'each Spark HF cache; models on either Spark are fine — the proxy routes ' + 'to the second Spark over your LAN). Local services: graders may also ' + 'reach LAN services like SearXNG (this network has egress unless you ' + 'firewall it).', default: 'airgapped', values: { airgapped: 'Air-gapped (no network, recommended)', local_services: 'Local services (SearXNG / 2nd Spark)', }, }), searxngUrl: Value.text({ name: 'SearXNG URL (local-services only)', description: 'JSON-search endpoint to give graders a web_search tool. Ignored in air-gapped mode. Empty = no web search.', required: false, default: null, placeholder: 'https://searxng.local', }), adjudicatorEnabled: Value.toggle({ name: 'Run an Adjudicator', description: 'After the panel finishes, run a local "lead grader" that reads every ' + 'grade sheet, reconciles disagreements, and settles the qualitative scores ' + 'the deterministic composite uses. No frontier model — stays on the Sparks.', default: true, }), adjudicatorModel: Value.text({ name: 'Adjudicator Model (optional)', description: 'Catalog alias of the model that adjudicates the panel. Empty = use the first model.', required: false, default: null, placeholder: 'grader-a', }), adjudicatorPersona: Value.textarea({ name: 'Adjudicator Instructions (optional)', description: 'Override how the adjudicator reconciles the panel. Empty = a sensible built-in default.', required: false, default: null, minRows: 3, maxRows: 14, }), wipeRemoteDocs: Value.toggle({ name: 'Wipe Decks From Sparks After Grading', description: 'Delete the extracted deck text from the Sparks when a job finishes. ' + 'Scorecards and ledgers are always kept on this StartOS box. Recommended ' + 'for confidential board material.', default: true, }), autoRunOnDrop: Value.toggle({ name: 'Auto-grade When Decks Are Dropped', description: 'Start grading automatically (after a short debounce) whenever new decks ' + 'land in the inbox. Off by default so you trigger grading explicitly.', default: false, }), preJobStopContainers: Value.text({ name: 'Stop These Containers Before Grading (optional)', description: 'Space- or comma-separated docker container names stopped on the HEAD ' + 'Spark at the start of every grading job, so the job gets the GPU to ' + 'itself (e.g. an always-on vLLM another service runs). They are NOT ' + 'restarted afterwards — the service that owns them reloads its own ' + 'models (e.g. the Gazette\'s Fleet job). Empty = stop nothing.', required: false, default: null, placeholder: 'vllm-gemma4-clerk', }), // --- Deterministic-scorer weights (composite = quant 60 + qual 40 - flags) --- profitabilityKpi: Value.number({ name: 'Weight: Profitability KPIs', description: 'Points for the profitability KPI attainment bucket — the heaviest slice of the quant 60. Default 30.', required: true, default: 30, integer: false, min: 0, max: 100, }), otherKpi: Value.number({ name: 'Weight: Other KPIs', description: 'Points for the non-profitability measurable-KPI bucket. Default 20.', required: true, default: 20, integer: false, min: 0, max: 100, }), forecastIntegrity: Value.number({ name: 'Weight: Forecast Integrity', description: 'Points for deck N actuals hitting what deck N-1 promised. Default 10.', required: true, default: 10, integer: false, min: 0, max: 100, }), qualCategoryMax: Value.number({ name: 'Weight: Max Per Qualitative Category', description: 'Max points per BDEF category A-H (8 categories x 5 = the qualitative 40). Default 5.', required: true, default: 5, integer: false, min: 0, max: 100, }), redFlagCap: Value.number({ name: 'Red-flag Penalty Cap', description: 'Maximum total points red flags can subtract from the composite. Default 15.', required: true, default: 15, integer: false, min: 0, max: 100, }), kpiCreditFloor: Value.number({ name: 'KPI Credit Floor', description: 'actual/target ratio below which a KPI earns zero credit (linear credit above it). Default 0.5.', required: true, default: 0.5, integer: false, min: 0, max: 1, }), droppedKpiPenalty: Value.number({ name: 'Dropped-KPI Penalty', description: 'Penalty per KPI that silently disappeared from the deck. Default 2.', required: true, default: 2, integer: false, min: 0, max: 100, }), droppedKpiMax: Value.number({ name: 'Dropped-KPI Flag Limit', description: 'Count at most this many dropped-KPI flags per deck. Default 3.', required: true, default: 3, integer: true, min: 0, max: 50, }), evidenceFullCredit: Value.number({ name: 'Evidence Full-credit Threshold', description: 'Quote characters a qualitative finding needs for full weight (thinner evidence scales down). Default 400.', required: true, default: 400, integer: true, min: 0, max: 100000, }), singleSourceFlagFactor: Value.number({ name: 'Single-source Flag Damping', description: 'Multiplier applied to red flags raised by only one grader. Default 0.5.', required: true, default: 0.5, integer: false, min: 0, max: 1, }), }) export const configureGrading = sdk.Action.withInput( 'configure-grading', async ({ effects }) => ({ name: 'Configure Grading', description: 'Set the BDEF rubric, air-gap mode, adjudication, retention, and the scoring weights.', warning: null, allowedStatuses: 'any', group: 'Grading', visibility: 'enabled', }), inputSpec, async ({ effects }) => { const cfg = await configFile.read().const(effects) if (!cfg) return {} return { bdefOverride: cfg.bdefOverride || undefined, extractorModel: cfg.extractorModel || undefined, networkMode: cfg.networkMode, searxngUrl: cfg.searxngUrl || undefined, adjudicatorEnabled: cfg.adjudicatorEnabled, adjudicatorModel: cfg.adjudicatorModel || undefined, adjudicatorPersona: cfg.adjudicatorPersona || undefined, wipeRemoteDocs: cfg.wipeRemoteDocs, autoRunOnDrop: cfg.autoRunOnDrop, preJobStopContainers: cfg.preJobStopContainers || undefined, profitabilityKpi: cfg.weights.profitabilityKpi, otherKpi: cfg.weights.otherKpi, forecastIntegrity: cfg.weights.forecastIntegrity, qualCategoryMax: cfg.weights.qualCategoryMax, redFlagCap: cfg.weights.redFlagCap, kpiCreditFloor: cfg.weights.kpiCreditFloor, droppedKpiPenalty: cfg.weights.droppedKpiPenalty, droppedKpiMax: cfg.weights.droppedKpiMax, evidenceFullCredit: cfg.weights.evidenceFullCredit, singleSourceFlagFactor: cfg.weights.singleSourceFlagFactor, } }, async ({ effects, input }) => { await configFile.merge(effects, { bdefOverride: input.bdefOverride ?? '', extractorModel: input.extractorModel ?? '', networkMode: input.networkMode, searxngUrl: input.searxngUrl ?? '', adjudicatorEnabled: input.adjudicatorEnabled, adjudicatorModel: input.adjudicatorModel ?? '', adjudicatorPersona: input.adjudicatorPersona ?? '', wipeRemoteDocs: input.wipeRemoteDocs, autoRunOnDrop: input.autoRunOnDrop, preJobStopContainers: input.preJobStopContainers ?? '', weights: { profitabilityKpi: input.profitabilityKpi, otherKpi: input.otherKpi, forecastIntegrity: input.forecastIntegrity, qualCategoryMax: input.qualCategoryMax, redFlagCap: input.redFlagCap, kpiCreditFloor: input.kpiCreditFloor, droppedKpiPenalty: input.droppedKpiPenalty, droppedKpiMax: input.droppedKpiMax, evidenceFullCredit: input.evidenceFullCredit, singleSourceFlagFactor: input.singleSourceFlagFactor, }, }) return { version: '1', title: 'Grading Settings Saved', message: input.networkMode === 'airgapped' ? 'Saved. Graders will run air-gapped (no internet). Ensure all models are on the head Spark and pre-pulled into its HF cache.' : 'Saved. Graders run in local-services mode and may reach the network — make sure that is acceptable for these board decks.', result: { type: 'single', value: input.networkMode, copyable: false, qr: false, masked: false }, } }, )