@haus-tech/haus-workflow 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,10 @@
14
14
  * delar), samt effort ur agentdefinitionen i add-run och ett state.json-skelett
15
15
  * från ny-korning. Del 4 (plan utvardera-parallellt-svep, Task 1): experiment,
16
16
  * den enda skrivaren av experiment.json på experimentytan, låst och atomisk,
17
- * så samtidiga svep-processer aldrig tappar varandras rader (#493).
17
+ * så samtidiga svep-processer aldrig tappar varandras rader (#493). Del 5
18
+ * (#530): nasta-steg, läs-enbart tabell över var posten står, vad en människa
19
+ * väntas göra och vilket /request-kommando som kör vidare, ur SKILL.md:s
20
+ * egna grindar (regeltabellen står ovanför `nastaStegFor`).
18
21
  *
19
22
  * Användning:
20
23
  * node archive.mjs add-run <katalog> --file F --skill S --agent A --model M [flaggor]
@@ -28,8 +31,9 @@
28
31
  * node archive.mjs stopp <katalog> --utfall KONCEPT|DIAGNOS|TILLBAKA_TILL_SPEC|ESKALERA --kalla spec.md|impact.md|granskarblock.md [--orsak <text>]
29
32
  * node archive.mjs respec <katalog>
30
33
  * node archive.mjs bluf <katalog>
34
+ * node archive.mjs nasta-steg <katalog>
31
35
  * node archive.mjs kalibrering <arkivrot>
32
- * node archive.mjs experiment <experimentkatalog> [--satt K=V]... [--variant M]... [--korning M/kN --status S [--orsak T]]
36
+ * node archive.mjs experiment <experimentkatalog> [--satt K=V]... [--variant M]... [--korning M/kN --status S [--orsak T] [--pid N]]
33
37
  * Exit 0 = ok, 1 = spärrad (sparr/set-status priced), 2 = användnings-/tillståndsfel.
34
38
  */
35
39
  import {
@@ -64,7 +68,7 @@ const [cmd, katalog, ...rest] = process.argv.slice(2)
64
68
 
65
69
  if (!cmd || !katalog) {
66
70
  console.error(
67
- 'användning: archive.mjs <add-run|next-n|ny-korning|ny-del|proveniens|sparr|utslag|set-status|stopp|respec|bluf|kalibrering|experiment> <katalog> [flaggor]',
71
+ 'användning: archive.mjs <add-run|next-n|ny-korning|ny-del|proveniens|sparr|utslag|set-status|stopp|respec|bluf|nasta-steg|kalibrering|experiment> <katalog> [flaggor]',
68
72
  )
69
73
  process.exit(2)
70
74
  }
@@ -1220,21 +1224,31 @@ if (cmd === 'add-run') {
1220
1224
  // skriver (#493: sonnet/k4 försvann i en läs-modifiera-skriv utan lås), så
1221
1225
  // den enda skrivaren är det här kommandot, under samma lås som state.json-
1222
1226
  // kommandona. acquireLock låser i `katalog`, alltså i experimentkatalogen.
1223
- avvisaOkandaFlaggor('experiment', ['satt', 'variant', 'korning', 'status', 'orsak'])
1227
+ avvisaOkandaFlaggor('experiment', ['satt', 'variant', 'korning', 'status', 'orsak', 'pid'])
1224
1228
  const sattPar = flags('satt').map(tolkaSatt)
1225
1229
  const nyaVarianter = flags('variant')
1226
1230
  const korningsflaggor = flags('korning')
1227
1231
  const status = flag('status')
1228
1232
  const orsak = flag('orsak')
1233
+ // #516: startraden (`ofärdig`, "startad, inte avslutad") bär startarens pid
1234
+ // medan körningen pågår, så ett andra svep kan skilja en körning som lever
1235
+ // från en som dog. Slutstatusen skrivs utan --pid och ersätter raden helt,
1236
+ // pid:n försvinner alltså av sig själv när processen är klar.
1237
+ const pidText = flag('pid')
1229
1238
  if (korningsflaggor.length > 1) {
1230
1239
  console.error('experiment: högst en --korning per anrop')
1231
1240
  process.exit(2)
1232
1241
  }
1233
1242
  const korning = korningsflaggor[0] ?? null
1234
- if (korning === null && (status !== null || orsak !== null)) {
1235
- console.error('experiment: --status och --orsak kräver --korning')
1243
+ if (korning === null && (status !== null || orsak !== null || pidText !== null)) {
1244
+ console.error('experiment: --status, --orsak och --pid kräver --korning')
1236
1245
  process.exit(2)
1237
1246
  }
1247
+ if (pidText !== null && !/^\d+$/.test(pidText)) {
1248
+ console.error(`experiment: --pid måste vara ett heltal (fick "${pidText}")`)
1249
+ process.exit(2)
1250
+ }
1251
+ const pid = pidText === null ? null : Number(pidText)
1238
1252
  if (korning !== null) {
1239
1253
  if (!KORNING_NYCKEL_RE.test(korning)) {
1240
1254
  console.error(`experiment: --korning ska ha formen <modell>/k<N>, fick "${korning}"`)
@@ -1304,7 +1318,11 @@ if (cmd === 'add-run') {
1304
1318
  }
1305
1319
  // Bara den egna raden skrivs; andra körningars rader kopieras orörda.
1306
1320
  if (korning !== null) {
1307
- const rad = orsak === null ? { status } : { status, orsak }
1321
+ const rad = {
1322
+ status,
1323
+ ...(orsak === null ? {} : { orsak }),
1324
+ ...(pid === null ? {} : { pid }),
1325
+ }
1308
1326
  const befintlig = nasta.korningar?.[korning]
1309
1327
  if (JSON.stringify(befintlig) !== JSON.stringify(rad)) {
1310
1328
  nasta = { ...nasta, korningar: { ...(nasta.korningar ?? {}), [korning]: rad } }
@@ -1700,9 +1718,40 @@ if (cmd === 'add-run') {
1700
1718
  throw e
1701
1719
  }
1702
1720
  console.log(rader.join('\n'))
1721
+ } else if (cmd === 'nasta-steg') {
1722
+ // #530: läs-enbart tabell över var posten står, vad en människa väntas
1723
+ // göra och vilket /request-kommando som kör vidare. Samma regler som
1724
+ // SKILL.md:s grindar, härledda i `nastaStegFor` nedan, aldrig handskrivna:
1725
+ // en handskriven avslutning tappade exakt den raden i skarp körning.
1726
+ avvisaOkandaFlaggor('nasta-steg', [])
1727
+ const cell = (v) => String(v).replace(/\|/g, '\\|')
1728
+ const radFor = (d, post) => {
1729
+ const state = readStateOrThrow(d)
1730
+ const direktaDelar = delKataloger(d).filter((p) => path.dirname(p) === d)
1731
+ const arBuntRot = !existsSync(path.join(d, 'spec.md')) && direktaDelar.length > 0
1732
+ const { star, vantar, nasta } = nastaStegFor(state, {
1733
+ post,
1734
+ antalDelar: arBuntRot ? direktaDelar.length : 0,
1735
+ scorerUtfall: gallandeScorerUtfall(state, d),
1736
+ })
1737
+ return `| ${[post, star, vantar, nasta].map(cell).join(' | ')} |`
1738
+ }
1739
+ const rot = path.resolve(katalog)
1740
+ const rader = ['| post | står | väntar på | nästa |', '| --- | --- | --- | --- |']
1741
+ try {
1742
+ rader.push(radFor(rot, path.basename(rot)))
1743
+ for (const d of delKataloger(rot)) rader.push(radFor(d, path.relative(rot, d)))
1744
+ } catch (e) {
1745
+ if (e instanceof ArkivFel) {
1746
+ console.error(e.message)
1747
+ process.exit(2)
1748
+ }
1749
+ throw e
1750
+ }
1751
+ console.log(rader.join('\n'))
1703
1752
  } else {
1704
1753
  console.error(
1705
- 'okänt kommando (add-run, next-n, ny-korning, ny-del, proveniens, sparr, utslag, set-status, stopp, respec, bluf, kalibrering, experiment)',
1754
+ 'okänt kommando (add-run, next-n, ny-korning, ny-del, proveniens, sparr, utslag, set-status, stopp, respec, bluf, nasta-steg, kalibrering, experiment)',
1706
1755
  )
1707
1756
  process.exit(2)
1708
1757
  }
@@ -1777,6 +1826,141 @@ function delKataloger(dir) {
1777
1826
  .flatMap((d) => [d, ...delKataloger(d)])
1778
1827
  }
1779
1828
 
1829
+ /** Scorerns gällande utfall ur prisfilen, eller null när inget poängvarv
1830
+ * finns. Läses bara för att `nasta-steg` inte ska säga "sync till brädan"
1831
+ * om en post vars gällande varv är DELA_UPP eller OVER_SKALAN: de utfallen
1832
+ * når arkivet via prisfilen, aldrig via `stopp` (se STOPP_UTFALL). Samma
1833
+ * fil som `bluf` och `pricedBlockers` läser, via samma hjälpare. */
1834
+ function gallandeScorerUtfall(state, d) {
1835
+ const n = maxN(state.runs ?? [], 'score')
1836
+ if (n === 0) return null
1837
+ const fil = gallandeDokumentFil(state, 'score', n)
1838
+ return { fil, outcome: readPricingFileOrThrow(fil, d).outcome ?? null }
1839
+ }
1840
+
1841
+ /** Regeltabellen bakom `nasta-steg` (#530). Varje regel är en avläsning av
1842
+ * en grind som redan står i SKILL.md; kolumnen "väntar på" är det en
1843
+ * MÄNNISKA måste göra, "nästa" det /request-kommando som kör vidare.
1844
+ * Första träff vinner, uppifrån:
1845
+ *
1846
+ * | läge | källa i SKILL.md |
1847
+ * | ------------------------------------- | ------------------------------------------------------------- |
1848
+ * | bunt-rot (ingen spec.md, del-N finns) | Experimentytan, buntregeln; bluf:s `bunt (N delar)` |
1849
+ * | spärr står (sparrSkal) | bedöm steg 6 OLIKA "Hänvisa sedan till `döm <post>`"; `döm` |
1850
+ * | stopp KONCEPT/DIAGNOS | speca steg 6 "Kandidatens katalog kan fortfarande läggas på |
1851
+ * | | brädan … men `bedöm` hoppar över den"; stopputfall → kunddialog |
1852
+ * | stopp TILLBAKA_TILL_SPEC | speca steg 6 (ADR-0118) "När valet avgjorts skrivs svaret |
1853
+ * | | under `## Avgjorda val` …, sedan `respec` och ny dispatch" |
1854
+ * | stopp ESKALERA | speca steg 6 "en människa dömer, posten går aldrig en tredje |
1855
+ * | | specrunda"; stopputfall ESKALERA → visa skälet och stanna |
1856
+ * | experiment | Experimentytan: "kan inte förväxlas med en prispost"; |
1857
+ * | | Hårda regler "Experimentdata når aldrig Asana" |
1858
+ * | spec_draft utan approval | speca steg 7 "Erbjud att lägga specarna på brädan (🔍)"; |
1859
+ * | | godkänn/underkänn verkar från 🔍 |
1860
+ * | spec_synced utan approval | godkänn/underkänn-lägesgrinden: 🔍 → ✅ Spec ok |
1861
+ * | spec_draft/spec_synced med approval | bedöm-grinden: "Endast `✅ Spec ok` … släpper igenom", |
1862
+ * | | "Post utan bräda: `godkänn` antecknar approval manual" |
1863
+ * | assessed utan poängvarv | bedöm steg 3–7: analysen finns, poängen saknas |
1864
+ * | assessed, scorerns DELA_UPP | stopputfall → "ny analysrunda per del … `split` satt" |
1865
+ * | assessed, scorerns OVER_SKALAN | stopputfall → "offereras separat; delas aldrig vidare" |
1866
+ * | assessed, scorerns övriga stopp | stopputfall → kunddialog / spec-steget / visa skälet, stanna |
1867
+ * | assessed (poäng) | bedöm steg 8: sync via request-sync tar ✅ → ⚖️, sedan |
1868
+ * | | godkänn/underkänn från ⚖️ |
1869
+ * | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund |
1870
+ * | guard_run | `guard_run` reserveras för guard-kommandot: inget väntar |
1871
+ * | allt annat | `okänt läge: <status>`, aldrig en gissning |
1872
+ *
1873
+ * `står` är statusen plus det som avviker från grundläget: stoppet,
1874
+ * scorerns stopputfall, olyft OLIKA, `respec_varv` > 0, manuell approval
1875
+ * och experimentmarkören. Funktionen är ren (ingen I/O) så tabellen går
1876
+ * att testa rad för rad. */
1877
+ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
1878
+ const status = state.status
1879
+ const gid = state.request_id ?? '<gid>'
1880
+ const adress = state.request_id ?? post
1881
+ const stopp = state.stopp ?? null
1882
+ const runs = state.runs ?? []
1883
+ const nScore = maxN(runs, 'score')
1884
+ const gallandeRad =
1885
+ nScore > 0 ? [...runs].reverse().find((r) => r.file === `score-pricing-v${nScore}.json`) : null
1886
+ const olikaOlyft = gallandeRad?.double_run === 'olika' && !liftedByUtslag(state, 'score', nScore)
1887
+ const vantarDom = sparrSkal(state).length > 0
1888
+ const scorerStopp =
1889
+ scorerUtfall !== null && SCORE_STOP.includes(scorerUtfall.outcome) ? scorerUtfall : null
1890
+ const star = [
1891
+ status,
1892
+ stopp ? `stopp ${stopp.utfall} (${stopp.kalla})` : null,
1893
+ !stopp && scorerStopp ? `${scorerStopp.outcome} (${scorerStopp.fil})` : null,
1894
+ olikaOlyft ? 'OLIKA olyft' : vantarDom ? 'omkörning olyft' : null,
1895
+ (state.respec_varv ?? 0) > 0 ? `respec_varv ${state.respec_varv}` : null,
1896
+ state.approval?.source === 'manual' ? 'approval manual' : null,
1897
+ state.experiment === true ? 'experiment' : null,
1898
+ ]
1899
+ .filter((v) => v !== null)
1900
+ .join(', ')
1901
+ const rad = (vantar, nasta, egenStar = star) => ({ star: egenStar, vantar, nasta })
1902
+ const godkannEller = `godkänn ${gid} eller underkänn ${gid} <skäl>`
1903
+ const kunddialog = () =>
1904
+ rad(
1905
+ 'kunddialog',
1906
+ 'kunddialog; lägg posten på brädan via request-sync (🔍) om den saknas där, bedöm hoppar över den',
1907
+ )
1908
+ const tillbakaTillSpec = (val) =>
1909
+ rad(
1910
+ `avgör valet: ${val}`,
1911
+ `avgör valet, skriv det under ## Avgjorda val i kallmanifest.md, sedan archive.mjs respec och speca ${adress} om`,
1912
+ )
1913
+
1914
+ if (antalDelar > 0) return rad('se delarna', 'se delarna', `bunt (${antalDelar} delar)`)
1915
+ if (vantarDom) return rad('dom (döm)', `döm ${post}`)
1916
+ if (stopp) {
1917
+ const val = stopp.orsak ?? stopp.kalla
1918
+ if (stopp.utfall === 'KONCEPT' || stopp.utfall === 'DIAGNOS') return kunddialog()
1919
+ if (stopp.utfall === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(val)
1920
+ if (stopp.utfall === 'ESKALERA')
1921
+ return rad(`avgör valet: ${val}`, 'människa dömer, ingen ny specrunda')
1922
+ }
1923
+ if (state.experiment === true)
1924
+ return rad('inget (experiment, når aldrig Asana)', 'utvärdera rapport')
1925
+ const godkand = state.approval !== null && state.approval !== undefined
1926
+ if (status === 'spec_draft' || status === 'spec_synced') {
1927
+ if (godkand) return rad('körning av bedöm', `bedöm ${adress}`)
1928
+ return status === 'spec_draft'
1929
+ ? rad(
1930
+ 'godkännande av spec på brädan',
1931
+ `sync till brädan (request-sync 🔍), sedan ${godkannEller}`,
1932
+ )
1933
+ : rad('godkännande av spec på brädan', godkannEller)
1934
+ }
1935
+ if (status === 'assessed') {
1936
+ if (nScore === 0) return rad('körning av bedöm', `bedöm ${adress} (poängsättning saknas)`)
1937
+ if (scorerStopp) {
1938
+ const o = scorerStopp.outcome
1939
+ if (o === 'DELA_UPP')
1940
+ return rad('ny analysrunda per del', 'dela posten (split), sedan bedöm per del')
1941
+ if (o === 'OVER_SKALAN')
1942
+ return rad('manuell offert', 'offereras separat, delas aldrig vidare')
1943
+ if (o === 'KONCEPT' || o === 'DIAGNOS') return kunddialog()
1944
+ if (o === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(scorerStopp.fil)
1945
+ return rad(
1946
+ `människa: ${o} ur ${scorerStopp.fil}`,
1947
+ `visa skälet och stanna (detaljer ${adress} poäng)`,
1948
+ )
1949
+ }
1950
+ return rad(
1951
+ 'granskning av poäng på brädan',
1952
+ `sync till brädan (request-sync ⚖️), sedan ${godkannEller}`,
1953
+ )
1954
+ }
1955
+ if (status === 'priced')
1956
+ return rad(
1957
+ 'granskning av poäng på brädan',
1958
+ `sync till brädan (request-sync ⚖️) om inte gjort, sedan godkänn ${gid} (📤 Klar – till kund) eller underkänn ${gid} <skäl>`,
1959
+ )
1960
+ if (status === 'guard_run') return rad('inget', 'inget')
1961
+ return rad('–', '–', `okänt läge: ${status}`)
1962
+ }
1963
+
1780
1964
  /** Det SENASTE viktningsutslaget (om något) som lyfter en axel vid ett givet
1781
1965
  * gällande n — namnger axeln, har ett `n` som är EXAKT det gällande n:et
1782
1966
  * (inte "minst lika högt"), och bär en `galler_fil`.
@@ -31,7 +31,13 @@
31
31
  * det enda giltiga utfallet. Varje annat outcome är brott. Saknad etikett
32
32
  * fäller inget här, precheck äger etikettkravet.
33
33
  *
34
+ * Språkgrinden (#521): med `--sprak <svenska|engelska>` prövas varje
35
+ * kundtextfält mot språket. Två OLIKA funktionsord ur fel språk (hela ord,
36
+ * skiftlägesokänsligt, listorna i lib.mjs FUNKTIONSORD) är BROTT; diakritiska
37
+ * tecken är ingen signal. Utan flaggan görs ingen språkkontroll.
38
+ *
34
39
  * Användning: node contract-check.mjs <run.json> <spec.md> [impact.md]
40
+ * [--expect-id <id>] [--state <state.json>] [--sprak <svenska|engelska>]
35
41
  * Exit 0 = ok, 1 = kontraktsbrott (listade), 2 = användningsfel.
36
42
  */
37
43
  import { readFileSync } from 'node:fs'
@@ -51,6 +57,7 @@ import {
51
57
  ankarPoang,
52
58
  toppAnkare,
53
59
  TYPER,
60
+ felSprakOrd,
54
61
  } from './lib.mjs'
55
62
 
56
63
  const argv = process.argv.slice(2)
@@ -87,6 +94,25 @@ if (stateFlag !== -1) {
87
94
  statePath = v
88
95
  argv.splice(stateFlag, 2)
89
96
  }
97
+ // --sprak (#521) ger kundtextens språk: varje kundtextfält prövas mot det med
98
+ // funktionsordsheuristiken i lib.mjs (felSprakOrd). Utan flaggan görs ingen
99
+ // språkkontroll. Ett saknat eller okänt värde är, som ovan, ett dispatcherfel
100
+ // som stannar hårt: grinden gissar aldrig språk (ADR-0114).
101
+ let sprak = null
102
+ const sprakFlag = argv.indexOf('--sprak')
103
+ if (sprakFlag !== -1) {
104
+ const v = argv[sprakFlag + 1]
105
+ if (v === undefined || v.startsWith('--')) {
106
+ console.error('--sprak kräver ett värde (svenska eller engelska)')
107
+ process.exit(2)
108
+ }
109
+ if (v !== 'svenska' && v !== 'engelska') {
110
+ console.error(`--sprak: okänt språk "${v}" (giltiga: svenska, engelska)`)
111
+ process.exit(2)
112
+ }
113
+ sprak = v
114
+ argv.splice(sprakFlag, 2)
115
+ }
90
116
  // En felstavad flaggform (t.ex. `--expectid`, bindestrecket i "expect-id"
91
117
  // tappat) matchar aldrig `--expect-id` ovan och blir kvar i `argv` som ett
92
118
  // obehandlat argument. Utan den här kontrollen landade den tyst som
@@ -97,13 +123,15 @@ if (stateFlag !== -1) {
97
123
  // användningsfel, aldrig en gissning om vad anroparen menade.
98
124
  const okandFlagga = argv.find((a) => a.startsWith('--'))
99
125
  if (okandFlagga !== undefined) {
100
- console.error(`okänd flagga: ${okandFlagga} (giltiga: --expect-id <id>, --state <state.json>)`)
126
+ console.error(
127
+ `okänd flagga: ${okandFlagga} (giltiga: --expect-id <id>, --state <state.json>, --sprak <svenska|engelska>)`,
128
+ )
101
129
  process.exit(2)
102
130
  }
103
131
  const [jsonPath, specPath, impactPath] = argv
104
132
  if (!jsonPath || !specPath) {
105
133
  console.error(
106
- 'användning: node contract-check.mjs <run.json> <spec.md> [impact.md] [--expect-id <id>] [--state <state.json>]',
134
+ 'användning: node contract-check.mjs <run.json> <spec.md> [impact.md] [--expect-id <id>] [--state <state.json>] [--sprak <svenska|engelska>]',
107
135
  )
108
136
  process.exit(2)
109
137
  }
@@ -238,6 +266,12 @@ const checkCustomerText = (text, field) => {
238
266
  // visa vad som faktiskt stod i texten.
239
267
  const a = text.match(ANCHOR_RE)
240
268
  if (a) warnings.push(`${field}: "${a[0]}" — ankar-id eller pappersformat? Kontrollera`)
269
+ // Språkgrinden (#521), också mot originalet: två OLIKA funktionsord ur fel
270
+ // språk fäller, ett enda är ett lån. Raden har med flit en annan form än
271
+ // protokollterm-brottet: kundtextfixen i kontroll.mjs byter ord, den
272
+ // översätter inte, så brottet ska till omdispatch.
273
+ if (sprak !== null && felSprakOrd(text, sprak).length >= 2)
274
+ problems.push(`${field}: kundtext på fel språk (väntat ${sprak})`)
241
275
  }
242
276
 
243
277
  /** Poängupplösning: 0.25-steg under 3, heltal från 3, alltid > 0.
@@ -24,6 +24,9 @@
24
24
  * Användning:
25
25
  * node kontroll.mjs <svar.txt> <spec.md> <impact.md> --expect-id <id> --ut <ren.json>
26
26
  * [--fixa-kundtext] [--claude <kommando>] [--fix-modell sonnet] [--fix-fil <rå.json>]
27
+ * [--state <state.json>] [--sprak <svenska|engelska>]
28
+ * `--state` och `--sprak` går rakt vidare till contract-check (split-status,
29
+ * #503; kundtextens språk, #521).
27
30
  * stdout: MODELL:, LÄSTA:, [AVVIKELSE:], REPARERAT: (0..n), kedjans rader, sist
28
31
  * "OK: kontrollkedjan håller" (exit 0) eller "BROTT (n):" + rader (exit 1).
29
32
  * Exit 2 = användningsfel eller ett delscript med exit 2.
@@ -47,15 +50,22 @@ import {
47
50
  const HAR = path.dirname(fileURLToPath(import.meta.url))
48
51
  export const MAX_FIXRUNDOR = 1
49
52
  const VIK = { hög: 'hog', låg: 'lag', större: 'storre' }
53
+ // coverage_post.yta går genom checkCustomerText i contract-check som varje
54
+ // annat kundnära fält (ADR-0119). Saknades det här tvingade ett flaggat ord i
55
+ // täckningsposten en betald omdispatch i stället för omskrivningen (#517).
50
56
  const KUNDTEXT_RE =
51
- /^- (customer_rationale|conditions\[\d+\]|assumptions_stated\[\d+\]|alternatives\[\d+\]\.(?:label|adds)): protokollterm "([^"]+)" i kundtext$/u
57
+ /^- (customer_rationale|conditions\[\d+\]|assumptions_stated\[\d+\]|alternatives\[\d+\]\.(?:label|adds)|coverage_post\.yta): protokollterm "([^"]+)" i kundtext$/u
52
58
 
53
- const VARDEFLAGGOR = ['expect-id', 'ut', 'claude', 'fix-modell', 'fix-fil', 'state']
59
+ // --sprak (#521) går rakt vidare till contract-check. Språkbrottet den ger
60
+ // matchar med flit inte KUNDTEXT_RE: kundtextfixen byter ett ord, den
61
+ // översätter inte, så brottet går till omdispatch.
62
+ const VARDEFLAGGOR = ['expect-id', 'ut', 'claude', 'fix-modell', 'fix-fil', 'state', 'sprak']
54
63
  const BOOLFLAGGOR = ['fixa-kundtext']
55
64
 
56
65
  export const ANVANDNING = [
57
66
  'användning: kontroll.mjs <svar.txt> <spec.md> <impact.md> --expect-id <id> --ut <ren.json>',
58
- ' [--fixa-kundtext] [--claude <kommando>] [--fix-modell sonnet] [--fix-fil <rå.json>] [--state <state.json>]',
67
+ ' [--fixa-kundtext] [--claude <kommando>] [--fix-modell sonnet] [--fix-fil <rå.json>]',
68
+ ' [--state <state.json>] [--sprak <svenska|engelska>]',
59
69
  ].join('\n')
60
70
 
61
71
  // ---------------------------------------------------------------------------
@@ -103,6 +113,7 @@ export function tolkaArgv(argv) {
103
113
  fixModell: enkla['fix-modell'] ?? 'sonnet',
104
114
  fixFil: enkla['fix-fil'] ?? null,
105
115
  statePath: enkla.state ?? null,
116
+ sprak: enkla.sprak ?? null,
106
117
  }
107
118
  }
108
119
 
@@ -268,11 +279,15 @@ export function kundtextBrott(brottRader) {
268
279
  }
269
280
 
270
281
  /** Ny doc med fältet (customer_rationale, conditions[n], assumptions_stated[n],
271
- * alternatives[n].label|adds) ersatt. */
282
+ * alternatives[n].label|adds, coverage_post.yta) ersatt. Ett punktfält utan
283
+ * index är ett objektfält (ADR-0119): bara undernyckeln byts, syskonen
284
+ * (omfattning) står kvar. */
272
285
  export function ersattFalt(doc, falt, text) {
273
286
  const m = /^(\w+)(?:\[(\d+)\])?(?:\.(\w+))?$/u.exec(falt)
274
287
  if (!m) throw new Error(`okänt fält: ${falt}`)
275
288
  const [, namn, idx, under] = m
289
+ if (idx === undefined && under !== undefined)
290
+ return { ...doc, [namn]: { ...(doc[namn] ?? {}), [under]: text } }
276
291
  if (idx === undefined) return { ...doc, [namn]: text }
277
292
  const lista = [...(doc[namn] ?? [])]
278
293
  const i = Number(idx)
@@ -282,11 +297,12 @@ export function ersattFalt(doc, falt, text) {
282
297
 
283
298
  /** Läser fältets nuvarande text, motsvarigheten till ersattFalt fast läsande.
284
299
  * Samma fältgrammatik: customer_rationale, conditions[n],
285
- * assumptions_stated[n], alternatives[n].label|adds. */
286
- function hamtaFalt(doc, falt) {
300
+ * assumptions_stated[n], alternatives[n].label|adds, coverage_post.yta. */
301
+ export function hamtaFalt(doc, falt) {
287
302
  const m = /^(\w+)(?:\[(\d+)\])?(?:\.(\w+))?$/u.exec(falt)
288
303
  if (!m) throw new Error(`okänt fält: ${falt}`)
289
304
  const [, namn, idx, under] = m
305
+ if (idx === undefined && under !== undefined) return doc[namn]?.[under]
290
306
  if (idx === undefined) return doc[namn]
291
307
  const post = (doc[namn] ?? [])[Number(idx)]
292
308
  return under === undefined ? post : post?.[under]
@@ -464,6 +480,7 @@ function korKedjaOchFixa({
464
480
  claudeCmd,
465
481
  fixModell,
466
482
  statePath,
483
+ sprak,
467
484
  }) {
468
485
  const kor = (script, args) =>
469
486
  spawnSync(process.execPath, [path.join(HAR, script), ...args], { encoding: 'utf8' })
@@ -488,6 +505,7 @@ function korKedjaOchFixa({
488
505
  '--expect-id',
489
506
  expectId,
490
507
  ...(statePath ? ['--state', statePath] : []),
508
+ ...(sprak ? ['--sprak', sprak] : []),
491
509
  ],
492
510
  false,
493
511
  ],