@haus-tech/haus-workflow 5.9.1 → 5.10.1
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 +13 -0
- package/dist/cli.js +13 -19
- package/library/catalog/manifest.json +4 -4
- package/library/global/agents/request-assessor.md +5 -1
- package/library/global/agents/request-specifier.md +3 -1
- package/library/global/skills/request/SKILL.md +394 -59
- package/library/global/skills/request/scripts/archive.mjs +546 -49
- package/library/global/skills/request/scripts/korning.mjs +41 -4
- package/library/global/skills/request/scripts/lib.mjs +189 -0
- package/library/global/skills/request/scripts/precheck.mjs +105 -130
- package/library/global/skills/request-assess-impact/SKILL.md +12 -0
- package/library/global/skills/request-sync/SKILL.md +61 -100
- package/library/global/skills/request-write-spec/SKILL.md +15 -1
- package/package.json +3 -3
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* så samtidiga svep-processer aldrig tappar varandras rader (#493). Del 5
|
|
18
18
|
* (#530): nasta-steg, läs-enbart tabell över var posten står, vad en människa
|
|
19
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`).
|
|
20
|
+
* egna grindar (regeltabellen står ovanför `nastaStegFor`). Del 6 (designa,
|
|
21
|
+
* spec 2026-09-24): add-design, som verifierar, lägger undan, kopierar in
|
|
22
|
+
* och bokför designdokumentet i state.design utan runs-rad.
|
|
21
23
|
*
|
|
22
24
|
* Användning:
|
|
23
25
|
* node archive.mjs add-run <katalog> --file F --skill S --agent A --model M [flaggor]
|
|
@@ -28,12 +30,15 @@
|
|
|
28
30
|
* node archive.mjs sparr <katalog>
|
|
29
31
|
* node archive.mjs utslag <katalog> --typ viktning|kalibrering|sakandring --beslutad-av N [...]
|
|
30
32
|
* node archive.mjs set-status <katalog> <status>
|
|
31
|
-
* node archive.mjs stopp <katalog> --utfall KONCEPT|DIAGNOS|TILLBAKA_TILL_SPEC|ESKALERA --kalla spec.md|impact.md|granskarblock.md [--orsak <text>]
|
|
33
|
+
* node archive.mjs stopp <katalog> --utfall KONCEPT|DIAGNOS|TILLBAKA_TILL_SPEC|ESKALERA --kalla spec.md|impact.md|granskarblock.md|design-avvisad-<datum>.md [--orsak <text>]
|
|
32
34
|
* node archive.mjs respec <katalog>
|
|
35
|
+
* node archive.mjs add-design <katalog> --file <absolut sökväg till designdokumentet>
|
|
33
36
|
* node archive.mjs bluf <katalog>
|
|
34
37
|
* node archive.mjs nasta-steg <katalog>
|
|
35
38
|
* node archive.mjs kalibrering <arkivrot>
|
|
36
39
|
* node archive.mjs experiment <experimentkatalog> [--satt K=V]... [--variant M]... [--korning M/kN --status S [--orsak T] [--pid N]]
|
|
40
|
+
* node archive.mjs set-split-gid <del-katalog> --gid <gid>
|
|
41
|
+
* node archive.mjs paraply <katalog> --file <spec>
|
|
37
42
|
* Exit 0 = ok, 1 = spärrad (sparr/set-status priced), 2 = användnings-/tillståndsfel.
|
|
38
43
|
*/
|
|
39
44
|
import {
|
|
@@ -48,6 +53,8 @@ import {
|
|
|
48
53
|
readdirSync,
|
|
49
54
|
existsSync,
|
|
50
55
|
mkdirSync,
|
|
56
|
+
realpathSync,
|
|
57
|
+
copyFileSync,
|
|
51
58
|
} from 'node:fs'
|
|
52
59
|
import path from 'node:path'
|
|
53
60
|
import { createHash } from 'node:crypto'
|
|
@@ -62,13 +69,18 @@ import {
|
|
|
62
69
|
findUpward,
|
|
63
70
|
CLASS_UTFALL,
|
|
64
71
|
SCORE_OUTCOMES,
|
|
72
|
+
designFrontmatter,
|
|
73
|
+
specConflicts,
|
|
74
|
+
katalogId,
|
|
75
|
+
specForm,
|
|
76
|
+
SPEC_HEADINGS,
|
|
65
77
|
} from './lib.mjs'
|
|
66
78
|
|
|
67
79
|
const [cmd, katalog, ...rest] = process.argv.slice(2)
|
|
68
80
|
|
|
69
81
|
if (!cmd || !katalog) {
|
|
70
82
|
console.error(
|
|
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]',
|
|
83
|
+
'användning: archive.mjs <add-run|next-n|ny-korning|ny-del|proveniens|sparr|utslag|set-status|stopp|respec|add-design|set-split-gid|paraply|bluf|nasta-steg|kalibrering|experiment> <katalog> [flaggor]',
|
|
72
84
|
)
|
|
73
85
|
process.exit(2)
|
|
74
86
|
}
|
|
@@ -188,6 +200,11 @@ const assertRowArrayField = (parsed, field, d = katalog) => {
|
|
|
188
200
|
}
|
|
189
201
|
}
|
|
190
202
|
|
|
203
|
+
/** Ett Asana-gid: bara siffror (ADR-0144, split.gid). Delat av
|
|
204
|
+
* readStateOrThrow och set-split-gid så läs- och skrivsidan aldrig kan
|
|
205
|
+
* godkänna olika former. */
|
|
206
|
+
const GID_RE = /^\d+$/
|
|
207
|
+
|
|
191
208
|
const readStateOrThrow = (d = katalog) => {
|
|
192
209
|
let raw
|
|
193
210
|
try {
|
|
@@ -217,6 +234,40 @@ const readStateOrThrow = (d = katalog) => {
|
|
|
217
234
|
) {
|
|
218
235
|
throw new ArkivFel(`${statePath(d)} har ett stopp-fält som varken är null eller ett objekt`)
|
|
219
236
|
}
|
|
237
|
+
// `design` är null, saknat (äldre arkiv) eller ett objekt { path, file,
|
|
238
|
+
// sha256, spec_sha256, date } skrivet av add-design. Allt annat är trasigt:
|
|
239
|
+
// nasta-steg och precheck --design läser fältet, och en sträng där hade
|
|
240
|
+
// tyst lästs som "designad".
|
|
241
|
+
if (
|
|
242
|
+
parsed.design !== undefined &&
|
|
243
|
+
parsed.design !== null &&
|
|
244
|
+
(typeof parsed.design !== 'object' || Array.isArray(parsed.design))
|
|
245
|
+
) {
|
|
246
|
+
throw new ArkivFel(`${statePath(d)} har ett design-fält som varken är null eller ett objekt`)
|
|
247
|
+
}
|
|
248
|
+
// ADR-0144 beslut 6: split.gid är saknat, null eller ett Asana-gid som
|
|
249
|
+
// sträng, skrivet av set-split-gid. Allt annat är trasigt: nasta-steg läser
|
|
250
|
+
// fältet för att avgöra om delens deluppgift finns, och ett tal eller en
|
|
251
|
+
// sträng med bokstäver där hade tyst lästs som "synkad".
|
|
252
|
+
if (
|
|
253
|
+
parsed.split !== null &&
|
|
254
|
+
typeof parsed.split === 'object' &&
|
|
255
|
+
parsed.split.gid !== undefined &&
|
|
256
|
+
parsed.split.gid !== null &&
|
|
257
|
+
(typeof parsed.split.gid !== 'string' || !GID_RE.test(parsed.split.gid))
|
|
258
|
+
) {
|
|
259
|
+
throw new ArkivFel(`${statePath(d)} har ett split.gid som inte är ett Asana-gid (bara siffror)`)
|
|
260
|
+
}
|
|
261
|
+
// ADR-0144 beslut 5: `umbrella` är saknat eller ett objekt { date, sha256 }
|
|
262
|
+
// skrivet av paraply. Allt annat är trasigt, samma regel som för stopp och
|
|
263
|
+
// design.
|
|
264
|
+
if (
|
|
265
|
+
parsed.umbrella !== undefined &&
|
|
266
|
+
parsed.umbrella !== null &&
|
|
267
|
+
(typeof parsed.umbrella !== 'object' || Array.isArray(parsed.umbrella))
|
|
268
|
+
) {
|
|
269
|
+
throw new ArkivFel(`${statePath(d)} har ett umbrella-fält som varken är null eller ett objekt`)
|
|
270
|
+
}
|
|
220
271
|
return parsed
|
|
221
272
|
}
|
|
222
273
|
|
|
@@ -529,9 +580,10 @@ const isKnownFile = (file) => FILE_PATTERNS.some((re) => re.test(file))
|
|
|
529
580
|
* spec-v1-avvisad.md-raden) bär tomt underlag_sha256, en
|
|
530
581
|
* impact(-superseded)-rad bär bara spec.md, en pricing-rad bär båda.
|
|
531
582
|
*/
|
|
532
|
-
const underlagFor = (file) => {
|
|
583
|
+
const underlagFor = (file, d = katalog) => {
|
|
533
584
|
if (SPEC_RE.test(file)) return []
|
|
534
|
-
if (IMPACT_RE.test(file))
|
|
585
|
+
if (IMPACT_RE.test(file))
|
|
586
|
+
return existsSync(path.join(d, 'design.md')) ? ['spec.md', 'design.md'] : ['spec.md']
|
|
535
587
|
return ['spec.md', 'impact.md']
|
|
536
588
|
}
|
|
537
589
|
|
|
@@ -566,11 +618,27 @@ const SCORE_STOP = [
|
|
|
566
618
|
* (KONCEPT/DIAGNOS, ADR-0105), motsägelse-etiketten i impact.md
|
|
567
619
|
* (TILLBAKA_TILL_SPEC, ADR-0106) och raden Prisavgörande öppet val i
|
|
568
620
|
* granskarblock.md (TILLBAKA_TILL_SPEC eller ESKALERA, ADR-0118) är de tre
|
|
569
|
-
* källor som kan bära ett stopp UTAN scorer.
|
|
570
|
-
*
|
|
571
|
-
*
|
|
621
|
+
* literala källor som kan bära ett stopp UTAN scorer. Den fjärde är ett
|
|
622
|
+
* mönster: ett designdokument som add-design vägrade på "## Spec conflicts"
|
|
623
|
+
* kopieras in som kvitto (design-avvisad-<datum>.md, `-2` vid kollision) och
|
|
624
|
+
* stoppar som TILLBAKA_TILL_SPEC eller ESKALERA (designa steg 5,
|
|
625
|
+
* slutgranskning I1). DELA_UPP, OVER_SKALAN och INPUT_SAKNAS är scorerns
|
|
626
|
+
* utfall och når arkivet via prisfilen, aldrig via stopp; ESKALERA når
|
|
627
|
+
* arkivet på båda vägarna. */
|
|
572
628
|
const STOPP_UTFALL = ['KONCEPT', 'DIAGNOS', 'TILLBAKA_TILL_SPEC', 'ESKALERA']
|
|
573
629
|
const STOPP_KALLOR = ['spec.md', 'impact.md', 'granskarblock.md']
|
|
630
|
+
const DESIGN_AVVISAD_RE = /^design-avvisad-\d{4}-\d{2}-\d{2}(-\d+)?\.md$/
|
|
631
|
+
|
|
632
|
+
/** Ett designkonfliktstopp (slutgranskning I2): assessorn fann designen
|
|
633
|
+
* obyggbar, motsägelseraden i impact.md börjar med `design conflict:` och
|
|
634
|
+
* bedöm bokförde den texten som `orsak`. En omdesign besvarar det, inte en
|
|
635
|
+
* respec: nasta-steg routar till designa och add-design nollar stoppet.
|
|
636
|
+
* Delad av de två så de aldrig kan läsa samma stopp olika. */
|
|
637
|
+
const arDesignkonflikt = (stopp) =>
|
|
638
|
+
stopp?.kalla === 'impact.md' &&
|
|
639
|
+
stopp.utfall === 'TILLBAKA_TILL_SPEC' &&
|
|
640
|
+
typeof stopp.orsak === 'string' &&
|
|
641
|
+
/^design conflict:/i.test(stopp.orsak.trim())
|
|
574
642
|
|
|
575
643
|
/** Tak för antal körningar per modell. Skyddet är mot en trasig loop, inte mot
|
|
576
644
|
* Tims arbetssätt: 999 körningar av samma modell på samma post är alltid ett
|
|
@@ -592,12 +660,14 @@ const PROVENIENS_REFERENSER = [
|
|
|
592
660
|
'skills/request/SKILL.md',
|
|
593
661
|
]
|
|
594
662
|
|
|
595
|
-
/** Kända statusvärden (SKILL.md: "spec_draft | spec_synced |
|
|
596
|
-
* priced | guard_run"). `
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
|
|
663
|
+
/** Kända statusvärden (SKILL.md: "spec_draft | spec_synced | designed |
|
|
664
|
+
* assessed | priced | guard_run"). `designed` (designa, spec 2026-09-24)
|
|
665
|
+
* ligger mellan godkännandet och analysen: add-design sätter det. `set-status` skrev
|
|
666
|
+
* tidigare VILKEN STRÄNG SOM HELST rakt in i state.json — inklusive en
|
|
667
|
+
* flaggformad sträng om anroparen råkat glömma statusvärdet (`set-status
|
|
668
|
+
* <katalog> --nagot-annat`), vilket hade gett en tyst, obegriplig status i
|
|
669
|
+
* stället för ett fel. */
|
|
670
|
+
const STATUSAR = ['spec_draft', 'spec_synced', 'designed', 'assessed', 'priced', 'guard_run']
|
|
601
671
|
|
|
602
672
|
/** Körningsstatus i experiment.json, exakt de literaler SKILL.md:s
|
|
603
673
|
* återupptagningsregel och svep.mjs läser. */
|
|
@@ -669,6 +739,7 @@ const skelett = (requestId, { bundle, split } = {}) => ({
|
|
|
669
739
|
stopp: null,
|
|
670
740
|
respec_varv: 0,
|
|
671
741
|
coverage_post: null,
|
|
742
|
+
design: null,
|
|
672
743
|
runs: [],
|
|
673
744
|
})
|
|
674
745
|
|
|
@@ -1478,6 +1549,7 @@ if (cmd === 'add-run') {
|
|
|
1478
1549
|
// Stopputfall UTAN scorer (ADR-0105/0106/0118): en kandidatspec routar direkt
|
|
1479
1550
|
// till KONCEPT/DIAGNOS, en analys med motsägelse direkt till
|
|
1480
1551
|
// TILLBAKA_TILL_SPEC, och ett prisavgörande öppet val i granskarblock.md
|
|
1552
|
+
// eller ett vägrat designdokuments kvitto (design-avvisad-<datum>.md)
|
|
1481
1553
|
// direkt till TILLBAKA_TILL_SPEC eller ESKALERA. Kommandot litar aldrig på
|
|
1482
1554
|
// anroparens påstående: källan måste bära det.
|
|
1483
1555
|
avvisaOkandaFlaggor('stopp', ['utfall', 'kalla', 'orsak'])
|
|
@@ -1488,8 +1560,11 @@ if (cmd === 'add-run') {
|
|
|
1488
1560
|
console.error(`okänt --utfall: ${utfall} (giltiga: ${STOPP_UTFALL.join(', ')})`)
|
|
1489
1561
|
process.exit(2)
|
|
1490
1562
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1563
|
+
const arDesignKvitto = DESIGN_AVVISAD_RE.test(kalla)
|
|
1564
|
+
if (!STOPP_KALLOR.includes(kalla) && !arDesignKvitto) {
|
|
1565
|
+
console.error(
|
|
1566
|
+
`okänd --kalla: ${kalla} (giltiga: ${STOPP_KALLOR.join(', ')}, design-avvisad-<datum>.md)`,
|
|
1567
|
+
)
|
|
1493
1568
|
process.exit(2)
|
|
1494
1569
|
}
|
|
1495
1570
|
const kallaPath = path.join(katalog, kalla)
|
|
@@ -1515,6 +1590,27 @@ if (cmd === 'add-run') {
|
|
|
1515
1590
|
)
|
|
1516
1591
|
process.exit(2)
|
|
1517
1592
|
}
|
|
1593
|
+
} else if (arDesignKvitto) {
|
|
1594
|
+
// Slutgranskning I1: kvittot är det vägrade designdokumentet självt, och
|
|
1595
|
+
// bara ett dokument med minst en konflikt under "## Spec conflicts" är ett
|
|
1596
|
+
// stopp. Statusen rörs inte, som för spec.md: designen bokfördes aldrig.
|
|
1597
|
+
const konflikter = specConflicts(text)
|
|
1598
|
+
if (konflikter === null || konflikter.length === 0) {
|
|
1599
|
+
console.error(`${kallaPath} bär inga konflikter under "## Spec conflicts", kan inte stoppas`)
|
|
1600
|
+
process.exit(2)
|
|
1601
|
+
}
|
|
1602
|
+
if (!['TILLBAKA_TILL_SPEC', 'ESKALERA'].includes(utfall)) {
|
|
1603
|
+
console.error(
|
|
1604
|
+
`en designkonflikt i ${kalla} stoppar som TILLBAKA_TILL_SPEC eller ESKALERA, inte ${utfall}`,
|
|
1605
|
+
)
|
|
1606
|
+
process.exit(2)
|
|
1607
|
+
}
|
|
1608
|
+
if (utfall === 'ESKALERA' && !orsak) {
|
|
1609
|
+
console.error(
|
|
1610
|
+
'ESKALERA kräver --orsak: en människa ska se varför posten inte gick tillbaka till specen en gång till',
|
|
1611
|
+
)
|
|
1612
|
+
process.exit(2)
|
|
1613
|
+
}
|
|
1518
1614
|
} else if (kalla === 'granskarblock.md') {
|
|
1519
1615
|
// ADR-0118: bara ett ja på raden Prisavgörande öppet val är ett stopp.
|
|
1520
1616
|
if (prisavgorandeVal(text) === null) {
|
|
@@ -1553,10 +1649,21 @@ if (cmd === 'add-run') {
|
|
|
1553
1649
|
|
|
1554
1650
|
const lockPath = acquireLock()
|
|
1555
1651
|
let skrivet = false
|
|
1652
|
+
let ersatter = false
|
|
1556
1653
|
try {
|
|
1557
1654
|
const state = readStateOrThrow()
|
|
1558
1655
|
const befintlig = state.stopp ?? null
|
|
1559
|
-
if (befintlig !== null) {
|
|
1656
|
+
if (befintlig !== null && arDesignkonflikt(befintlig) && arDesignKvitto) {
|
|
1657
|
+
// Slutgranskning omgång 2 (F1): omdesignen som designkonfliktstoppet
|
|
1658
|
+
// routade till vägrades på "## Spec conflicts", så specen är problemet
|
|
1659
|
+
// och spec-vägen vinner. Kvittot ersätter stoppet; utan det föll stopp
|
|
1660
|
+
// på write-once nedan och nasta-steg sa designa igen, en återvändsgränd.
|
|
1661
|
+
// Statusen rörs inte, orsaksreglerna är kvittots egna ovan.
|
|
1662
|
+
state.stopp = { utfall, kalla, date: today(), ...(orsak ? { orsak } : {}) }
|
|
1663
|
+
writeStateOrThrow(state)
|
|
1664
|
+
skrivet = true
|
|
1665
|
+
ersatter = true
|
|
1666
|
+
} else if (befintlig !== null) {
|
|
1560
1667
|
// Write-once på utfall+kalla; datumet och orsaken ingår inte, ett
|
|
1561
1668
|
// identiskt omanrop en annan dag (eller med en annan orsakstext) är en
|
|
1562
1669
|
// no-op.
|
|
@@ -1570,9 +1677,10 @@ if (cmd === 'add-run') {
|
|
|
1570
1677
|
}
|
|
1571
1678
|
} else {
|
|
1572
1679
|
state.stopp = { utfall, kalla, date: today(), ...(orsak ? { orsak } : {}) }
|
|
1573
|
-
// impact.md: posten gick genom bedöm och är analyserad. spec.md
|
|
1574
|
-
// granskarblock.md: kandidaten
|
|
1575
|
-
//
|
|
1680
|
+
// impact.md: posten gick genom bedöm och är analyserad. spec.md,
|
|
1681
|
+
// granskarblock.md och ett designkvitto: kandidaten, det prisavgörande
|
|
1682
|
+
// valet eller den vägrade designen passerade aldrig bedöms grind,
|
|
1683
|
+
// statusen står kvar som förut.
|
|
1576
1684
|
if (kalla === 'impact.md') state.status = 'assessed'
|
|
1577
1685
|
writeStateOrThrow(state)
|
|
1578
1686
|
skrivet = true
|
|
@@ -1587,29 +1695,26 @@ if (cmd === 'add-run') {
|
|
|
1587
1695
|
}
|
|
1588
1696
|
releaseLock(lockPath)
|
|
1589
1697
|
console.log(
|
|
1590
|
-
|
|
1698
|
+
ersatter
|
|
1699
|
+
? `OK: stopp ${utfall} ur ${kalla} (ersätter designkonflikten ur impact.md)`
|
|
1700
|
+
: skrivet
|
|
1701
|
+
? `OK: stopp ${utfall} ur ${kalla}`
|
|
1702
|
+
: `OK: stopp ${utfall} ur ${kalla} oförändrat`,
|
|
1591
1703
|
)
|
|
1592
1704
|
} else if (cmd === 'respec') {
|
|
1593
1705
|
// Speca steg 5 som kommando: en ny spec över befintlig katalog invaliderar
|
|
1594
|
-
// analysen, godkännandet, domarna och stoppet. runs[] rörs
|
|
1595
|
-
// är append-only; den gamla analysen
|
|
1706
|
+
// analysen, designen, godkännandet, domarna och stoppet. runs[] rörs
|
|
1707
|
+
// aldrig, arkivet är append-only; den gamla analysen och designen döps
|
|
1708
|
+
// om, raderas inte.
|
|
1596
1709
|
avvisaOkandaFlaggor('respec', [])
|
|
1597
1710
|
const lockPath = acquireLock()
|
|
1598
1711
|
let antal = 0
|
|
1599
1712
|
try {
|
|
1600
1713
|
const state = readStateOrThrow()
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
for (let n = 2; existsSync(mal); n++) mal = `${bas}-${n}.md`
|
|
1606
|
-
try {
|
|
1607
|
-
renameSync(impact, mal)
|
|
1608
|
-
} catch (e) {
|
|
1609
|
-
throw new ArkivFel(`kunde inte döpa om ${impact} till ${mal}: ${e.message}`)
|
|
1610
|
-
}
|
|
1611
|
-
antal++
|
|
1612
|
-
}
|
|
1714
|
+
if (laggUndan(katalog, 'impact')) antal++
|
|
1715
|
+
// Designa (spec 2026-09-24): en ny spec gör den gamla designen inaktuell
|
|
1716
|
+
// av samma skäl som den gör analysen inaktuell. Läggs undan, raderas inte.
|
|
1717
|
+
if (laggUndan(katalog, 'design')) antal++
|
|
1613
1718
|
const nytt = { ...state }
|
|
1614
1719
|
if (nytt.status !== 'spec_draft') {
|
|
1615
1720
|
nytt.status = 'spec_draft'
|
|
@@ -1623,10 +1728,14 @@ if (cmd === 'add-run') {
|
|
|
1623
1728
|
nytt.granskarutslag = []
|
|
1624
1729
|
antal++
|
|
1625
1730
|
}
|
|
1626
|
-
if (nytt.stopp !== null) {
|
|
1731
|
+
if (nytt.stopp !== null && nytt.stopp !== undefined) {
|
|
1627
1732
|
nytt.stopp = null
|
|
1628
1733
|
antal++
|
|
1629
1734
|
}
|
|
1735
|
+
if (nytt.design !== null && nytt.design !== undefined) {
|
|
1736
|
+
nytt.design = null
|
|
1737
|
+
antal++
|
|
1738
|
+
}
|
|
1630
1739
|
// Monoton räknare (#505): varje respec-anrop är ett nytt varv oavsett om
|
|
1631
1740
|
// övriga fält redan var normaliserade, till skillnad från fälten ovan
|
|
1632
1741
|
// finns inget "redan rätt värde" att jämföra mot, så det här villkoret
|
|
@@ -1644,6 +1753,93 @@ if (cmd === 'add-run') {
|
|
|
1644
1753
|
}
|
|
1645
1754
|
releaseLock(lockPath)
|
|
1646
1755
|
console.log(`OK: respec (${antal} ändringar)`)
|
|
1756
|
+
} else if (cmd === 'add-design') {
|
|
1757
|
+
// Designa steg 5 (spec 2026-09-24): designdokumentet verifieras, kopieras
|
|
1758
|
+
// byteidentiskt in som design.md, bokförs i state.design och sätter status
|
|
1759
|
+
// designed. Ingen runs-rad: designen är ingen agentdispatch, kvittot är
|
|
1760
|
+
// fältet självt, write-once per version (en ny version lägger undan den
|
|
1761
|
+
// gamla som design-superseded-<datum>.md, se laggUndan).
|
|
1762
|
+
avvisaOkandaFlaggor('add-design', ['file'])
|
|
1763
|
+
const fil = requireFlag('file')
|
|
1764
|
+
const lockPath = acquireLock()
|
|
1765
|
+
let kvitto
|
|
1766
|
+
try {
|
|
1767
|
+
const state = readStateOrThrow()
|
|
1768
|
+
kvitto = laggTillDesign(state, fil)
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
releaseLock(lockPath)
|
|
1771
|
+
if (e instanceof ArkivFel) {
|
|
1772
|
+
console.error(e.message)
|
|
1773
|
+
process.exit(2)
|
|
1774
|
+
}
|
|
1775
|
+
throw e
|
|
1776
|
+
}
|
|
1777
|
+
releaseLock(lockPath)
|
|
1778
|
+
console.log(`OK: design ${kvitto.design.path} (${kvitto.design.sha256})`)
|
|
1779
|
+
if (kvitto.impactUndanlagd) console.log('OK: impact.md undanlagd (designkonfliktstoppet löst)')
|
|
1780
|
+
} else if (cmd === 'set-split-gid') {
|
|
1781
|
+
// ADR-0144 beslut 6: delens Asana-deluppgift bokförs i split.gid när
|
|
1782
|
+
// request-sync har skapat den, så en omsynk uppdaterar deluppgiften i
|
|
1783
|
+
// stället för att skapa en till. Write-once: samma gid igen är en no-op,
|
|
1784
|
+
// ett annat gid vägras (en del har en deluppgift, aldrig två).
|
|
1785
|
+
avvisaOkandaFlaggor('set-split-gid', ['gid'])
|
|
1786
|
+
const gid = requireFlag('gid')
|
|
1787
|
+
if (!GID_RE.test(gid)) {
|
|
1788
|
+
console.error(`--gid måste vara ett Asana-gid (bara siffror), fick "${gid}"`)
|
|
1789
|
+
process.exit(2)
|
|
1790
|
+
}
|
|
1791
|
+
const lockPath = acquireLock()
|
|
1792
|
+
let oforandrat = false
|
|
1793
|
+
try {
|
|
1794
|
+
const state = readStateOrThrow()
|
|
1795
|
+
const split = state.split
|
|
1796
|
+
if (split === null || typeof split !== 'object' || split.parent == null)
|
|
1797
|
+
throw new ArkivFel(
|
|
1798
|
+
`${statePath()} har ingen split (split.parent saknas), set-split-gid gäller bara en delningsdel`,
|
|
1799
|
+
)
|
|
1800
|
+
if (split.gid != null && split.gid !== gid)
|
|
1801
|
+
throw new ArkivFel(
|
|
1802
|
+
`${statePath()} har redan split.gid "${split.gid}" som skiljer sig från "${gid}"; en del har en deluppgift, aldrig två`,
|
|
1803
|
+
)
|
|
1804
|
+
if (split.gid === gid) oforandrat = true
|
|
1805
|
+
else writeStateOrThrow({ ...state, split: { ...split, gid } })
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
releaseLock(lockPath)
|
|
1808
|
+
if (e instanceof ArkivFel) {
|
|
1809
|
+
console.error(e.message)
|
|
1810
|
+
process.exit(2)
|
|
1811
|
+
}
|
|
1812
|
+
throw e
|
|
1813
|
+
}
|
|
1814
|
+
releaseLock(lockPath)
|
|
1815
|
+
console.log(oforandrat ? `OK: split-gid ${gid} oförändrat` : `OK: split-gid ${gid}`)
|
|
1816
|
+
} else if (cmd === 'paraply') {
|
|
1817
|
+
// ADR-0144 beslut 5: förälderns spec skrivs om som en paraplyspec efter ett
|
|
1818
|
+
// ja. Den gamla spec.md läggs undan (spec-superseded-<datum>.md),
|
|
1819
|
+
// paraplyet kopieras in byteidentiskt, titeln följer dess rubrik och
|
|
1820
|
+
// state.umbrella bokför datum och hash. Godkännande, design, runs, stopp och
|
|
1821
|
+
// delarna rörs inte: delarna bär sina ärvda kopior av den godkända specen,
|
|
1822
|
+
// den som deras assessorer körde mot.
|
|
1823
|
+
avvisaOkandaFlaggor('paraply', ['file'])
|
|
1824
|
+
const fil = requireFlag('file')
|
|
1825
|
+
const lockPath = acquireLock()
|
|
1826
|
+
let resultat
|
|
1827
|
+
try {
|
|
1828
|
+
const state = readStateOrThrow()
|
|
1829
|
+
resultat = laggInParaply(state, fil)
|
|
1830
|
+
} catch (e) {
|
|
1831
|
+
releaseLock(lockPath)
|
|
1832
|
+
if (e instanceof ArkivFel) {
|
|
1833
|
+
console.error(e.message)
|
|
1834
|
+
process.exit(2)
|
|
1835
|
+
}
|
|
1836
|
+
throw e
|
|
1837
|
+
}
|
|
1838
|
+
releaseLock(lockPath)
|
|
1839
|
+
// F9: specForms varningar (samma form som precheck.mjs) skrivs ut före
|
|
1840
|
+
// OK-raden i stället för att tystas.
|
|
1841
|
+
for (const w of resultat.varningar) console.log(`VARNING: ${w}`)
|
|
1842
|
+
console.log(`OK: paraply ${resultat.umbrella.sha256}`)
|
|
1647
1843
|
} else if (cmd === 'bluf') {
|
|
1648
1844
|
// Läs-enbart tabellvy över katalogen och dess delar. Poängen tas ur
|
|
1649
1845
|
// prisfilerna och stoppet ur state.json, aldrig ur `note` eller prosa: den
|
|
@@ -1700,7 +1896,16 @@ if (cmd === 'add-run') {
|
|
|
1700
1896
|
? 'kandidat (spec.md)'
|
|
1701
1897
|
: state.stopp.kalla === 'granskarblock.md'
|
|
1702
1898
|
? 'prisavgörande val (granskarblock.md)'
|
|
1703
|
-
:
|
|
1899
|
+
: DESIGN_AVVISAD_RE.test(state.stopp.kalla ?? '')
|
|
1900
|
+
? `designkonflikt (${state.stopp.kalla})`
|
|
1901
|
+
: 'motsägelse (impact.md)'
|
|
1902
|
+
}
|
|
1903
|
+
// ADR-0144 beslut 6: delningsroten visar hur många delar som är klara,
|
|
1904
|
+
// samma räkning som nasta-steg (delningslage), före täckningsposten.
|
|
1905
|
+
const delning = delningslage(d)
|
|
1906
|
+
if (delning !== null) {
|
|
1907
|
+
const text = `${delning.klara} av ${delning.av} delar prissatta`
|
|
1908
|
+
anm = anm === SAKNAS ? text : `${anm}; ${text}`
|
|
1704
1909
|
}
|
|
1705
1910
|
if (taeckning) anm = anm === SAKNAS ? taeckning : `${anm}; ${taeckning}`
|
|
1706
1911
|
return `| ${[post, poang, utfall, konfidens, anm].map(cell).join(' | ')} |`
|
|
@@ -1733,6 +1938,7 @@ if (cmd === 'add-run') {
|
|
|
1733
1938
|
post,
|
|
1734
1939
|
antalDelar: arBuntRot ? direktaDelar.length : 0,
|
|
1735
1940
|
scorerUtfall: gallandeScorerUtfall(state, d),
|
|
1941
|
+
delning: delningslage(d),
|
|
1736
1942
|
})
|
|
1737
1943
|
return `| ${[post, star, vantar, nasta].map(cell).join(' | ')} |`
|
|
1738
1944
|
}
|
|
@@ -1751,7 +1957,7 @@ if (cmd === 'add-run') {
|
|
|
1751
1957
|
console.log(rader.join('\n'))
|
|
1752
1958
|
} else {
|
|
1753
1959
|
console.error(
|
|
1754
|
-
'okänt kommando (add-run, next-n, ny-korning, ny-del, proveniens, sparr, utslag, set-status, stopp, respec, bluf, nasta-steg, kalibrering, experiment)',
|
|
1960
|
+
'okänt kommando (add-run, next-n, ny-korning, ny-del, proveniens, sparr, utslag, set-status, stopp, respec, add-design, set-split-gid, paraply, bluf, nasta-steg, kalibrering, experiment)',
|
|
1755
1961
|
)
|
|
1756
1962
|
process.exit(2)
|
|
1757
1963
|
}
|
|
@@ -1826,6 +2032,196 @@ function delKataloger(dir) {
|
|
|
1826
2032
|
.flatMap((d) => [d, ...delKataloger(d)])
|
|
1827
2033
|
}
|
|
1828
2034
|
|
|
2035
|
+
/** Lägger undan <bas>.md som <bas>-superseded-<datum>.md, numrerad svans vid
|
|
2036
|
+
* kollision (-2, -3 …), aldrig en överskrivning: arkivet raderar inget.
|
|
2037
|
+
* Delad av respec (impact.md och design.md) och add-design (design.md).
|
|
2038
|
+
* Returnerar true när en fil flyttades. */
|
|
2039
|
+
function laggUndan(d, bas) {
|
|
2040
|
+
const fil = path.join(d, `${bas}.md`)
|
|
2041
|
+
if (!existsSync(fil)) return false
|
|
2042
|
+
const stam = path.join(d, `${bas}-superseded-${today()}`)
|
|
2043
|
+
let mal = `${stam}.md`
|
|
2044
|
+
for (let n = 2; existsSync(mal); n++) mal = `${stam}-${n}.md`
|
|
2045
|
+
try {
|
|
2046
|
+
renameSync(fil, mal)
|
|
2047
|
+
} catch (e) {
|
|
2048
|
+
throw new ArkivFel(`kunde inte döpa om ${fil} till ${mal}: ${e.message}`)
|
|
2049
|
+
}
|
|
2050
|
+
return true
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/** Repo-roten för en arkivkatalog: föräldern till närmaste `.haus-workflow`
|
|
2054
|
+
* uppåt i sökvägen (arkivet bor i <rot>/.haus-workflow/requests/). null när
|
|
2055
|
+
* katalogen inte ligger under någon; add-design vägrar då i stället för att
|
|
2056
|
+
* gissa en rot. Inget git-anrop: scripten är node-inbyggt bara. */
|
|
2057
|
+
function repoRotFor(d) {
|
|
2058
|
+
let cur = path.resolve(d)
|
|
2059
|
+
for (;;) {
|
|
2060
|
+
if (path.basename(cur) === '.haus-workflow') return path.dirname(cur)
|
|
2061
|
+
const parent = path.dirname(cur)
|
|
2062
|
+
if (parent === cur) return null
|
|
2063
|
+
cur = parent
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/** add-design: vägransreglerna i ordning (rot, fil, inneslutning, arkivet,
|
|
2068
|
+
* frontmatter, spec-hash, konflikter, status, stopp), sedan undanläggning,
|
|
2069
|
+
* kopia och state. Inneslutningen prövas på realpath (ADR-0082: en länk
|
|
2070
|
+
* matchas på målet), och sökvägen som bokförs är realpath så precheck
|
|
2071
|
+
* --design hittar samma fil oavsett länkar. Ett designkonfliktstopp ur
|
|
2072
|
+
* impact.md nollas (en omdesign besvarar det, slutgranskning I2) och
|
|
2073
|
+
* impact.md läggs undan med det; varje annat stopp vägras. Kastar
|
|
2074
|
+
* ArkivFel, skriver inget före sista steget. Returnerar designkvittot och
|
|
2075
|
+
* om en analys lades undan. */
|
|
2076
|
+
function laggTillDesign(state, fil) {
|
|
2077
|
+
const rot = repoRotFor(katalog)
|
|
2078
|
+
if (rot === null)
|
|
2079
|
+
throw new ArkivFel(
|
|
2080
|
+
`${katalog} ligger inte under någon .haus-workflow-katalog, kan inte avgöra repo-roten`,
|
|
2081
|
+
)
|
|
2082
|
+
let real
|
|
2083
|
+
try {
|
|
2084
|
+
real = realpathSync(path.resolve(fil))
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
throw new ArkivFel(`kan inte läsa ${fil}: ${e.message}`)
|
|
2087
|
+
}
|
|
2088
|
+
const rotReal = realpathSync(rot)
|
|
2089
|
+
if (!real.startsWith(rotReal + path.sep))
|
|
2090
|
+
throw new ArkivFel(`${real} ligger utanför repo-roten ${rotReal}`)
|
|
2091
|
+
// Slutgranskning M3: arkivet är aldrig designdokumentets hem. Arkivets egen
|
|
2092
|
+
// design.md som --file hade lagts undan av laggUndan innan kopian, som då
|
|
2093
|
+
// föll på en fil som inte längre fanns.
|
|
2094
|
+
if (real.startsWith(path.join(rotReal, '.haus-workflow') + path.sep))
|
|
2095
|
+
throw new ArkivFel(
|
|
2096
|
+
`${real} ligger under .haus-workflow, designdokumentet ska ligga i repot, aldrig i arkivet`,
|
|
2097
|
+
)
|
|
2098
|
+
let text
|
|
2099
|
+
try {
|
|
2100
|
+
text = readFileSync(real, 'utf8')
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
throw new ArkivFel(`kan inte läsa ${real}: ${e.message}`)
|
|
2103
|
+
}
|
|
2104
|
+
const fm = designFrontmatter(text)
|
|
2105
|
+
if (fm === null || !fm.request_id) throw new ArkivFel(`${real}: frontmattern saknar request_id`)
|
|
2106
|
+
const id = katalogId(state, katalog)
|
|
2107
|
+
if (fm.request_id !== id)
|
|
2108
|
+
throw new ArkivFel(`${real}: request_id "${fm.request_id}" är inte katalogens id "${id}"`)
|
|
2109
|
+
const specFil = path.join(katalog, 'spec.md')
|
|
2110
|
+
let specHash
|
|
2111
|
+
try {
|
|
2112
|
+
specHash = markerStrippedSha256(specFil)
|
|
2113
|
+
} catch (e) {
|
|
2114
|
+
throw new ArkivFel(`kan inte hasha ${specFil}: ${e.message}`)
|
|
2115
|
+
}
|
|
2116
|
+
if (fm.spec_sha256 !== specHash)
|
|
2117
|
+
throw new ArkivFel(
|
|
2118
|
+
`${real}: spec_sha256 "${fm.spec_sha256 ?? ''}" är inte nuvarande spec.md (${specHash}), ` +
|
|
2119
|
+
'designen gjordes mot en annan spec',
|
|
2120
|
+
)
|
|
2121
|
+
const konflikter = specConflicts(text)
|
|
2122
|
+
if (konflikter === null) throw new ArkivFel(`${real}: sektionen "## Spec conflicts" saknas`)
|
|
2123
|
+
if (konflikter.length > 0)
|
|
2124
|
+
throw new ArkivFel(
|
|
2125
|
+
`${real}: "## Spec conflicts" är inte none (${konflikter.length}): kunddialog, respec och ny speca (ADR-0118)`,
|
|
2126
|
+
)
|
|
2127
|
+
// Slutgranskning M4: en prissatt post designas inte om, analysen och
|
|
2128
|
+
// poängen står på den bokförda designen.
|
|
2129
|
+
if (state.status === 'priced' || state.status === 'guard_run')
|
|
2130
|
+
throw new ArkivFel(
|
|
2131
|
+
`${statePath()} har status ${state.status}, en prissatt post designas inte om utan respec`,
|
|
2132
|
+
)
|
|
2133
|
+
const stopp = state.stopp ?? null
|
|
2134
|
+
if (stopp !== null && !arDesignkonflikt(stopp))
|
|
2135
|
+
throw new ArkivFel(
|
|
2136
|
+
`${statePath()} har ett stopp (${stopp.utfall} ur ${stopp.kalla}) som en design inte löser, kör respec eller avgör stoppet först`,
|
|
2137
|
+
)
|
|
2138
|
+
laggUndan(katalog, 'design')
|
|
2139
|
+
const mal = path.join(katalog, 'design.md')
|
|
2140
|
+
try {
|
|
2141
|
+
copyFileSync(real, mal)
|
|
2142
|
+
} catch (e) {
|
|
2143
|
+
throw new ArkivFel(`kunde inte kopiera ${real} till ${mal}: ${e.message}`)
|
|
2144
|
+
}
|
|
2145
|
+
const design = {
|
|
2146
|
+
path: real,
|
|
2147
|
+
file: 'design.md',
|
|
2148
|
+
sha256: markerStrippedSha256(mal),
|
|
2149
|
+
spec_sha256: specHash,
|
|
2150
|
+
date: today(),
|
|
2151
|
+
}
|
|
2152
|
+
// Ett designkonfliktstopp besvaras av just den här designen (I2); det är
|
|
2153
|
+
// det enda stopp som når hit, alla andra vägrades ovan. Analysen som fann
|
|
2154
|
+
// designen obyggbar läggs undan med stoppet (slutgranskning omgång 2, F3):
|
|
2155
|
+
// en ny analys är skyldig oavsett omdesignens bytes, och en byteidentisk
|
|
2156
|
+
// omdesign hade annars lämnat motsägelseanalysen proveniensgiltig. runs[]
|
|
2157
|
+
// rörs inte, som i respec.
|
|
2158
|
+
const impactUndanlagd = stopp !== null ? laggUndan(katalog, 'impact') : false
|
|
2159
|
+
writeStateOrThrow({
|
|
2160
|
+
...state,
|
|
2161
|
+
design,
|
|
2162
|
+
status: 'designed',
|
|
2163
|
+
...(stopp !== null ? { stopp: null } : {}),
|
|
2164
|
+
})
|
|
2165
|
+
return { design, impactUndanlagd }
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** paraply (ADR-0144 beslut 5): vägransreglerna i ordning (delningsdel,
|
|
2169
|
+
* delningsrot, godkännande, läsbar fil, specens form enligt specForm,
|
|
2170
|
+
* titelrad), sedan undanläggning, kopia och state. Beslutslåset (Decision)
|
|
2171
|
+
* är synkens sak, inte arkivets. Titeln läses med forstaRubrik, samma regel
|
|
2172
|
+
* som add-run använder för spec.md (#495). Filen läses som bytes före
|
|
2173
|
+
* undanläggningen, så även katalogens egen spec.md går att peka ut. Kastar
|
|
2174
|
+
* ArkivFel. Returnerar { umbrella: kvittot { date, sha256 }, varningar:
|
|
2175
|
+
* specForms varningar, tryckta av anroparen som VARNING-rader före
|
|
2176
|
+
* OK-raden (F9) }. */
|
|
2177
|
+
function laggInParaply(state, fil) {
|
|
2178
|
+
if (state.split?.parent != null)
|
|
2179
|
+
throw new ArkivFel(
|
|
2180
|
+
`${statePath()} är en delningsdel (split.parent ${state.split.parent}), paraply gäller bara föräldern`,
|
|
2181
|
+
)
|
|
2182
|
+
// Slutgranskning F1: förälderns spec.md får inte skrivas om innan del-N/
|
|
2183
|
+
// finns, annars ärver varje del (bedöm-valet 1) paraplyet i stället för
|
|
2184
|
+
// specen den prissattes mot.
|
|
2185
|
+
if (delningslage(katalog) === null)
|
|
2186
|
+
throw new ArkivFel(
|
|
2187
|
+
`${statePath()} är ingen delningsrot (inga del-N/ med split.parent), paraply gäller bara en uppdelad förälder`,
|
|
2188
|
+
)
|
|
2189
|
+
if (state.approval == null)
|
|
2190
|
+
throw new ArkivFel(`${statePath()} saknar approval, paraply skriver bara om en godkänd spec`)
|
|
2191
|
+
let bytes
|
|
2192
|
+
try {
|
|
2193
|
+
bytes = readFileSync(path.resolve(fil))
|
|
2194
|
+
} catch (e) {
|
|
2195
|
+
throw new ArkivFel(`kan inte läsa ${fil}: ${e.message}`)
|
|
2196
|
+
}
|
|
2197
|
+
const text = bytes.toString('utf8')
|
|
2198
|
+
const namn = path.basename(fil)
|
|
2199
|
+
const form = specForm(text, namn)
|
|
2200
|
+
const fel = [
|
|
2201
|
+
...form.brister,
|
|
2202
|
+
...form.kandidater.map((k) => `${namn}: kandidatformen (${k}) är ingen paraplyspec`),
|
|
2203
|
+
]
|
|
2204
|
+
if (fel.length > 0)
|
|
2205
|
+
throw new ArkivFel(
|
|
2206
|
+
`${fil} klarar inte specens formkontroll:\n${fel.map((f) => `- ${f}`).join('\n')}`,
|
|
2207
|
+
)
|
|
2208
|
+
const titel = forstaRubrik(text)
|
|
2209
|
+
if (titel === null || SPEC_HEADINGS.some((r) => r.sv === titel || r.en === titel))
|
|
2210
|
+
throw new ArkivFel(
|
|
2211
|
+
`${fil} saknar titelrad före "Problem": paraplyets titel är det ändrade läget för helheten`,
|
|
2212
|
+
)
|
|
2213
|
+
laggUndan(katalog, 'spec')
|
|
2214
|
+
const mal = path.join(katalog, 'spec.md')
|
|
2215
|
+
try {
|
|
2216
|
+
writeFileSync(mal, bytes)
|
|
2217
|
+
} catch (e) {
|
|
2218
|
+
throw new ArkivFel(`kunde inte skriva ${mal}: ${e.message}`)
|
|
2219
|
+
}
|
|
2220
|
+
const umbrella = { date: today(), sha256: markerStrippedSha256(mal) }
|
|
2221
|
+
writeStateOrThrow({ ...state, title: titel, umbrella })
|
|
2222
|
+
return { umbrella, varningar: form.varningar }
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1829
2225
|
/** Scorerns gällande utfall ur prisfilen, eller null när inget poängvarv
|
|
1830
2226
|
* finns. Läses bara för att `nasta-steg` inte ska säga "sync till brädan"
|
|
1831
2227
|
* om en post vars gällande varv är DELA_UPP eller OVER_SKALAN: de utfallen
|
|
@@ -1838,6 +2234,43 @@ function gallandeScorerUtfall(state, d) {
|
|
|
1838
2234
|
return { fil, outcome: readPricingFileOrThrow(fil, d).outcome ?? null }
|
|
1839
2235
|
}
|
|
1840
2236
|
|
|
2237
|
+
/** Delningsläget för en katalog (ADR-0144 beslut 4 och 6): null när ingen
|
|
2238
|
+
* direkt del-N bär split.parent (ingen delningsrot), annars { klara, av }.
|
|
2239
|
+
* `av` är delarnas split.of: arkivet vet hur många delar det finns även när
|
|
2240
|
+
* alla kataloger inte skapats än. `klara` räknar delarna som delKlar godtar.
|
|
2241
|
+
* Delar som är oeniga om split.of, ett split.of som inte är ett heltal >= 1
|
|
2242
|
+
* eller fler delkataloger än split.of är ett arkivfel, aldrig en gissning.
|
|
2243
|
+
* Delad av nasta-steg och bluf så de två aldrig räknar olika. */
|
|
2244
|
+
function delningslage(d) {
|
|
2245
|
+
const delar = delKataloger(d)
|
|
2246
|
+
.filter((p) => path.dirname(p) === d)
|
|
2247
|
+
.map((p) => ({ p, state: readStateOrThrow(p) }))
|
|
2248
|
+
.filter(({ state }) => state.split?.parent != null)
|
|
2249
|
+
if (delar.length === 0) return null
|
|
2250
|
+
const antal = [...new Set(delar.map(({ state }) => state.split.of))]
|
|
2251
|
+
if (antal.length > 1)
|
|
2252
|
+
throw new ArkivFel(
|
|
2253
|
+
`${d}: delarna är oeniga om split.of (${antal.join(', ')}), en delning har ett antal delar`,
|
|
2254
|
+
)
|
|
2255
|
+
const av = antal[0]
|
|
2256
|
+
if (!Number.isInteger(av) || av < 1)
|
|
2257
|
+
throw new ArkivFel(`${d}: delarnas split.of är inte ett heltal >= 1 (fick ${av})`)
|
|
2258
|
+
if (delar.length > av) throw new ArkivFel(`${d}: ${delar.length} delkataloger men split.of ${av}`)
|
|
2259
|
+
return { klara: delar.filter(({ p, state }) => delKlar(state, p)).length, av }
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/** En del är klar när den är priced (eller guard_run, som bara nås efter
|
|
2263
|
+
* priced), eller står på scorerns OVER_SKALAN med status assessed, utan stopp
|
|
2264
|
+
* och utan olyft spärr: exakt de delar vars egen nasta-steg-rad är
|
|
2265
|
+
* deluppgiftsraden (ADR-0144 beslut 4, en OVER_SKALAN-del får en deluppgift
|
|
2266
|
+
* utan Score och räknas som prissatt). */
|
|
2267
|
+
function delKlar(state, d) {
|
|
2268
|
+
if (state.status === 'priced' || state.status === 'guard_run') return true
|
|
2269
|
+
if (state.status !== 'assessed' || state.stopp != null) return false
|
|
2270
|
+
if (sparrSkal(state).length > 0) return false
|
|
2271
|
+
return gallandeScorerUtfall(state, d)?.outcome === 'OVER_SKALAN'
|
|
2272
|
+
}
|
|
2273
|
+
|
|
1841
2274
|
/** Regeltabellen bakom `nasta-steg` (#530). Varje regel är en avläsning av
|
|
1842
2275
|
* en grind som redan står i SKILL.md; kolumnen "väntar på" är det en
|
|
1843
2276
|
* MÄNNISKA måste göra, "nästa" det /request-kommando som kör vidare.
|
|
@@ -1850,8 +2283,13 @@ function gallandeScorerUtfall(state, d) {
|
|
|
1850
2283
|
* | spärr står (sparrSkal) | bedöm steg 6 OLIKA "Hänvisa sedan till `döm <post>`"; `döm` |
|
|
1851
2284
|
* | stopp KONCEPT/DIAGNOS | speca steg 6 "Kandidatens katalog kan fortfarande läggas på |
|
|
1852
2285
|
* | | brädan … men `bedöm` hoppar över den"; stopputfall → kunddialog |
|
|
2286
|
+
* | stopp TILLBAKA_TILL_SPEC ur impact.md | bedöm steg 3 "starts with `design conflict:` … the Closing |
|
|
2287
|
+
* | med orsak `design conflict: …` | routes it to `designa <adress>`"; add-design nollar stoppet |
|
|
2288
|
+
* | | och lägger undan impact.md, ett kvitto ur designa steg 5 |
|
|
2289
|
+
* | | ersätter det (då gäller raden nedan) |
|
|
1853
2290
|
* | stopp TILLBAKA_TILL_SPEC | speca steg 6 (ADR-0118) "När valet avgjorts skrivs svaret |
|
|
1854
|
-
* | | under `## Avgjorda val` …, sedan `respec` och ny dispatch"
|
|
2291
|
+
* | | under `## Avgjorda val` …, sedan `respec` och ny dispatch"; |
|
|
2292
|
+
* | | designa steg 5, ett kvitto design-avvisad-<datum>.md |
|
|
1855
2293
|
* | stopp ESKALERA | speca steg 6 "en människa dömer, posten går aldrig en tredje |
|
|
1856
2294
|
* | | specrunda"; stopputfall ESKALERA → visa skälet och stanna |
|
|
1857
2295
|
* | experiment | The experiment area: `experiments/`: "kan inte förväxlas |
|
|
@@ -1859,23 +2297,36 @@ function gallandeScorerUtfall(state, d) {
|
|
|
1859
2297
|
* | spec_draft utan approval | speca steg 7 "Erbjud att lägga specarna på brädan (🔍)"; |
|
|
1860
2298
|
* | | godkänn/underkänn verkar från 🔍 |
|
|
1861
2299
|
* | spec_synced utan approval | godkänn/underkänn-lägesgrinden: 🔍 → ✅ Spec ok |
|
|
1862
|
-
* | spec_draft/spec_synced med approval
|
|
1863
|
-
* |
|
|
2300
|
+
* | spec_draft/spec_synced med approval, | bedöm-grinden 1b: en del ärver förälderns design, grindas |
|
|
2301
|
+
* | split.parent satt | aldrig |
|
|
2302
|
+
* | spec_draft/spec_synced med approval, | designa steg 5: designen är bokförd, bara statusen släpar |
|
|
2303
|
+
* | design satt | (t.ex. en sync efter add-design) → bedöm, aldrig designa igen |
|
|
2304
|
+
* | spec_draft/spec_synced med approval, | designa-grinden: "Only `✅ Spec ok` … lets it through"; |
|
|
2305
|
+
* | ingen design | Closing ISÄR-raden "with `approval` set → `designa`" |
|
|
2306
|
+
* | designed | bedöm grind 1b: "`DESIGN: saknas` or any BRIST … refuse"; |
|
|
2307
|
+
* | | Closing ISÄR-raden "`designed` ↔ ✅ Spec ok → `bedöm`" |
|
|
1864
2308
|
* | assessed utan poängvarv | bedöm steg 3–7: analysen finns, poängen saknas |
|
|
2309
|
+
* | assessed, scorerns DELA_UPP, delar | ADR-0144 beslut 4 och 6: "se delarna (K av M delar |
|
|
2310
|
+
* | finns (delningslage) | prissatta)"; alla klara (delKlar) → sync föräldern (⚖️) |
|
|
1865
2311
|
* | assessed, scorerns DELA_UPP | stopputfall → "ny analysrunda per del … `split` satt" |
|
|
2312
|
+
* | assessed, scorerns OVER_SKALAN, del | ADR-0144 beslut 3: deluppgift utan Score; split.gid säger |
|
|
2313
|
+
* | (split.parent satt) | synkad, annars sync och set-split-gid |
|
|
1866
2314
|
* | assessed, scorerns OVER_SKALAN | stopputfall → "offereras separat; delas aldrig vidare" |
|
|
1867
2315
|
* | assessed, scorerns övriga stopp | stopputfall → kunddialog / spec-steget / visa skälet, stanna |
|
|
1868
2316
|
* | assessed (poäng) | bedöm steg 8: sync via request-sync tar ✅ → ⚖️, sedan |
|
|
1869
2317
|
* | | godkänn/underkänn från ⚖️ |
|
|
2318
|
+
* | priced, del (split.parent satt) | ADR-0144 beslut 3 och 6: deluppgift med Score; split.gid |
|
|
2319
|
+
* | | säger synkad, annars sync och set-split-gid |
|
|
1870
2320
|
* | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund |
|
|
1871
2321
|
* | guard_run | `guard_run` reserveras för guard-kommandot: inget väntar |
|
|
1872
2322
|
* | allt annat | `okänt läge: <status>`, aldrig en gissning |
|
|
1873
2323
|
*
|
|
1874
2324
|
* `står` är statusen plus det som avviker från grundläget: stoppet,
|
|
1875
|
-
* scorerns stopputfall, olyft OLIKA, `respec_varv` > 0, manuell approval
|
|
1876
|
-
* och experimentmarkören.
|
|
1877
|
-
*
|
|
1878
|
-
|
|
2325
|
+
* scorerns stopputfall, olyft OLIKA, `respec_varv` > 0, manuell approval,
|
|
2326
|
+
* designfältet (`design <datum>`) och experimentmarkören. `delning` är
|
|
2327
|
+
* delningslage för katalogen (null utom på en delningsrot). Funktionen är
|
|
2328
|
+
* ren (ingen I/O) så tabellen går att testa rad för rad. */
|
|
2329
|
+
function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null }) {
|
|
1879
2330
|
const status = state.status
|
|
1880
2331
|
const gid = state.request_id ?? '<gid>'
|
|
1881
2332
|
const adress = state.request_id ?? post
|
|
@@ -1895,6 +2346,7 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
|
1895
2346
|
olikaOlyft ? 'OLIKA olyft' : vantarDom ? 'omkörning olyft' : null,
|
|
1896
2347
|
(state.respec_varv ?? 0) > 0 ? `respec_varv ${state.respec_varv}` : null,
|
|
1897
2348
|
state.approval?.source === 'manual' ? 'approval manual' : null,
|
|
2349
|
+
state.design ? `design ${state.design.date}` : null,
|
|
1898
2350
|
state.experiment === true ? 'experiment' : null,
|
|
1899
2351
|
]
|
|
1900
2352
|
.filter((v) => v !== null)
|
|
@@ -1906,6 +2358,17 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
|
1906
2358
|
'kunddialog',
|
|
1907
2359
|
'kunddialog; lägg posten på brädan via request-sync (🔍) om den saknas där, bedöm hoppar över den',
|
|
1908
2360
|
)
|
|
2361
|
+
// ADR-0144 beslut 3 och 6: en prissatt del, eller en OVER_SKALAN-del, blir
|
|
2362
|
+
// en deluppgift under föräldern. Med split.gid satt är den synkad; utan
|
|
2363
|
+
// säger raden sync och set-split-gid, samma "om inte gjort" som för en
|
|
2364
|
+
// prissatt post.
|
|
2365
|
+
const deluppgift = (utanScore) => {
|
|
2366
|
+
const score = utanScore ? ' utan Score' : ''
|
|
2367
|
+
const offert = utanScore ? ', offereras separat' : ''
|
|
2368
|
+
return state.split?.gid
|
|
2369
|
+
? `synkad som deluppgift ${state.split.gid}${score}, se föräldern ${state.split.parent}${offert}`
|
|
2370
|
+
: `sync till brädan som deluppgift${score} under ${state.split?.parent} (request-sync) om inte gjort, sedan archive.mjs set-split-gid <del-katalog> --gid <deluppgiftens gid>${offert}`
|
|
2371
|
+
}
|
|
1909
2372
|
const tillbakaTillSpec = (val) =>
|
|
1910
2373
|
rad(
|
|
1911
2374
|
`avgör valet: ${val}`,
|
|
@@ -1917,6 +2380,9 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
|
1917
2380
|
if (stopp) {
|
|
1918
2381
|
const val = stopp.orsak ?? stopp.kalla
|
|
1919
2382
|
if (stopp.utfall === 'KONCEPT' || stopp.utfall === 'DIAGNOS') return kunddialog()
|
|
2383
|
+
// Slutgranskning I2: en designkonflikt besvaras av en omdesign, inte av
|
|
2384
|
+
// en respec; add-design nollar stoppet.
|
|
2385
|
+
if (arDesignkonflikt(stopp)) return rad('omdesign (designa)', `designa ${adress}`)
|
|
1920
2386
|
if (stopp.utfall === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(val)
|
|
1921
2387
|
if (stopp.utfall === 'ESKALERA')
|
|
1922
2388
|
return rad(`avgör valet: ${val}`, 'människa dömer, ingen ny specrunda')
|
|
@@ -1924,8 +2390,19 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
|
1924
2390
|
if (state.experiment === true)
|
|
1925
2391
|
return rad('inget (experiment, når aldrig Asana)', 'utvärdera rapport')
|
|
1926
2392
|
const godkand = state.approval !== null && state.approval !== undefined
|
|
2393
|
+
const delAvSplit = state.split?.parent !== null && state.split?.parent !== undefined
|
|
1927
2394
|
if (status === 'spec_draft' || status === 'spec_synced') {
|
|
1928
|
-
|
|
2395
|
+
// Designa (spec 2026-09-24): en godkänd post går till designa, aldrig
|
|
2396
|
+
// direkt till bedöm. Hashkontrollen är prechecks jobb, inte tabellens.
|
|
2397
|
+
// En delningsdel (split.parent satt) ärver förälderns design (Ruling B):
|
|
2398
|
+
// den grindas aldrig i designa-grinden, utan går direkt till bedöm. En
|
|
2399
|
+
// post med designen bokförd men statusen kvar (slutgranskning M6) går
|
|
2400
|
+
// också till bedöm: designa igen hade frågat om en design som finns.
|
|
2401
|
+
if (godkand) {
|
|
2402
|
+
if (delAvSplit) return rad('körning av bedöm', `bedöm ${adress}`)
|
|
2403
|
+
if (state.design) return rad('körning av bedöm', `bedöm ${adress}`)
|
|
2404
|
+
return rad('körning av designa', `designa ${adress}`)
|
|
2405
|
+
}
|
|
1929
2406
|
return status === 'spec_draft'
|
|
1930
2407
|
? rad(
|
|
1931
2408
|
'godkännande av spec på brädan',
|
|
@@ -1933,14 +2410,28 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
|
1933
2410
|
)
|
|
1934
2411
|
: rad('godkännande av spec på brädan', godkannEller)
|
|
1935
2412
|
}
|
|
2413
|
+
if (status === 'designed') return rad('körning av bedöm', `bedöm ${adress}`)
|
|
1936
2414
|
if (status === 'assessed') {
|
|
1937
2415
|
if (nScore === 0) return rad('körning av bedöm', `bedöm ${adress} (poängsättning saknas)`)
|
|
1938
2416
|
if (scorerStopp) {
|
|
1939
2417
|
const o = scorerStopp.outcome
|
|
1940
|
-
if (o === 'DELA_UPP')
|
|
2418
|
+
if (o === 'DELA_UPP') {
|
|
2419
|
+
// ADR-0144 beslut 4 och 6: delningsroten räknar sina delar. Så länge
|
|
2420
|
+
// någon återstår står föräldern kvar på ✅ Spec ok; när varje del är
|
|
2421
|
+
// klar (delKlar) flyttar synken den till ⚖️.
|
|
2422
|
+
if (delning !== null && delning.klara < delning.av)
|
|
2423
|
+
return rad('se delarna', `se delarna (${delning.klara} av ${delning.av} delar prissatta)`)
|
|
2424
|
+
if (delning !== null)
|
|
2425
|
+
return rad(
|
|
2426
|
+
'granskning av poäng på brädan',
|
|
2427
|
+
`sync föräldern till brädan (request-sync ⚖️), sedan ${godkannEller}`,
|
|
2428
|
+
)
|
|
1941
2429
|
return rad('ny analysrunda per del', 'dela posten (split), sedan bedöm per del')
|
|
2430
|
+
}
|
|
1942
2431
|
if (o === 'OVER_SKALAN')
|
|
1943
|
-
return
|
|
2432
|
+
return delAvSplit
|
|
2433
|
+
? rad('manuell offert', deluppgift(true))
|
|
2434
|
+
: rad('manuell offert', 'offereras separat, delas aldrig vidare')
|
|
1944
2435
|
if (o === 'KONCEPT' || o === 'DIAGNOS') return kunddialog()
|
|
1945
2436
|
if (o === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(scorerStopp.fil)
|
|
1946
2437
|
return rad(
|
|
@@ -1953,11 +2444,17 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
|
1953
2444
|
`sync till brädan (request-sync ⚖️), sedan ${godkannEller}`,
|
|
1954
2445
|
)
|
|
1955
2446
|
}
|
|
1956
|
-
if (status === 'priced')
|
|
2447
|
+
if (status === 'priced') {
|
|
2448
|
+
if (delAvSplit)
|
|
2449
|
+
return rad(
|
|
2450
|
+
state.split.gid ? 'granskning av föräldern på brädan' : 'sync av deluppgiften',
|
|
2451
|
+
deluppgift(false),
|
|
2452
|
+
)
|
|
1957
2453
|
return rad(
|
|
1958
2454
|
'granskning av poäng på brädan',
|
|
1959
2455
|
`sync till brädan (request-sync ⚖️) om inte gjort, sedan godkänn ${gid} (📤 Klar – till kund) eller underkänn ${gid} <skäl>`,
|
|
1960
2456
|
)
|
|
2457
|
+
}
|
|
1961
2458
|
if (status === 'guard_run') return rad('inget', 'inget')
|
|
1962
2459
|
return rad('–', '–', `okänt läge: ${status}`)
|
|
1963
2460
|
}
|