@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.
- package/CHANGELOG.md +18 -0
- package/library/global/agents/request-assessor.md +13 -5
- package/library/global/agents/request-scorer.md +3 -1
- package/library/global/skills/request/SKILL.md +148 -27
- package/library/global/skills/request/scripts/archive.mjs +192 -8
- package/library/global/skills/request/scripts/contract-check.mjs +36 -2
- package/library/global/skills/request/scripts/kontroll.mjs +24 -6
- package/library/global/skills/request/scripts/korning.mjs +183 -50
- package/library/global/skills/request/scripts/lib.mjs +77 -13
- package/library/global/skills/request/scripts/precheck.mjs +16 -0
- package/library/global/skills/request/scripts/report.mjs +37 -2
- package/library/global/skills/request/scripts/svep.mjs +145 -48
- package/library/global/skills/request-assess-impact/SKILL.md +13 -0
- package/library/global/skills/request-score/SKILL.md +6 -0
- package/library/global/skills/request-sync/SKILL.md +23 -19
- package/library/global/skills/request-write-spec/SKILL.md +5 -1
- package/package.json +1 -1
|
@@ -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 --
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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>]
|
|
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
|
],
|