/** * Genererar REVISION.md. Kör från projektroten: * bun run scripts/revision.ts > REVISION.md * * Skriptet skriver ut kalibrering, samtliga mellanled per standardscenario, * varje före/efter-jämförelse för de materiella rättelserna, inventeringen av * ändrade strukturparametrar, kontrollfall och känslighetstabell. * * Isolering av enskilda rättelser görs genom att tillfälligt ändra * kalibreringen eller STRUCTURAL i nuvarande modell (m3.0.0) och sedan * återställa värdet. Ingen rad nedan är ett resultat som någon publicerad * version har visat; jämförelsen mellan modellversionerna finns i AVSTAMNING.md. */ import { EMPTY_POLICY, MODEL_VERSION, STRUCTURAL, calibrate, evaluate, simulate, statutoryCorporateRate, type Calibration, type ScenarioParams, } from "../src/model/engine"; import { SCENARIOS } from "../src/model/scenarios"; import { DATA_VERSION } from "../src/data/dataset"; const YEAR = 2024; const n = (v: number, d = 1) => v.toLocaleString("sv-SE", { minimumFractionDigits: d, maximumFractionDigits: d }); const pct = (v: number) => `${n(v * 100, 2)} %`; const cal = calibrate(YEAR); const d = cal.data; const ref = evaluate(cal, SCENARIOS[0]!.params, EMPTY_POLICY).ref; let out = `# Revision av beräkningsmodellen Modellversion ${MODEL_VERSION}. Dataversion ${DATA_VERSION}. Basår i alla tabeller nedan: ${YEAR}. Alla belopp är miljarder kronor per år i basårets penningvärde. Genererad av \`scripts/revision.ts\` (\`bun run scripts/revision.ts > REVISION.md\`). ## Kalibrering, basår ${YEAR} | Storhet | Värde | Status | | --- | --- | --- | | Skatteintäkter, referens | ${n(ref.taxTotal)} | officiellt utfall | | Utgifter, referens | ${n(ref.expenditureTotal)} | officiellt utfall | | Saldo, referens | ${n(ref.balance)} | officiellt utfall | | D1, ersättning till anställda | ${n(d.compensation)} | officiellt utfall | | D11, löner före skatt | ${n(d.wages)} | officiellt utfall | | Löneberoende del av D29 (0,9 × D29), andel av D1 | ${pct(cal.employerTaxRatio)} | bearbetning med redaktionellt antagande | | Arbetskraftskostnad i modellen, D1 + 0,9 × D29 | ${n(d.compensation * (1 + cal.employerTaxRatio))} | bearbetning med redaktionellt antagande | | B2A3G, driftsöverskott och blandinkomst | ${n(d.operatingSurplus)} | officiellt utfall | | Lagstadgad bolagsskatt ${YEAR} | ${pct(statutoryCorporateRate(YEAR))} | lagstadgad sats | | Marginell skatt på förändrat överskott | ${pct(cal.marginalProfitTax)} | lagstadgad sats × redaktionellt genomslag 0,85 | Modellen använder 0,9 × D29 (\`payrollShareOfProductionTax\`) som uppskattning av den löneberoende delen av övriga produktionsskatter. Det är alltså **inte** en inläsning av SCB:s verifierade D29C-serie, utan en bearbetning med ett redaktionellt antagande om hur D29 delas mellan lönesumma och BNP. Samma andel används i skattegruppen "skatt på arbetsgivaravgifter", vilket innebär att en ändring av parametern påverkar både arbetskraftskostnaden och den delen av D29 som knyts till lönesumman. `; // --- Mellanled per scenario ------------------------------------------------- for (const sc of SCENARIOS) { const r = simulate(cal, sc.params, EMPTY_POLICY); const t = r.detail; const ev = evaluate(cal, sc.params, EMPTY_POLICY); out += ` ## Scenario: ${sc.label} (${sc.id}) Indata: ${Object.entries(sc.params) .map(([k, v]) => `${k}=${v}`) .join(", ")} | Mellanled | Formel | Värde | | --- | --- | --- | | Löneförändring, privat sektor | jobbindex − 1 | ${n(t.wageChangePct, 2)} % | | Förändrad D1, privat | D1 × privatandel × löneförändring | ${n(t.dCompensation)} | | Förändrad arbetskraftskostnad | ΔD1 × (1 + 0,9 × D29 / D1) | ${n(t.dLabourCost)} | | Kostnadsbesparing | −Δarbetskraftskostnad | ${n(t.grossPrivateSaving)} | | Produktionsökning | privat förädlingsvärde × produktionsantagande | ${n(t.productionGain)} | | AI-inköp | antagande | ${n(t.aiImportCost)} | | Netto att fördela | besparing + produktion − AI-inköp | ${n(t.netSaving)} | | Till svensk vinst | netto × vinstandel | ${n(t.toProfits)} | | Bolagsskatt på förändringen | Δvinst × marginell sats | ${n(t.corporateTaxOnChange)} | | Utdelning | (Δvinst − bolagsskatt) × utdelningsandel | ${n(t.dividends)} | | Skatt på utdelning | utdelning × 30 % | ${n(t.dividendTax)} | | Kvarhållen vinst | efter skatt − utdelning | ${n(t.retainedProfit)} | | Till lägre priser | netto × prisandel | ${n(t.toPrices)} | | varav till svenska hushåll | × 55 % | ${n(t.pricesToHouseholds)} | | Bortfallen löneinkomst, berörda personer | andel × privat D11 | ${n(t.lostWageIncome)} | | Omställningsstöd | bortfall × ersättningsgrad | ${n(t.support)} | | Offentlig effektivisering, brutto | offentlig konsumtion × antagande | ${n(t.grossPublicSaving)} | | Införandekostnad | brutto × antagande | ${n(t.implementationCost)} | | Offentlig effektivisering, netto | brutto − införande | ${n(t.netPublicSaving)} | | Förändrad disponibel inkomst | löner efter skatt + stöd efter skatt + utdelning efter skatt | ${n(t.disposableChange)} | | Resultat | Värde | | --- | --- | | Skatteintäkter, förändring (B − A) | ${n(ev.taxChangeAi)} | | Skatteintäkter, nivå | ${n(r.taxTotal)} | | Saldoförändring (B − A) | ${n(ev.balanceChangeAi)} | | Summa delposter | ${n(ev.contributions.reduce((a, c) => a + c.amount, 0))} | Delposter: ${ev.contributions.map((c) => `${c.label} ${n(c.amount)}`).join("; ")} `; } // --- Hjälpare för isolerade jämförelser ------------------------------------ type Override = { structural?: Partial; params?: Partial; calibration?: (c: Calibration) => void; }; function taxChange(base: ScenarioParams, o: Override = {}): number { const saved: Record = {}; for (const [k, v] of Object.entries(o.structural ?? {})) { saved[k] = (STRUCTURAL as Record)[k]; (STRUCTURAL as Record)[k] = v; } const c = calibrate(YEAR); o.calibration?.(c); const e = evaluate(c, { ...base, ...(o.params ?? {}) }, EMPTY_POLICY); for (const k of Object.keys(o.structural ?? {})) (STRUCTURAL as Record)[k] = saved[k]; return e.taxChangeAi; } // --- Före/efter per materiell rättelse ------------------------------------- out += ` ## Före och efter varje materiell rättelse Varje rad visar samma scenario med nuvarande antaganden, beräknat med det enskilda sambandet före rättelsen respektive efter. Före-värdena är **mellanled**: övriga rättelser och nuvarande scenarioantaganden är redan på plats i båda kolumnerna. Jämförelsen mellan modellversionerna m2.1.0 och m3.0.0, och uppdelningen mellan rättade formler och ändrade antaganden, finns i \`AVSTAMNING.md\`. Arbetskraftskostnaden isoleras genom att \`cal.employerTaxRatio\` sätts till 0. Det påverkar bara arbetskraftskostnaden och underlaget för produktionsökningen, inte kopplingen mellan D29 och lönesumman i skattegrupperna. (En tidigare version av det här dokumentet isolerade rättelsen genom att sätta \`payrollShareOfProductionTax\` till 0. Det ändrar samtidigt hur D29 fördelas mellan lönesumma och BNP och isolerar därför inte arbetskraftskostnaden. Den raden gav −67,4 i kapitalscenariot och är ersatt nedan.) | Scenario | Rättelse | Före | Efter | Skillnad | | --- | --- | --- | --- | --- | `; const rows: string[] = []; for (const sc of SCENARIOS) { const p = sc.params; const after = taxChange(p); // 1. Arbetskraftskostnad: enbart employerTaxRatio = 0. const noEmployerTax = taxChange(p, { calibration: (c) => void (c.employerTaxRatio = 0) }); rows.push( `| ${sc.id} | Arbetskraftskostnad D1 → D1 + löneberoende D29 (isolerat via employerTaxRatio) | ${n(noEmployerTax)} | ${n(after)} | ${n(after - noEmployerTax)} |`, ); // 1b. Samma sak, men med produktionsökningen hållen konstant i kronor, så att // enbart kostnadssidan skiljer sig. const privateComp = d.compensation * STRUCTURAL.privateShareOfWages; const vaFull = privateComp * (1 + cal.employerTaxRatio) + d.operatingSurplus; const vaNone = privateComp + d.operatingSurplus; const scaledPg = p.productionGainPct * (vaFull / vaNone); const costOnly = taxChange(p, { calibration: (c) => void (c.employerTaxRatio = 0), params: { productionGainPct: scaledPg }, }); rows.push( `| ${sc.id} | varav enbart kostnadssidan (produktionsökningen hållen konstant i kronor) | ${n(costOnly)} | ${n(after)} | ${n(after - costOnly)} |`, ); // 2. Marginell bolagsskatt i stället för kalibrerad nivåsats. const levelRate = cal.comp.corporate[0]!.eff; const levelTax = taxChange(p, { calibration: (c) => void (c.marginalProfitTax = levelRate), }); rows.push( `| ${sc.id} | Skatt på tillkommande vinst: kalibrerad nivåsats (${pct(levelRate)}) → marginell sats (${pct(cal.marginalProfitTax)}) | ${n(levelTax)} | ${n(after)} | ${n(after - levelTax)} |`, ); // 3. Prissänkning: allt till svenska hushåll → 55 %. const allPrices = taxChange(p, { structural: { priceShareToHouseholds: 1 } }); rows.push( `| ${sc.id} | Prissänkning: allt till svenska hushåll → 55 % | ${n(allPrices)} | ${n(after)} | ${n(after - allPrices)} |`, ); // 4. Produktionsökning som uttryckligt antagande. const noProduction = taxChange(p, { params: { productionGainPct: 0 } }); rows.push( `| ${sc.id} | Produktionsökning som uttryckligt antagande (0 → ${n(p.productionGainPct, 1)} %) | ${n(noProduction)} | ${n(after)} | ${n(after - noProduction)} |`, ); // 5. Stödbehovet överlever höjda löner för kvarvarande. const supportNow = simulate(cal, p, EMPTY_POLICY).detail.support; const supportNet = simulate( cal, { ...p, inputMode: "direct", wageChangePct: simulate(cal, p, EMPTY_POLICY).detail.wageChangePct }, EMPTY_POLICY, ).detail.support; rows.push( `| ${sc.id} | Omställningsstöd: på nettot i ekonomin → på berörda personers bortfall | ${n(supportNet)} | ${n(supportNow)} | ${n(supportNow - supportNet)} |`, ); } out += rows.join("\n") + "\n"; // --- Ändrade strukturparametrar -------------------------------------------- const capital = SCENARIOS.find((s) => s.id === "capital")!.params; const bridge: ScenarioParams = { ...capital, lostJobWagePct: 100, productionGainPct: 0 }; const oldStructural = { labourShareOfIncomeTax: 0.78, capitalShareOfIncomeTax: 0, priceShareToHouseholds: 1, }; const stepA = taxChange(bridge, { structural: oldStructural }); const stepB = taxChange(bridge, { structural: { ...oldStructural, labourShareOfIncomeTax: 0.74, capitalShareOfIncomeTax: 0.04 }, }); const stepC = taxChange(bridge, { structural: { ...oldStructural, labourShareOfIncomeTax: 0.74, capitalShareOfIncomeTax: 0.04, priceShareToHouseholds: 0.55 } }); const stepD = taxChange(capital); out += ` ## Ändrade strukturparametrar, skilda från rena beräkningsfel Inventering av samtliga strukturparametrar som skiljer m2.1.0 från m3.0.0. "Formeländring" betyder att sambandet i koden ändrades; "parametervärde" betyder att ett tal ändrades utan att sambandet ändrades. | Parameter | m2.1.0 | m3.0.0 | Typ | Klass | | --- | --- | --- | --- | --- | | \`labourShareOfIncomeTax\` (D51A, arbete) | 0,78 | 0,74 | parametervärde | bearbetad fördelning, redaktionell | | \`capitalShareOfIncomeTax\` (D51A, kapital) | saknades (0) | 0,04 | parametervärde + ny kanal | bearbetad fördelning, redaktionell | | D51A, pension och transfereringar (rest) | 0,22 | 0,22 | oförändrad | bearbetad fördelning, redaktionell | | \`priceShareToHouseholds\` | saknades (1,0) | 0,55 | parametervärde | redaktionellt antagande | | \`profitTaxableShareOfMarginal\` | fanns inte | 0,85 | ny parameter i ny formel | redaktionellt antagande | | \`capitalGainsTaxRate\` | kalibrerad effektiv sats | 0,30 | lagstadgad sats i stället för kalibrerad | lagstadgad sats | | \`capitalGainsPassthrough\` | 0,50 | 0,50 | oförändrad | redaktionellt antagande | | \`dividendTaxRate\` | 0,30 | 0,30 | oförändrad | redaktionellt antagande (effektiv beskattning) | | \`payrollShareOfProductionTax\` | 0,90 | 0,90 | oförändrad | redaktionellt antagande | | \`privateShareOfWages\` | 0,72 | 0,72 | oförändrad | bearbetad fördelning | | \`publicWageShareOfConsumption\` | 0,60 | 0,60 | oförändrad | redaktionellt antagande | | \`priceVolumeResponse\` | 0,90 | 0,90 | oförändrad | redaktionellt antagande | | \`taxElasticity\` (alla grupper) | oförändrade | oförändrade | oförändrad | redaktionellt antagande | Lagstadgade satser (bolagsskatt per år, 30 procent kapitalinkomstskatt) är lagtext. Antaganden om effektiv beskattning är de tal som avgör hur stor del av en förändring som faktiskt beskattas: \`profitTaxableShareOfMarginal\` (0,85), \`capitalGainsPassthrough\` (0,50) och \`dividendTaxRate\` (0,30 tillämpad på hela utdelningen, alltså utan hänsyn till sparformer med lägre effektiv skatt). De är redaktionella antaganden, inte lagstadgade satser. ### Ordning när flera ändringar samverkar Steget "gamla antaganden, rättade formler" i \`AVSTAMNING.md\` innehöll tidigare även de ändrade strukturparametrarna. Nedan separeras de, i angiven ordning, för scenariot "AI ersätter mycket arbete" med gamla scenarioantaganden (\`lostJobWagePct\`=100, \`productionGainPct\`=0). Ordningen påverkar fördelningen mellan raderna eftersom effekterna inte är exakt additiva; den valda ordningen är D51A först, därefter prisandelen. | Steg | Skatteintäkter, förändring | Effekt av steget | | --- | --- | --- | | 1. Rättade formler, m2.1.0:s strukturparametrar (78/0/22, hela prissänkningen till hushållen) | ${n(stepA)} | – | | 2. D51A-fördelning 78/0/22 → 74/4/22 | ${n(stepB)} | ${n(stepB - stepA)} | | 3. Prissänkning till hushållen 1,00 → 0,55 | ${n(stepC)} | ${n(stepC - stepB)} | | 4. Nya scenarioantaganden (lostJobWagePct 100 → 90, productionGainPct 0 → 0,5) | ${n(stepD)} | ${n(stepD - stepC)} | `; // --- Kontrollfall ----------------------------------------------------------- // Kontrollfallet anger bara de fält som m3.0.0-tabellen använder; övriga fält // saknas avsiktligt (typomvandling, ingen ändring av beräkningen). const flat = { inputMode: "direct", jobsLostPct: 0, jobsReplacedPct: 0, retainedJobWagePct: 0, newJobWagePct: 100, lostJobWagePct: 100, productionGainPct: 0, wageChangePct: 0, aiImportCost: 0, savingToProfitsPct: 0, savingToPricesPct: 0, publicEfficiencyPct: 0, publicImplementationPct: 0, transferSupportPct: 0, consumptionPassthroughPct: 0, dividendToHouseholdsPct: 0, } as unknown as ScenarioParams; const cases: [string, ScenarioParams][] = [ ["Oförändrat läge", flat], ["Högre löner utan högre produktion (+5 %)", { ...flat, wageChangePct: 5 }], [ "Ren kostnadsbesparing (−5 %, allt till vinst)", { ...flat, wageChangePct: -5, savingToProfitsPct: 100 }, ], [ "Offentlig effektivisering 3 % med 30 % införandekostnad", { ...flat, publicEfficiencyPct: 3, publicImplementationPct: 30 }, ], ]; out += ` ## Kontrollfall | Kontrollfall | Skatteintäkter, förändring | Saldoförändring | Vinstförändring | Stöd | | --- | --- | --- | --- | --- | `; for (const [label, p] of cases) { const e = evaluate(cal, p, EMPTY_POLICY); out += `| ${label} | ${n(e.taxChangeAi, 3)} | ${n(e.balanceChangeAi, 3)} | ${n(e.ai.detail.toProfits, 2)} | ${n(e.ai.detail.support, 2)} |\n`; } // --- Känslighet ------------------------------------------------------------- const sens: [string, number, number, number][] = [ [ "Utländska AI-inköp, mdkr (80 / 160 / 240)", taxChange(capital, { params: { aiImportCost: 80 } }), taxChange(capital), taxChange(capital, { params: { aiImportCost: 240 } }), ], [ "Löneinkomst i försvunna jobb, % av snittet (80 / 90 / 120)", taxChange(capital, { params: { lostJobWagePct: 80 } }), taxChange(capital), taxChange(capital, { params: { lostJobWagePct: 120 } }), ], [ "Produktionsökning, % av privat förädlingsvärde (0 / 0,5 / 2)", taxChange(capital, { params: { productionGainPct: 0 } }), taxChange(capital), taxChange(capital, { params: { productionGainPct: 2 } }), ], [ "Arbetsinkomstandel av D51A (0,70 / 0,74 / 0,80)", taxChange(capital, { structural: { labourShareOfIncomeTax: 0.7 } }), taxChange(capital), taxChange(capital, { structural: { labourShareOfIncomeTax: 0.8 } }), ], [ "Kapitalvinstgenomslag (0,25 / 0,50 / 0,75)", taxChange(capital, { structural: { capitalGainsPassthrough: 0.25 } }), taxChange(capital), taxChange(capital, { structural: { capitalGainsPassthrough: 0.75 } }), ], [ "Genomslag i skattemässigt överskott (0,60 / 0,85 / 1,00)", taxChange(capital, { structural: { profitTaxableShareOfMarginal: 0.6 } }), taxChange(capital), taxChange(capital, { structural: { profitTaxableShareOfMarginal: 1 } }), ], [ "Prissänkning till svenska hushåll (0,35 / 0,55 / 0,80)", taxChange(capital, { structural: { priceShareToHouseholds: 0.35 } }), taxChange(capital), taxChange(capital, { structural: { priceShareToHouseholds: 0.8 } }), ], ]; out += ` ## Känslighet, scenariot "AI ersätter mycket arbete" Förändring av skatteintäkter i miljarder kronor per år. Kolumnerna "Lågt värde" och "Högt värde" är **valda exempel** som visar riktning och storleksordning. De är inte ett statistiskt osäkerhetsintervall och har ingen sannolikhet kopplad till sig. | Antagande | Lågt värde | Förval | Högt värde | | --- | --- | --- | --- | `; for (const [l, a, b, c] of sens) out += `| ${l} | ${n(a)} | ${n(b)} | ${n(c)} |\n`; console.log(out);