@haus-tech/haus-workflow 5.12.1 → 6.1.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 +66 -0
- package/README.md +7 -4
- package/dist/cli.js +7944 -4000
- package/library/catalog/manifest.json +126 -9
- package/library/global/agents/request-design-reviewer.md +54 -0
- package/library/global/agents/request-designer.md +113 -0
- package/library/global/skills/haus-workflow/SKILL.md +10 -3
- package/library/global/skills/haus-workflow/references/init.md +7 -4
- package/library/global/skills/haus-workflow/references/new.md +1 -1
- package/library/global/skills/request/SKILL.md +493 -107
- package/library/global/skills/request/scripts/archive.mjs +736 -56
- package/library/global/skills/request/scripts/bedom.mjs +651 -0
- package/library/global/skills/request/scripts/korning.mjs +911 -70
- package/library/global/skills/request/scripts/lib.mjs +328 -3
- package/library/global/skills/request/scripts/precheck.mjs +134 -34
- package/library/global/skills/request/scripts/svep.mjs +44 -14
- package/library/global/skills/request-assess-impact/SKILL.md +1 -4
- package/library/global/skills/request-sync/SKILL.md +8 -2
- package/package.json +4 -3
|
@@ -19,26 +19,38 @@
|
|
|
19
19
|
* väntas göra och vilket /request-kommando som kör vidare, ur SKILL.md:s
|
|
20
20
|
* egna grindar (regeltabellen står ovanför `nastaStegFor`). Del 6 (designa,
|
|
21
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.
|
|
22
|
+
* och bokför designdokumentet i state.design utan runs-rad. Del 7 (ADR-0156
|
|
23
|
+
* beslut 3 och 5): add-design-review, som kopierar in granskarens svar som
|
|
24
|
+
* design-review.md och bokför kvittot i state.design.review (utan runs-rad,
|
|
25
|
+
* kostnaden står i korning.json), och antaganden, läs-enbart lista över
|
|
26
|
+
* utkastets antaganden, granskningens utslag och spec conflicts.
|
|
23
27
|
*
|
|
24
28
|
* Användning:
|
|
25
29
|
* node archive.mjs add-run <katalog> --file F --skill S --agent A --model M [flaggor]
|
|
26
30
|
* node archive.mjs next-n <katalog> --typ classification [--typ score]
|
|
27
31
|
* node archive.mjs ny-korning <modellkatalog> [--request-id G]
|
|
28
32
|
* node archive.mjs ny-del <körningskatalog> --typ bundle|split --part N --of M --request-id G
|
|
33
|
+
* node archive.mjs ny-del <postkatalog> --typ split --part N --of M (produktionsdel, #599)
|
|
29
34
|
* node archive.mjs proveniens <körningskatalog> --modell M --konto K --underlag F [--datum D]
|
|
30
35
|
* node archive.mjs sparr <katalog>
|
|
31
36
|
* node archive.mjs utslag <katalog> --typ viktning|kalibrering|sakandring --beslutad-av N [...]
|
|
32
37
|
* node archive.mjs set-status <katalog> <status>
|
|
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>]
|
|
38
|
+
* node archive.mjs stopp <katalog> --utfall KONCEPT|DIAGNOS|TILLBAKA_TILL_SPEC|ESKALERA --kalla spec.md|impact.md|granskarblock.md|design-avvisad-<datum>.md|design-intervju-kravs-<datum>.md [--orsak <text>]
|
|
34
39
|
* node archive.mjs respec <katalog>
|
|
35
40
|
* node archive.mjs add-design <katalog> --file <absolut sökväg till designdokumentet>
|
|
41
|
+
* node archive.mjs add-design-review <katalog> --file <abs> --verdict fortsätt|stopp --model <m>
|
|
42
|
+
* node archive.mjs antaganden <katalog>
|
|
36
43
|
* node archive.mjs bluf <katalog>
|
|
37
44
|
* node archive.mjs nasta-steg <katalog>
|
|
38
45
|
* node archive.mjs kalibrering <arkivrot>
|
|
39
46
|
* node archive.mjs experiment <experimentkatalog> [--satt K=V]... [--variant M]... [--korning M/kN --status S [--orsak T] [--pid N]]
|
|
40
47
|
* node archive.mjs set-split-gid <del-katalog> --gid <gid>
|
|
48
|
+
* node archive.mjs approve <katalog> --source asana --status "<Offer status>" | --source manual --by <namn>
|
|
49
|
+
* node archive.mjs synced <katalog> --offer-status "<Offer status>"
|
|
41
50
|
* node archive.mjs paraply <katalog> --file <spec>
|
|
51
|
+
* node archive.mjs korning-start <katalog> --pid <N> [--katalog-korning <absolut sökväg>]
|
|
52
|
+
* node archive.mjs korning-slut <katalog>
|
|
53
|
+
* node archive.mjs rot [<katalog>]
|
|
42
54
|
* Exit 0 = ok, 1 = spärrad (sparr/set-status priced), 2 = användnings-/tillståndsfel.
|
|
43
55
|
*/
|
|
44
56
|
import {
|
|
@@ -57,6 +69,7 @@ import {
|
|
|
57
69
|
copyFileSync,
|
|
58
70
|
} from 'node:fs'
|
|
59
71
|
import path from 'node:path'
|
|
72
|
+
import { spawnSync } from 'node:child_process'
|
|
60
73
|
import { createHash } from 'node:crypto'
|
|
61
74
|
import {
|
|
62
75
|
markerStrippedSha256,
|
|
@@ -65,22 +78,31 @@ import {
|
|
|
65
78
|
canonicalModel,
|
|
66
79
|
kandidatIn,
|
|
67
80
|
avvikelseEtikett,
|
|
81
|
+
avvikelseText,
|
|
68
82
|
prisavgorandeVal,
|
|
69
83
|
findUpward,
|
|
70
84
|
CLASS_UTFALL,
|
|
71
85
|
SCORE_OUTCOMES,
|
|
72
86
|
designFrontmatter,
|
|
73
87
|
specConflicts,
|
|
88
|
+
designMode,
|
|
89
|
+
designAssumptions,
|
|
90
|
+
sectionLines,
|
|
74
91
|
katalogId,
|
|
92
|
+
designForm,
|
|
93
|
+
designSymbolkontroll,
|
|
94
|
+
repoRotForFil,
|
|
75
95
|
specForm,
|
|
76
96
|
SPEC_HEADINGS,
|
|
97
|
+
alfabetiskKarna,
|
|
98
|
+
arkivRot,
|
|
77
99
|
} from './lib.mjs'
|
|
78
100
|
|
|
79
101
|
const [cmd, katalog, ...rest] = process.argv.slice(2)
|
|
80
102
|
|
|
81
|
-
if (!cmd || !katalog) {
|
|
103
|
+
if (!cmd || (!katalog && cmd !== 'rot')) {
|
|
82
104
|
console.error(
|
|
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]',
|
|
105
|
+
'användning: archive.mjs <add-run|next-n|ny-korning|ny-del|proveniens|sparr|utslag|set-status|stopp|respec|add-design|add-design-review|antaganden|set-split-gid|approve|synced|paraply|korning-start|korning-slut|rot|bluf|nasta-steg|kalibrering|experiment> <katalog> [flaggor]',
|
|
84
106
|
)
|
|
85
107
|
process.exit(2)
|
|
86
108
|
}
|
|
@@ -245,6 +267,20 @@ const readStateOrThrow = (d = katalog) => {
|
|
|
245
267
|
) {
|
|
246
268
|
throw new ArkivFel(`${statePath(d)} har ett design-fält som varken är null eller ett objekt`)
|
|
247
269
|
}
|
|
270
|
+
// ADR-0156 beslut 3: design.review är saknat, null eller ett objekt med en
|
|
271
|
+
// sträng `verdict`, skrivet av add-design-review. Allt annat är trasigt:
|
|
272
|
+
// nasta-steg läser verdict, och en sträng där hade tyst lästs som "ingen
|
|
273
|
+
// granskning".
|
|
274
|
+
const review = parsed.design?.review
|
|
275
|
+
if (
|
|
276
|
+
review !== undefined &&
|
|
277
|
+
review !== null &&
|
|
278
|
+
(typeof review !== 'object' || Array.isArray(review) || typeof review.verdict !== 'string')
|
|
279
|
+
) {
|
|
280
|
+
throw new ArkivFel(
|
|
281
|
+
`${statePath(d)} har ett design.review som varken är null eller ett objekt med en sträng verdict`,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
248
284
|
// ADR-0144 beslut 6: split.gid är saknat, null eller ett Asana-gid som
|
|
249
285
|
// sträng, skrivet av set-split-gid. Allt annat är trasigt: nasta-steg läser
|
|
250
286
|
// fältet för att avgöra om delens deluppgift finns, och ett tal eller en
|
|
@@ -268,6 +304,23 @@ const readStateOrThrow = (d = katalog) => {
|
|
|
268
304
|
) {
|
|
269
305
|
throw new ArkivFel(`${statePath(d)} har ett umbrella-fält som varken är null eller ett objekt`)
|
|
270
306
|
}
|
|
307
|
+
// ADR-0155 beslut 3: `korning` är null, saknat eller ett objekt { pid,
|
|
308
|
+
// started, katalog } skrivet av korning-start. pid måste vara ett heltal:
|
|
309
|
+
// isAlive(pid) och nasta-steg läser det, och en sträng där hade tyst lästs
|
|
310
|
+
// som "ingen körning pågår". pid 0 är ingen process: kill(0, 0) prövar
|
|
311
|
+
// processgruppen och svarar alltid levande.
|
|
312
|
+
if (
|
|
313
|
+
parsed.korning !== undefined &&
|
|
314
|
+
parsed.korning !== null &&
|
|
315
|
+
(typeof parsed.korning !== 'object' ||
|
|
316
|
+
Array.isArray(parsed.korning) ||
|
|
317
|
+
!Number.isInteger(parsed.korning.pid) ||
|
|
318
|
+
parsed.korning.pid <= 0)
|
|
319
|
+
) {
|
|
320
|
+
throw new ArkivFel(
|
|
321
|
+
`${statePath(d)} har ett korning-fält som varken är null eller ett objekt med ett heltals-pid > 0`,
|
|
322
|
+
)
|
|
323
|
+
}
|
|
271
324
|
return parsed
|
|
272
325
|
}
|
|
273
326
|
|
|
@@ -462,8 +515,8 @@ const isAlive = (pid) => {
|
|
|
462
515
|
* (en hoppad hydrering), den här funktionen har ingen "hoppa över"-väg att
|
|
463
516
|
* falla tillbaka på.
|
|
464
517
|
*/
|
|
465
|
-
const acquireLock = () => {
|
|
466
|
-
const lockPath = path.join(
|
|
518
|
+
const acquireLock = (d = katalog) => {
|
|
519
|
+
const lockPath = path.join(d, '.archive.lock')
|
|
467
520
|
const start = Date.now()
|
|
468
521
|
let warned = false
|
|
469
522
|
const maybeWarn = (info) => {
|
|
@@ -542,6 +595,35 @@ const releaseLock = (lockPath) => {
|
|
|
542
595
|
}
|
|
543
596
|
}
|
|
544
597
|
|
|
598
|
+
/** #693: en ny runda på en delningsdel (respec, eller add-run på en rundas
|
|
599
|
+
* huvudfil) gör förälderns ⚖️-kvitto inaktuellt: föräldern stod på ⚖️ för
|
|
600
|
+
* delarnas förra priser. Roten är katalogen ovanför del-N/ och nollas under
|
|
601
|
+
* sitt eget lås, efter att delens lås släppts (ingen nästlad låsning).
|
|
602
|
+
* Delens skrivning är då redan gjord, så ett arkivfel på roten (t.ex.
|
|
603
|
+
* trasig state.json) är en VARNING och aldrig exit 2: en omkörning av
|
|
604
|
+
* add-run hade vägrats som dubblett. Arkivet är en cache; Asana är
|
|
605
|
+
* sanningen. De två skrivningarna är inte atomära: dör processen mellan
|
|
606
|
+
* dem står rotens kvitto kvar till nästa respec eller add-run på en del.
|
|
607
|
+
* Returnerar ett suffix till OK-raden. */
|
|
608
|
+
const nollaRotensKvitto = (delState, d = katalog) => {
|
|
609
|
+
if (delState?.split?.parent == null) return ''
|
|
610
|
+
const rotDir = path.dirname(d)
|
|
611
|
+
if (!existsSync(path.join(rotDir, 'state.json'))) return ''
|
|
612
|
+
const lockPath = acquireLock(rotDir)
|
|
613
|
+
try {
|
|
614
|
+
const rotState = readStateOrThrow(rotDir)
|
|
615
|
+
if (rotState.board === null || rotState.board === undefined) return ''
|
|
616
|
+
writeStateOrThrow({ ...rotState, board: null }, rotDir)
|
|
617
|
+
return ', förälderns brädkvitto nollat'
|
|
618
|
+
} catch (e) {
|
|
619
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
620
|
+
console.error(`VARNING: förälderns brädkvitto kunde inte nollas: ${e.message}`)
|
|
621
|
+
return ', förälderns brädkvitto INTE nollat (se VARNING)'
|
|
622
|
+
} finally {
|
|
623
|
+
releaseLock(lockPath)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
545
627
|
/**
|
|
546
628
|
* Kända arkivfilnamn — allt annat är ett formfel i `--file` (t.ex. ett
|
|
547
629
|
* saknat "v" i "classification-pricing-1.json") som annars tyst arkiverar
|
|
@@ -564,6 +646,9 @@ const releaseLock = (lockPath) => {
|
|
|
564
646
|
const SPEC_RE = /^spec(-[a-z0-9-]+)?\.md$/
|
|
565
647
|
const IMPACT_RE = /^impact(-[a-z0-9-]+)?\.md$/
|
|
566
648
|
const PRICING_RE = /^(classification|score)-(pricing|guard)-v\d+(-[a-z0-9-]+)?\.json$/
|
|
649
|
+
/** En rundas huvudfil: PRICING_RE utan suffixsvans. -run2 och -avvisad är
|
|
650
|
+
* aldrig huvudfil (bedöm steg 6: "-run2 files are NEVER counted"). */
|
|
651
|
+
const HUVUDFIL_RE = /^(classification|score)-(pricing|guard)-v\d+\.json$/
|
|
567
652
|
const FILE_PATTERNS = [SPEC_RE, IMPACT_RE, PRICING_RE]
|
|
568
653
|
const isKnownFile = (file) => FILE_PATTERNS.some((re) => re.test(file))
|
|
569
654
|
|
|
@@ -628,6 +713,12 @@ const SCORE_STOP = [
|
|
|
628
713
|
const STOPP_UTFALL = ['KONCEPT', 'DIAGNOS', 'TILLBAKA_TILL_SPEC', 'ESKALERA']
|
|
629
714
|
const STOPP_KALLOR = ['spec.md', 'impact.md', 'granskarblock.md']
|
|
630
715
|
const DESIGN_AVVISAD_RE = /^design-avvisad-\d{4}-\d{2}-\d{2}(-\d+)?\.md$/
|
|
716
|
+
/** Designerns INTERVJU KRÄVS (P4-24): korning.mjs skriver svaret som kvittot
|
|
717
|
+
* design-intervju-kravs-<datum>.md (`-2` vid kollision) och stoppar posten
|
|
718
|
+
* som ESKALERA på det, så nästa batch inte betalar designern igen. Ett
|
|
719
|
+
* designa löser det: add-design nollar stoppet. */
|
|
720
|
+
const DESIGN_INTERVJU_RE = /^design-intervju-kravs-\d{4}-\d{2}-\d{2}(-\d+)?\.md$/
|
|
721
|
+
const arIntervjuStopp = (stopp) => DESIGN_INTERVJU_RE.test(stopp?.kalla ?? '')
|
|
631
722
|
|
|
632
723
|
/** Ett designkonfliktstopp (slutgranskning I2): assessorn fann designen
|
|
633
724
|
* obyggbar, motsägelseraden i impact.md börjar med `design conflict:` och
|
|
@@ -654,6 +745,8 @@ const MAX_KORNINGAR = 999
|
|
|
654
745
|
* är inte samma kontrakt. */
|
|
655
746
|
const PROVENIENS_REFERENSER = [
|
|
656
747
|
'agents/request-assessor.md',
|
|
748
|
+
'agents/request-design-reviewer.md',
|
|
749
|
+
'agents/request-designer.md',
|
|
657
750
|
'agents/request-scorer.md',
|
|
658
751
|
'agents/request-specifier-eskalerad.md',
|
|
659
752
|
'agents/request-specifier.md',
|
|
@@ -724,8 +817,8 @@ const slugAv = (titel) =>
|
|
|
724
817
|
* `stopp: null`. `bundle`/`split` överstyrs bara när anroparen ger dem, så
|
|
725
818
|
* ny-korning (ingen del) och ny-del (precis en av dem) delar exakt samma
|
|
726
819
|
* funktion i stället för två skrivvägar som kan glida isär. */
|
|
727
|
-
const skelett = (requestId, { bundle, split } = {}) => ({
|
|
728
|
-
experiment: true,
|
|
820
|
+
const skelett = (requestId, { bundle, split, experiment = true } = {}) => ({
|
|
821
|
+
...(experiment ? { experiment: true } : {}),
|
|
729
822
|
request_id: requestId,
|
|
730
823
|
slug: null,
|
|
731
824
|
former_slug: null,
|
|
@@ -740,6 +833,7 @@ const skelett = (requestId, { bundle, split } = {}) => ({
|
|
|
740
833
|
respec_varv: 0,
|
|
741
834
|
coverage_post: null,
|
|
742
835
|
design: null,
|
|
836
|
+
board: null,
|
|
743
837
|
runs: [],
|
|
744
838
|
})
|
|
745
839
|
|
|
@@ -768,6 +862,8 @@ if (cmd === 'add-run') {
|
|
|
768
862
|
'double-run',
|
|
769
863
|
'superseded-by',
|
|
770
864
|
'pairs-with',
|
|
865
|
+
// ADR-0155 beslut 6: HEAD-sha som en analys lästes mot (korning.mjs).
|
|
866
|
+
'repo-sha',
|
|
771
867
|
]
|
|
772
868
|
const ADD_RUN_BOOLFLAGGOR = ['partial-rerun']
|
|
773
869
|
avvisaOkandaFlaggor('add-run', ADD_RUN_VARDEFLAGGOR, ADD_RUN_BOOLFLAGGOR)
|
|
@@ -822,11 +918,17 @@ if (cmd === 'add-run') {
|
|
|
822
918
|
const supersededBy = flag('superseded-by')
|
|
823
919
|
const partialRerun = has('partial-rerun')
|
|
824
920
|
const pairsWith = flag('pairs-with')
|
|
921
|
+
const repoSha = flag('repo-sha')
|
|
922
|
+
if (repoSha !== null && !/^[0-9a-f]{40}$/.test(repoSha)) {
|
|
923
|
+
console.error('--repo-sha kräver 40 hexsiffror')
|
|
924
|
+
process.exit(2)
|
|
925
|
+
}
|
|
825
926
|
|
|
826
927
|
// Kritisk sektion: från låset förvärvas till det släpps får ingen annan
|
|
827
928
|
// add-run-process läsa eller skriva state.json för samma katalog.
|
|
828
929
|
const lockPath = acquireLock()
|
|
829
930
|
let rowCount
|
|
931
|
+
let delStateForRot = null
|
|
830
932
|
try {
|
|
831
933
|
const state = readStateOrThrow()
|
|
832
934
|
// Dubblettvakt: en PRICING-/GUARD-fil (klass- eller poängaxelns
|
|
@@ -866,6 +968,8 @@ if (cmd === 'add-run') {
|
|
|
866
968
|
pairs_with: pairsWith,
|
|
867
969
|
}
|
|
868
970
|
if (note) row.note = note
|
|
971
|
+
// Frånvarande, inte null, när flaggan utelämnas (ADR-0155 beslut 6).
|
|
972
|
+
if (repoSha !== null) row.repo_sha = repoSha
|
|
869
973
|
for (const u of underlagFor(file)) {
|
|
870
974
|
const p = path.join(katalog, u)
|
|
871
975
|
try {
|
|
@@ -894,20 +998,32 @@ if (cmd === 'add-run') {
|
|
|
894
998
|
const rubrik = forstaRubrik(readFileSync(path.join(katalog, 'spec.md'), 'utf8'))
|
|
895
999
|
if (rubrik) titelFalt = { title: rubrik, slug: slugAv(rubrik) }
|
|
896
1000
|
}
|
|
897
|
-
// Täckningspost (ADR-0119, #
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1001
|
+
// Täckningspost (ADR-0159, ändrar ADR-0119 beslut 4; #763, #687): fältet
|
|
1002
|
+
// härleds ur den poängfil som GÄLLER efter den här raden, aldrig ur filen
|
|
1003
|
+
// som råkar stämplas. Bara en poängprisfil (huvudfil, -run2 eller
|
|
1004
|
+
// -avvisad) räknar om: en guardfil har en egen N-sekvens och synkas
|
|
1005
|
+
// aldrig (SKILL.md, arkivträdet), och spec.md/impact.md får inte väcka en
|
|
1006
|
+
// post som respec redan rensat. En -run2 eller -avvisad från en NYARE
|
|
1007
|
+
// runda än den gällande (huvudfilen är inte stämplad än) räknar inte om
|
|
1008
|
+
// heller: den rundan pågår och dess gällande fil finns inte än, så fältet
|
|
1009
|
+
// behåller sitt värde (null efter en respec) tills rundans huvudfil
|
|
1010
|
+
// stämplas. Se gallandeTackning för regeln.
|
|
1011
|
+
const poangfil = /^score-pricing-v(\d+)(-[a-z0-9-]+)?\.json$/.exec(file)
|
|
1012
|
+
if (poangfil) {
|
|
1013
|
+
const efter = { ...state, runs: [...runs, row] }
|
|
1014
|
+
if (Number(poangfil[1]) <= maxN(efter.runs, 'score'))
|
|
1015
|
+
titelFalt = { ...titelFalt, coverage_post: gallandeTackning(efter) }
|
|
1016
|
+
}
|
|
1017
|
+
// ADR-0153 beslut 3: brädkvittot betyder att den gällande prisrundan
|
|
1018
|
+
// ligger på brädan. En ny rundas huvudfil gör ett tidigare kvitto
|
|
1019
|
+
// inaktuellt, så nasta-steg säger "om inte gjort" igen. -run2, -avvisad,
|
|
1020
|
+
// spec.md och impact.md rör aldrig kvittot.
|
|
1021
|
+
if (HUVUDFIL_RE.test(file) && state.board !== null && state.board !== undefined)
|
|
1022
|
+
titelFalt = { ...titelFalt, board: null }
|
|
908
1023
|
const nyState = { ...state, ...titelFalt, runs: [...runs, row] }
|
|
909
1024
|
writeStateOrThrow(nyState)
|
|
910
1025
|
rowCount = nyState.runs.length
|
|
1026
|
+
if (HUVUDFIL_RE.test(file)) delStateForRot = state
|
|
911
1027
|
} catch (e) {
|
|
912
1028
|
releaseLock(lockPath)
|
|
913
1029
|
if (e instanceof ArkivFel) {
|
|
@@ -917,7 +1033,8 @@ if (cmd === 'add-run') {
|
|
|
917
1033
|
throw e
|
|
918
1034
|
}
|
|
919
1035
|
releaseLock(lockPath)
|
|
920
|
-
|
|
1036
|
+
const rotSuffix = nollaRotensKvitto(delStateForRot)
|
|
1037
|
+
console.log(`OK: runs-rad för ${file} (${rowCount} rader)${rotSuffix}`)
|
|
921
1038
|
} else if (cmd === 'next-n') {
|
|
922
1039
|
// `flags('typ')` bara SAMLAR IN varje värde som direkt följer ett `--typ`
|
|
923
1040
|
// — den validerar aldrig att RESTEN av `rest` är just sådana par. Ett
|
|
@@ -1120,6 +1237,13 @@ if (cmd === 'add-run') {
|
|
|
1120
1237
|
}
|
|
1121
1238
|
}
|
|
1122
1239
|
state.granskarutslag = [...(state.granskarutslag ?? []), row]
|
|
1240
|
+
// ADR-0159: en dom på poängaxeln kan byta den gällande filen, så
|
|
1241
|
+
// täckningsposten räknas om ur den. En trasig gällande fil fäller domen
|
|
1242
|
+
// (exit 2) innan något skrivs.
|
|
1243
|
+
// En dom om ett äldre varv byter aldrig den gällande filen (liftingUtslag
|
|
1244
|
+
// kräver n === maxN) och räknar därför inte om.
|
|
1245
|
+
if (row.typ === 'viktning' && row.axel === 'score' && row.n === maxN(state.runs ?? [], 'score'))
|
|
1246
|
+
state.coverage_post = gallandeTackning(state)
|
|
1123
1247
|
writeStateOrThrow(state)
|
|
1124
1248
|
rowCount = state.granskarutslag.length
|
|
1125
1249
|
} catch (e) {
|
|
@@ -1154,6 +1278,10 @@ if (cmd === 'add-run') {
|
|
|
1154
1278
|
}
|
|
1155
1279
|
if (stopp.length === 0) {
|
|
1156
1280
|
state.status = status
|
|
1281
|
+
// ADR-0159: priced är steget före synken, så täckningsposten räknas om
|
|
1282
|
+
// här också. En huvudfil som stämplades innan den skrevs får då sin
|
|
1283
|
+
// post även när ingen senare stämpling kom.
|
|
1284
|
+
if (status === 'priced') state.coverage_post = gallandeTackning(state)
|
|
1157
1285
|
writeStateOrThrow(state)
|
|
1158
1286
|
}
|
|
1159
1287
|
} catch (e) {
|
|
@@ -1253,19 +1381,96 @@ if (cmd === 'add-run') {
|
|
|
1253
1381
|
// katalog är körningskatalogen (k<N>) eller en buntdel (DELA_UPP inuti en
|
|
1254
1382
|
// del ger del-N/del-M). Skelettet skrivs av kommandot, aldrig av den inre
|
|
1255
1383
|
// processen: samma skäl som ny-korning (ADR-0107 beslut 4).
|
|
1384
|
+
//
|
|
1385
|
+
// #599: under en produktionspost (state.json finns och saknar
|
|
1386
|
+
// experiment: true) skriver `--typ split` en produktionsdel i stället:
|
|
1387
|
+
// ingen experimentmarkör (set-status priced vägrar den annars), split.parent
|
|
1388
|
+
// är förälderns gid (katalognamnet när gid saknas), förälderns approval
|
|
1389
|
+
// ärvs (delen ärver den godkända specen, ADR-0144), och split valideras som
|
|
1390
|
+
// heltal vid skrivning, så ett handskrivet "of": "2" aldrig fäller
|
|
1391
|
+
// delningslage för hela trädet efteråt. --request-id är då valfri och måste
|
|
1392
|
+
// vara förälderns id.
|
|
1256
1393
|
avvisaOkandaFlaggor('ny-del', ['typ', 'part', 'of', 'request-id'])
|
|
1257
1394
|
const typ = requireFlag('typ')
|
|
1258
|
-
const
|
|
1259
|
-
const
|
|
1260
|
-
const
|
|
1395
|
+
const partRa = requireFlag('part')
|
|
1396
|
+
const ofRa = requireFlag('of')
|
|
1397
|
+
const heltal = (v) => (/^[1-9]\d{0,2}$/.test(v) ? Number(v) : NaN)
|
|
1398
|
+
const part = heltal(partRa)
|
|
1399
|
+
const of = heltal(ofRa)
|
|
1261
1400
|
if (!['bundle', 'split'].includes(typ)) {
|
|
1262
1401
|
console.error(`--typ måste vara bundle eller split (fick "${typ}")`)
|
|
1263
1402
|
process.exit(2)
|
|
1264
1403
|
}
|
|
1265
|
-
if (!Number.isInteger(part) || !Number.isInteger(of) || part
|
|
1266
|
-
console.error(
|
|
1404
|
+
if (!Number.isInteger(part) || !Number.isInteger(of) || part > of) {
|
|
1405
|
+
console.error(
|
|
1406
|
+
`--part och --of måste vara heltal 1-999 med part <= of (fick "${partRa}"/"${ofRa}")`,
|
|
1407
|
+
)
|
|
1267
1408
|
process.exit(2)
|
|
1268
1409
|
}
|
|
1410
|
+
let foralder = null
|
|
1411
|
+
if (existsSync(path.join(katalog, 'state.json'))) {
|
|
1412
|
+
// Förälderns lås hålls från läsningen till delens state.json är skriven:
|
|
1413
|
+
// två samtidiga ny-del med olika --of kan annars båda passera
|
|
1414
|
+
// syskonkontrollen. Varje utgång nedan är process.exit, så låset släpps
|
|
1415
|
+
// i exit-hooken.
|
|
1416
|
+
const lockPath = acquireLock(katalog)
|
|
1417
|
+
process.on('exit', () => releaseLock(lockPath))
|
|
1418
|
+
try {
|
|
1419
|
+
foralder = readStateOrThrow(katalog)
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1422
|
+
console.error(e.message)
|
|
1423
|
+
process.exit(2)
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const produktion = foralder !== null && foralder.experiment !== true
|
|
1427
|
+
let requestId
|
|
1428
|
+
let arvtGodkannande = null
|
|
1429
|
+
if (produktion) {
|
|
1430
|
+
const vagra = (msg) => {
|
|
1431
|
+
console.error(`ny-del vägrar: ${msg}`)
|
|
1432
|
+
process.exit(2)
|
|
1433
|
+
}
|
|
1434
|
+
if (typ !== 'split')
|
|
1435
|
+
vagra('--typ bundle under en produktionspost (produktionsbuntar skrivs av speca)')
|
|
1436
|
+
if (foralder.split?.parent != null)
|
|
1437
|
+
vagra('föräldern är själv en delningsdel (en del delas aldrig vidare, den blir OVER_SKALAN)')
|
|
1438
|
+
if (foralder.approval === null || foralder.approval === undefined)
|
|
1439
|
+
vagra('föräldern saknar godkännande (delning följer bedöm, som kräver ett godkännande)')
|
|
1440
|
+
// Delningen följer scorerns DELA_UPP (bedöm-routningen, ADR-0144): samma
|
|
1441
|
+
// avläsning som nasta-steg gör, aldrig en handdelning av en annan post.
|
|
1442
|
+
let utfall
|
|
1443
|
+
try {
|
|
1444
|
+
utfall = gallandeScorerUtfall(foralder, katalog)?.outcome ?? null
|
|
1445
|
+
} catch (e) {
|
|
1446
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1447
|
+
vagra(e.message)
|
|
1448
|
+
}
|
|
1449
|
+
if (foralder.status !== 'assessed' || utfall !== 'DELA_UPP')
|
|
1450
|
+
vagra(
|
|
1451
|
+
`föräldern står inte på scorerns DELA_UPP (status ${foralder.status}, utfall ${utfall ?? 'inget'})`,
|
|
1452
|
+
)
|
|
1453
|
+
if (of < 2) vagra(`en delning har minst två delar (fick --of ${of})`)
|
|
1454
|
+
const foralderId = foralder.request_id ?? path.basename(path.resolve(katalog))
|
|
1455
|
+
const givet = flag('request-id')
|
|
1456
|
+
if (givet !== null && givet !== foralderId)
|
|
1457
|
+
vagra(`--request-id ${givet} är inte förälderns id ${foralderId}`)
|
|
1458
|
+
for (const syskon of delKataloger(katalog).filter((d) => path.dirname(d) === katalog)) {
|
|
1459
|
+
let st
|
|
1460
|
+
try {
|
|
1461
|
+
st = readStateOrThrow(syskon)
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1464
|
+
vagra(e.message)
|
|
1465
|
+
}
|
|
1466
|
+
if (st.split?.parent != null && st.split.of !== of)
|
|
1467
|
+
vagra(`${path.basename(syskon)} har split.of ${st.split.of}, inte ${of}`)
|
|
1468
|
+
}
|
|
1469
|
+
requestId = foralderId
|
|
1470
|
+
arvtGodkannande = foralder.approval
|
|
1471
|
+
} else {
|
|
1472
|
+
requestId = requireFlag('request-id')
|
|
1473
|
+
}
|
|
1269
1474
|
const p = path.join(katalog, `del-${part}`)
|
|
1270
1475
|
try {
|
|
1271
1476
|
mkdirSync(p)
|
|
@@ -1274,12 +1479,17 @@ if (cmd === 'add-run') {
|
|
|
1274
1479
|
process.exit(2)
|
|
1275
1480
|
}
|
|
1276
1481
|
const ref = { part, of }
|
|
1277
|
-
const st =
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1482
|
+
const st = produktion
|
|
1483
|
+
? {
|
|
1484
|
+
...skelett(null, { split: { parent: requestId, ...ref }, experiment: false }),
|
|
1485
|
+
approval: arvtGodkannande,
|
|
1486
|
+
}
|
|
1487
|
+
: skelett(
|
|
1488
|
+
requestId,
|
|
1489
|
+
typ === 'bundle'
|
|
1490
|
+
? { bundle: { parent_slug: requestId, ...ref } }
|
|
1491
|
+
: { split: { parent: requestId, ...ref } },
|
|
1492
|
+
)
|
|
1283
1493
|
try {
|
|
1284
1494
|
writeStateOrThrow(st, p)
|
|
1285
1495
|
} catch (e) {
|
|
@@ -1555,15 +1765,16 @@ if (cmd === 'add-run') {
|
|
|
1555
1765
|
avvisaOkandaFlaggor('stopp', ['utfall', 'kalla', 'orsak'])
|
|
1556
1766
|
const utfall = requireFlag('utfall')
|
|
1557
1767
|
const kalla = requireFlag('kalla')
|
|
1558
|
-
|
|
1768
|
+
let orsak = flag('orsak')
|
|
1559
1769
|
if (!STOPP_UTFALL.includes(utfall)) {
|
|
1560
1770
|
console.error(`okänt --utfall: ${utfall} (giltiga: ${STOPP_UTFALL.join(', ')})`)
|
|
1561
1771
|
process.exit(2)
|
|
1562
1772
|
}
|
|
1563
1773
|
const arDesignKvitto = DESIGN_AVVISAD_RE.test(kalla)
|
|
1564
|
-
|
|
1774
|
+
const arIntervjuKvitto = DESIGN_INTERVJU_RE.test(kalla)
|
|
1775
|
+
if (!STOPP_KALLOR.includes(kalla) && !arDesignKvitto && !arIntervjuKvitto) {
|
|
1565
1776
|
console.error(
|
|
1566
|
-
`okänd --kalla: ${kalla} (giltiga: ${STOPP_KALLOR.join(', ')}, design-avvisad-<datum>.md)`,
|
|
1777
|
+
`okänd --kalla: ${kalla} (giltiga: ${STOPP_KALLOR.join(', ')}, design-avvisad-<datum>.md, design-intervju-kravs-<datum>.md)`,
|
|
1567
1778
|
)
|
|
1568
1779
|
process.exit(2)
|
|
1569
1780
|
}
|
|
@@ -1611,6 +1822,23 @@ if (cmd === 'add-run') {
|
|
|
1611
1822
|
)
|
|
1612
1823
|
process.exit(2)
|
|
1613
1824
|
}
|
|
1825
|
+
} else if (arIntervjuKvitto) {
|
|
1826
|
+
// P4-24: kvittot är designerns svar, och bara en första rad
|
|
1827
|
+
// `INTERVJU KRÄVS: <skäl>` är ett stopp. Det går till en människa
|
|
1828
|
+
// (designa, intervjun), aldrig tillbaka till specen.
|
|
1829
|
+
const forsta = text.replace(/\r\n/g, '\n').trimStart().split('\n')[0].trim()
|
|
1830
|
+
if (!/^INTERVJU KRÄVS:\s*\S/u.test(forsta)) {
|
|
1831
|
+
console.error(`${kallaPath} börjar inte med "INTERVJU KRÄVS: <skäl>", kan inte stoppas`)
|
|
1832
|
+
process.exit(2)
|
|
1833
|
+
}
|
|
1834
|
+
if (utfall !== 'ESKALERA') {
|
|
1835
|
+
console.error(`ett intervjukrav i ${kalla} stoppar som ESKALERA, inte ${utfall}`)
|
|
1836
|
+
process.exit(2)
|
|
1837
|
+
}
|
|
1838
|
+
if (!orsak) {
|
|
1839
|
+
console.error('ESKALERA kräver --orsak: en människa ska se varför designern inte skrev något')
|
|
1840
|
+
process.exit(2)
|
|
1841
|
+
}
|
|
1614
1842
|
} else if (kalla === 'granskarblock.md') {
|
|
1615
1843
|
// ADR-0118: bara ett ja på raden Prisavgörande öppet val är ett stopp.
|
|
1616
1844
|
if (prisavgorandeVal(text) === null) {
|
|
@@ -1645,6 +1873,26 @@ if (cmd === 'add-run') {
|
|
|
1645
1873
|
console.error(`en motsägelse i ${kalla} stoppar som TILLBAKA_TILL_SPEC, inte ${utfall}`)
|
|
1646
1874
|
process.exit(2)
|
|
1647
1875
|
}
|
|
1876
|
+
// #595: designkonflikten (texten efter etiketten börjar med `design
|
|
1877
|
+
// conflict:`) avgör routningen till designa (arDesignkonflikt), så den
|
|
1878
|
+
// läses ur filen, aldrig bara ur flaggan. Utan --orsak blir filens text
|
|
1879
|
+
// orsaken; en --orsak som motsäger filen (en annan konflikttext, en
|
|
1880
|
+
// specorsak mot en designkonflikt, eller en designkonflikt filen inte
|
|
1881
|
+
// bär) vägras. En vanlig motsägelse behåller anroparens fria orsak.
|
|
1882
|
+
const filText = avvikelseText(text)
|
|
1883
|
+
const filKonflikt = filText !== null && /^design conflict:/i.test(filText) ? filText : null
|
|
1884
|
+
const givenKonflikt = orsak !== null && /^design conflict:/i.test(orsak.trim())
|
|
1885
|
+
const motsager =
|
|
1886
|
+
orsak !== null && (filKonflikt !== null ? orsak.trim() !== filKonflikt : givenKonflikt)
|
|
1887
|
+
if (motsager) {
|
|
1888
|
+
console.error(
|
|
1889
|
+
`--orsak motsäger impact.md: filen säger ${
|
|
1890
|
+
filKonflikt === null ? 'ingen designkonflikt' : `"${filKonflikt}"`
|
|
1891
|
+
}, flaggan "${orsak}"`,
|
|
1892
|
+
)
|
|
1893
|
+
process.exit(2)
|
|
1894
|
+
}
|
|
1895
|
+
if (filKonflikt !== null) orsak = filKonflikt
|
|
1648
1896
|
}
|
|
1649
1897
|
|
|
1650
1898
|
const lockPath = acquireLock()
|
|
@@ -1703,18 +1951,28 @@ if (cmd === 'add-run') {
|
|
|
1703
1951
|
)
|
|
1704
1952
|
} else if (cmd === 'respec') {
|
|
1705
1953
|
// Speca steg 5 som kommando: en ny spec över befintlig katalog invaliderar
|
|
1706
|
-
// analysen, designen, godkännandet, domarna
|
|
1954
|
+
// analysen, designen, godkännandet, domarna, stoppet och brädkvittot
|
|
1955
|
+
// (ADR-0153 beslut 3: kvittot gällde en prisrunda som inte längre gäller). runs[] rörs
|
|
1707
1956
|
// aldrig, arkivet är append-only; den gamla analysen och designen döps
|
|
1708
1957
|
// om, raderas inte.
|
|
1709
1958
|
avvisaOkandaFlaggor('respec', [])
|
|
1710
1959
|
const lockPath = acquireLock()
|
|
1711
1960
|
let antal = 0
|
|
1961
|
+
let delState = null
|
|
1712
1962
|
try {
|
|
1713
1963
|
const state = readStateOrThrow()
|
|
1964
|
+
// ADR-0155 beslut 3: en levande produktionskörning skriver mot samma
|
|
1965
|
+
// katalog; vägra innan något läggs undan. Ett dött pid rensas nedan.
|
|
1966
|
+
if (state.korning && isAlive(state.korning.pid))
|
|
1967
|
+
throw new ArkivFel(`respec vägrar: pågår (pid ${state.korning.pid})`)
|
|
1968
|
+
delState = state
|
|
1714
1969
|
if (laggUndan(katalog, 'impact')) antal++
|
|
1715
1970
|
// Designa (spec 2026-09-24): en ny spec gör den gamla designen inaktuell
|
|
1716
1971
|
// av samma skäl som den gör analysen inaktuell. Läggs undan, raderas inte.
|
|
1717
1972
|
if (laggUndan(katalog, 'design')) antal++
|
|
1973
|
+
// ADR-0156 beslut 3: granskningen gällde den gamla designen och följer
|
|
1974
|
+
// den, undanlagd; state.design.review nollas med state.design nedan.
|
|
1975
|
+
if (laggUndan(katalog, 'design-review')) antal++
|
|
1718
1976
|
const nytt = { ...state }
|
|
1719
1977
|
if (nytt.status !== 'spec_draft') {
|
|
1720
1978
|
nytt.status = 'spec_draft'
|
|
@@ -1736,6 +1994,22 @@ if (cmd === 'add-run') {
|
|
|
1736
1994
|
nytt.design = null
|
|
1737
1995
|
antal++
|
|
1738
1996
|
}
|
|
1997
|
+
if (nytt.board !== null && nytt.board !== undefined) {
|
|
1998
|
+
nytt.board = null
|
|
1999
|
+
antal++
|
|
2000
|
+
}
|
|
2001
|
+
// ADR-0159: täckningsposten hörde till en prisrunda som inte längre
|
|
2002
|
+
// gäller; nästa poängrunda räknar fram den på nytt.
|
|
2003
|
+
if (nytt.coverage_post !== null && nytt.coverage_post !== undefined) {
|
|
2004
|
+
nytt.coverage_post = null
|
|
2005
|
+
antal++
|
|
2006
|
+
}
|
|
2007
|
+
// Ett dött kvitto (levande vägrades ovan) hör till en körning som inte
|
|
2008
|
+
// finns; en ny spec nollar det.
|
|
2009
|
+
if (nytt.korning !== null && nytt.korning !== undefined) {
|
|
2010
|
+
nytt.korning = null
|
|
2011
|
+
antal++
|
|
2012
|
+
}
|
|
1739
2013
|
// Monoton räknare (#505): varje respec-anrop är ett nytt varv oavsett om
|
|
1740
2014
|
// övriga fält redan var normaliserade, till skillnad från fälten ovan
|
|
1741
2015
|
// finns inget "redan rätt värde" att jämföra mot, så det här villkoret
|
|
@@ -1752,7 +2026,7 @@ if (cmd === 'add-run') {
|
|
|
1752
2026
|
throw e
|
|
1753
2027
|
}
|
|
1754
2028
|
releaseLock(lockPath)
|
|
1755
|
-
console.log(`OK: respec (${antal} ändringar)`)
|
|
2029
|
+
console.log(`OK: respec (${antal} ändringar${nollaRotensKvitto(delState)})`)
|
|
1756
2030
|
} else if (cmd === 'add-design') {
|
|
1757
2031
|
// Designa steg 5 (spec 2026-09-24): designdokumentet verifieras, kopieras
|
|
1758
2032
|
// byteidentiskt in som design.md, bokförs i state.design och sätter status
|
|
@@ -1775,8 +2049,113 @@ if (cmd === 'add-run') {
|
|
|
1775
2049
|
throw e
|
|
1776
2050
|
}
|
|
1777
2051
|
releaseLock(lockPath)
|
|
2052
|
+
// ADR-0156 beslut 4: intervjudokumentets saknade utkastsektioner och en
|
|
2053
|
+
// symbolkontroll som inte kunde köras vägrar inte; de syns på stderr.
|
|
2054
|
+
for (const v of kvitto.varningar) console.error(`VARNING: ${v}`)
|
|
1778
2055
|
console.log(`OK: design ${kvitto.design.path} (${kvitto.design.sha256})`)
|
|
1779
2056
|
if (kvitto.impactUndanlagd) console.log('OK: impact.md undanlagd (designkonfliktstoppet löst)')
|
|
2057
|
+
} else if (cmd === 'add-design-review') {
|
|
2058
|
+
// ADR-0156 beslut 3: designgranskarens svar kopieras byteidentiskt in som
|
|
2059
|
+
// design-review.md och bokförs i state.design.review. Ingen runs-rad:
|
|
2060
|
+
// granskningen är intern som designen, kostnaden står i korning.json. En
|
|
2061
|
+
// tidigare granskning läggs undan som design-review-superseded-<datum>.md.
|
|
2062
|
+
// --model lagras som det ges (korning.mjs skickar MODELL:-svansraden).
|
|
2063
|
+
avvisaOkandaFlaggor('add-design-review', ['file', 'verdict', 'model'])
|
|
2064
|
+
const fil = requireFlag('file')
|
|
2065
|
+
const verdict = requireFlag('verdict')
|
|
2066
|
+
const model = requireFlag('model')
|
|
2067
|
+
if (verdict !== 'fortsätt' && verdict !== 'stopp') {
|
|
2068
|
+
console.error(
|
|
2069
|
+
`add-design-review vägrar: --verdict måste vara fortsätt eller stopp, fick "${verdict}"`,
|
|
2070
|
+
)
|
|
2071
|
+
process.exit(2)
|
|
2072
|
+
}
|
|
2073
|
+
const lockPath = acquireLock()
|
|
2074
|
+
let sha256
|
|
2075
|
+
try {
|
|
2076
|
+
const state = readStateOrThrow()
|
|
2077
|
+
if (state.experiment === true) throw new ArkivFel('add-design-review vägrar: experimentkatalog')
|
|
2078
|
+
if (state.design === null || state.design === undefined)
|
|
2079
|
+
throw new ArkivFel('add-design-review vägrar: ingen design att granska')
|
|
2080
|
+
// Bytes läses före undanläggningen, så även katalogens egen
|
|
2081
|
+
// design-review.md går att peka ut utan att kopian faller på en flyttad fil.
|
|
2082
|
+
let bytes
|
|
2083
|
+
try {
|
|
2084
|
+
bytes = readFileSync(path.resolve(fil))
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
throw new ArkivFel(`add-design-review vägrar: kan inte läsa ${fil}: ${e.message}`)
|
|
2087
|
+
}
|
|
2088
|
+
laggUndan(katalog, 'design-review')
|
|
2089
|
+
const mal = path.join(katalog, 'design-review.md')
|
|
2090
|
+
try {
|
|
2091
|
+
writeFileSync(mal, bytes)
|
|
2092
|
+
} catch (e) {
|
|
2093
|
+
throw new ArkivFel(`kunde inte skriva ${mal}: ${e.message}`)
|
|
2094
|
+
}
|
|
2095
|
+
sha256 = markerStrippedSha256(mal)
|
|
2096
|
+
writeStateOrThrow({
|
|
2097
|
+
...state,
|
|
2098
|
+
design: { ...state.design, review: { verdict, sha256, date: today(), model } },
|
|
2099
|
+
})
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
releaseLock(lockPath)
|
|
2102
|
+
if (e instanceof ArkivFel) {
|
|
2103
|
+
console.error(e.message)
|
|
2104
|
+
process.exit(2)
|
|
2105
|
+
}
|
|
2106
|
+
throw e
|
|
2107
|
+
}
|
|
2108
|
+
releaseLock(lockPath)
|
|
2109
|
+
console.log(`OK: design-review ${verdict} (${sha256})`)
|
|
2110
|
+
} else if (cmd === 'antaganden') {
|
|
2111
|
+
// ADR-0156 beslut 5: läs-enbart lista inför "synka" över varje antagande
|
|
2112
|
+
// utkastet gjorde själv (fråga, valt, ej valt, skäl), granskningens utslag
|
|
2113
|
+
// med antal fynd och spec conflicts, ur design.md och state.json, aldrig
|
|
2114
|
+
// handskriven. Inget lås, ingen skrivning.
|
|
2115
|
+
avvisaOkandaFlaggor('antaganden', [])
|
|
2116
|
+
const ut = []
|
|
2117
|
+
try {
|
|
2118
|
+
let text
|
|
2119
|
+
try {
|
|
2120
|
+
text = readFileSync(path.join(katalog, 'design.md'), 'utf8')
|
|
2121
|
+
} catch {
|
|
2122
|
+
throw new ArkivFel('antaganden vägrar: ingen design')
|
|
2123
|
+
}
|
|
2124
|
+
const state = readStateOrThrow()
|
|
2125
|
+
ut.push(`Designläge: ${designMode(text)}`)
|
|
2126
|
+
const antaganden = designAssumptions(text)
|
|
2127
|
+
if (antaganden === null) ut.push('Antaganden: avsnitt saknas')
|
|
2128
|
+
else if (!Array.isArray(antaganden)) ut.push(`Antaganden: felformade (${antaganden.fel})`)
|
|
2129
|
+
else if (antaganden.length === 0) ut.push('Antaganden: inga')
|
|
2130
|
+
else {
|
|
2131
|
+
ut.push(`Antaganden (${antaganden.length}):`)
|
|
2132
|
+
antaganden.forEach((a, i) => {
|
|
2133
|
+
ut.push(
|
|
2134
|
+
`${i + 1}. ${a.fraga}`,
|
|
2135
|
+
` vald: ${a.vald}`,
|
|
2136
|
+
` ej valda: ${a.ejValda.length > 0 ? a.ejValda.join('; ') : 'inga'}`,
|
|
2137
|
+
` skäl: ${a.skal}`,
|
|
2138
|
+
)
|
|
2139
|
+
})
|
|
2140
|
+
}
|
|
2141
|
+
const review = state.design?.review ?? null
|
|
2142
|
+
ut.push(
|
|
2143
|
+
review === null
|
|
2144
|
+
? 'Granskning: ingen'
|
|
2145
|
+
: `Granskning: ${review.verdict} (${antalFynd(path.join(katalog, 'design-review.md'))})`,
|
|
2146
|
+
)
|
|
2147
|
+
const konflikter = specConflicts(text)
|
|
2148
|
+
if (konflikter === null) ut.push('Spec conflicts: avsnitt saknas')
|
|
2149
|
+
else if (konflikter.length === 0) ut.push('Spec conflicts: none')
|
|
2150
|
+
else ut.push(`Spec conflicts: ${konflikter.length}`, ...konflikter.map((k) => ` ${k}`))
|
|
2151
|
+
} catch (e) {
|
|
2152
|
+
if (e instanceof ArkivFel) {
|
|
2153
|
+
console.error(e.message)
|
|
2154
|
+
process.exit(2)
|
|
2155
|
+
}
|
|
2156
|
+
throw e
|
|
2157
|
+
}
|
|
2158
|
+
console.log(ut.join('\n'))
|
|
1780
2159
|
} else if (cmd === 'set-split-gid') {
|
|
1781
2160
|
// ADR-0144 beslut 6: delens Asana-deluppgift bokförs i split.gid när
|
|
1782
2161
|
// request-sync har skapat den, så en omsynk uppdaterar deluppgiften i
|
|
@@ -1813,6 +2192,186 @@ if (cmd === 'add-run') {
|
|
|
1813
2192
|
}
|
|
1814
2193
|
releaseLock(lockPath)
|
|
1815
2194
|
console.log(oforandrat ? `OK: split-gid ${gid} oförändrat` : `OK: split-gid ${gid}`)
|
|
2195
|
+
} else if (cmd === 'approve') {
|
|
2196
|
+
// ADR-0153 beslut 2: approval skrivs bara här. Grinden (designa eller
|
|
2197
|
+
// bedöm) läser brädan och skickar Offer status som den lästes; allt annat
|
|
2198
|
+
// än kärnan specok vägras, så en grind som inte lästs kan inte noteras.
|
|
2199
|
+
// En tidigare notering skrivs över: grinden läses om varje gång.
|
|
2200
|
+
avvisaOkandaFlaggor('approve', ['source', 'status', 'by'])
|
|
2201
|
+
const source = requireFlag('source')
|
|
2202
|
+
if (source !== 'asana' && source !== 'manual') {
|
|
2203
|
+
console.error(`--source måste vara asana eller manual, fick "${source}"`)
|
|
2204
|
+
process.exit(2)
|
|
2205
|
+
}
|
|
2206
|
+
// En flagga som hör till den andra källan tappas aldrig tyst: den vägras.
|
|
2207
|
+
if (source === 'asana' && has('by')) {
|
|
2208
|
+
console.error('--by hör inte till --source asana')
|
|
2209
|
+
process.exit(2)
|
|
2210
|
+
}
|
|
2211
|
+
if (source === 'manual' && has('status')) {
|
|
2212
|
+
console.error('--status hör inte till --source manual')
|
|
2213
|
+
process.exit(2)
|
|
2214
|
+
}
|
|
2215
|
+
let approval
|
|
2216
|
+
if (source === 'asana') {
|
|
2217
|
+
const status = requireFlag('status')
|
|
2218
|
+
if (alfabetiskKarna(status) !== 'specok') {
|
|
2219
|
+
console.error(`approve vägrar: Offer status "${status}" är inte ✅ Spec ok`)
|
|
2220
|
+
process.exit(2)
|
|
2221
|
+
}
|
|
2222
|
+
approval = { source, status_read: status, date: today() }
|
|
2223
|
+
} else {
|
|
2224
|
+
const by = requireFlag('by')
|
|
2225
|
+
if (by.trim() === '') {
|
|
2226
|
+
console.error('--by får inte vara tom')
|
|
2227
|
+
process.exit(2)
|
|
2228
|
+
}
|
|
2229
|
+
approval = { source, by, date: today() }
|
|
2230
|
+
}
|
|
2231
|
+
const lockPath = acquireLock()
|
|
2232
|
+
try {
|
|
2233
|
+
const state = readStateOrThrow()
|
|
2234
|
+
if (state.experiment === true) throw new ArkivFel('approve vägrar: experimentkatalog')
|
|
2235
|
+
writeStateOrThrow({ ...state, approval })
|
|
2236
|
+
} catch (e) {
|
|
2237
|
+
releaseLock(lockPath)
|
|
2238
|
+
if (e instanceof ArkivFel) {
|
|
2239
|
+
console.error(e.message)
|
|
2240
|
+
process.exit(2)
|
|
2241
|
+
}
|
|
2242
|
+
throw e
|
|
2243
|
+
}
|
|
2244
|
+
releaseLock(lockPath)
|
|
2245
|
+
console.log(source === 'asana' ? 'OK: approval asana' : `OK: approval manual by ${approval.by}`)
|
|
2246
|
+
} else if (cmd === 'korning-start') {
|
|
2247
|
+
// ADR-0155 beslut 3: bedom.mjs bokför sin process här innan den spawnar
|
|
2248
|
+
// prisningen, så nasta-steg kan säga "pågår" och en andra start vägras.
|
|
2249
|
+
// Ett dött pid övertas med en varning; EPERM räknas som levande (isAlive).
|
|
2250
|
+
avvisaOkandaFlaggor('korning-start', ['pid', 'katalog-korning'])
|
|
2251
|
+
const pidTxt = flag('pid')
|
|
2252
|
+
if (pidTxt === null || !/^\d+$/.test(pidTxt) || Number(pidTxt) <= 0) {
|
|
2253
|
+
console.error('korning-start kräver --pid <heltal>')
|
|
2254
|
+
process.exit(2)
|
|
2255
|
+
}
|
|
2256
|
+
const pid = Number(pidTxt)
|
|
2257
|
+
const katalogKorning = flag('katalog-korning')
|
|
2258
|
+
const lockPath = acquireLock()
|
|
2259
|
+
let varning = null
|
|
2260
|
+
try {
|
|
2261
|
+
const state = readStateOrThrow()
|
|
2262
|
+
if (state.experiment === true) throw new ArkivFel('korning-start vägrar: experimentkatalog')
|
|
2263
|
+
if (state.korning) {
|
|
2264
|
+
if (isAlive(state.korning.pid))
|
|
2265
|
+
throw new ArkivFel(`korning-start vägrar: pågår (pid ${state.korning.pid})`)
|
|
2266
|
+
varning = `VARNING: tog över död körning (pid ${state.korning.pid})`
|
|
2267
|
+
}
|
|
2268
|
+
// `started` är en full ISO-tidsstämpel, den enda i state.json (övriga
|
|
2269
|
+
// datum är today()): en körning behöver klockslag för att kunna läsas.
|
|
2270
|
+
writeStateOrThrow({
|
|
2271
|
+
...state,
|
|
2272
|
+
korning: { pid, started: new Date().toISOString(), katalog: katalogKorning },
|
|
2273
|
+
})
|
|
2274
|
+
} catch (e) {
|
|
2275
|
+
releaseLock(lockPath)
|
|
2276
|
+
if (e instanceof ArkivFel) {
|
|
2277
|
+
console.error(e.message)
|
|
2278
|
+
process.exit(2)
|
|
2279
|
+
}
|
|
2280
|
+
throw e
|
|
2281
|
+
}
|
|
2282
|
+
releaseLock(lockPath)
|
|
2283
|
+
if (varning) console.error(varning)
|
|
2284
|
+
console.log(`OK: korning-start pid ${pid}`)
|
|
2285
|
+
} else if (cmd === 'korning-slut') {
|
|
2286
|
+
// ADR-0155 beslut 3: kvittot rensas när bedom.mjs är klar. Utan kvitto är
|
|
2287
|
+
// det ett no-op, aldrig ett fel: slutet ska kunna köras i en finally.
|
|
2288
|
+
avvisaOkandaFlaggor('korning-slut', [])
|
|
2289
|
+
const lockPath = acquireLock()
|
|
2290
|
+
try {
|
|
2291
|
+
const state = readStateOrThrow()
|
|
2292
|
+
if (state.experiment === true) throw new ArkivFel('korning-slut vägrar: experimentkatalog')
|
|
2293
|
+
if (state.korning !== null) writeStateOrThrow({ ...state, korning: null })
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
releaseLock(lockPath)
|
|
2296
|
+
if (e instanceof ArkivFel) {
|
|
2297
|
+
console.error(e.message)
|
|
2298
|
+
process.exit(2)
|
|
2299
|
+
}
|
|
2300
|
+
throw e
|
|
2301
|
+
}
|
|
2302
|
+
releaseLock(lockPath)
|
|
2303
|
+
console.log('OK: korning-slut')
|
|
2304
|
+
} else if (cmd === 'rot') {
|
|
2305
|
+
// ADR-0154: arkivets enda hem, löst från katalogen (default cwd). `katalog`
|
|
2306
|
+
// är här cwd att lösa från, inte en post; en flagga i första läget är ingen
|
|
2307
|
+
// katalog. Läs-enbart, ingen lås.
|
|
2308
|
+
const flaggaForst = katalog !== undefined && katalog.startsWith('-')
|
|
2309
|
+
const tokens = flaggaForst ? [katalog, ...rest] : rest
|
|
2310
|
+
if (tokens.length > 0) {
|
|
2311
|
+
console.error(
|
|
2312
|
+
`rot: okänd eller ej konsumerad flagga/argument (${tokens.join(' ')}), kommandot tar inga flaggor`,
|
|
2313
|
+
)
|
|
2314
|
+
process.exit(2)
|
|
2315
|
+
}
|
|
2316
|
+
const fran = path.resolve(katalog ?? process.cwd())
|
|
2317
|
+
const r = arkivRot(fran)
|
|
2318
|
+
if (!r) {
|
|
2319
|
+
console.error(`rot vägrar: ingen git-katalog (${fran})`)
|
|
2320
|
+
process.exit(2)
|
|
2321
|
+
}
|
|
2322
|
+
if (r.arkivRot === null) {
|
|
2323
|
+
console.error(
|
|
2324
|
+
`rot vägrar: git-common-dir heter inte .git (${r.commonDir}); kör från superprojektets egen utcheckning`,
|
|
2325
|
+
)
|
|
2326
|
+
process.exit(2)
|
|
2327
|
+
}
|
|
2328
|
+
console.log(`ARKIVROT: ${r.arkivRot}`)
|
|
2329
|
+
console.log(`IGNORERAD: ${r.ignorerad ? 'ja' : 'nej'}`)
|
|
2330
|
+
// Ett herrelöst arkiv i cwd:ns egen utcheckning (en worktree med eget
|
|
2331
|
+
// requests/, Convendums läge mellan släppet och flytten) får en VARNING på
|
|
2332
|
+
// stdout som precheck.mjs, exit fortfarande 0.
|
|
2333
|
+
const topp = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
2334
|
+
cwd: fran,
|
|
2335
|
+
encoding: 'utf8',
|
|
2336
|
+
timeout: 5000,
|
|
2337
|
+
})
|
|
2338
|
+
const toppDir = topp.status === 0 ? String(topp.stdout ?? '').trim() : ''
|
|
2339
|
+
if (toppDir) {
|
|
2340
|
+
const strat = path.join(path.resolve(toppDir), '.haus-workflow', 'requests')
|
|
2341
|
+
if (strat !== path.resolve(r.arkivRot) && existsSync(strat))
|
|
2342
|
+
console.log(`VARNING: arkiv i worktree: ${strat} (flytta per ADR-0154)`)
|
|
2343
|
+
}
|
|
2344
|
+
} else if (cmd === 'synced') {
|
|
2345
|
+
// ADR-0153 beslut 3: brädskrivningen i speca steg 7 och bedöm steg 8 följs
|
|
2346
|
+
// av det här kvittot i state.board, Offer status ordagrant som request-sync
|
|
2347
|
+
// satte den. Varje sync är ett nytt kvitto och skriver över det förra.
|
|
2348
|
+
// Arkivet är fortfarande en cache: Closing-radens brädläge läses ur Asana.
|
|
2349
|
+
avvisaOkandaFlaggor('synced', ['offer-status'])
|
|
2350
|
+
const offerStatus = requireFlag('offer-status')
|
|
2351
|
+
if (offerStatus.trim() === '') {
|
|
2352
|
+
console.error('--offer-status får inte vara tom')
|
|
2353
|
+
process.exit(2)
|
|
2354
|
+
}
|
|
2355
|
+
const board = { offer_status: offerStatus, date: today() }
|
|
2356
|
+
const lockPath = acquireLock()
|
|
2357
|
+
try {
|
|
2358
|
+
const state = readStateOrThrow()
|
|
2359
|
+
if (state.experiment === true) throw new ArkivFel('synced vägrar: experimentkatalog')
|
|
2360
|
+
if (state.split?.parent != null)
|
|
2361
|
+
throw new ArkivFel(
|
|
2362
|
+
'synced vägrar: en delningsdel har ingen egen Offer status (split.gid är kvittot)',
|
|
2363
|
+
)
|
|
2364
|
+
writeStateOrThrow({ ...state, board })
|
|
2365
|
+
} catch (e) {
|
|
2366
|
+
releaseLock(lockPath)
|
|
2367
|
+
if (e instanceof ArkivFel) {
|
|
2368
|
+
console.error(e.message)
|
|
2369
|
+
process.exit(2)
|
|
2370
|
+
}
|
|
2371
|
+
throw e
|
|
2372
|
+
}
|
|
2373
|
+
releaseLock(lockPath)
|
|
2374
|
+
console.log(`OK: synced ${offerStatus}`)
|
|
1816
2375
|
} else if (cmd === 'paraply') {
|
|
1817
2376
|
// ADR-0144 beslut 5: förälderns spec skrivs om som en paraplyspec efter ett
|
|
1818
2377
|
// ja. Den gamla spec.md läggs undan (spec-superseded-<datum>.md),
|
|
@@ -1898,7 +2457,9 @@ if (cmd === 'add-run') {
|
|
|
1898
2457
|
? 'prisavgörande val (granskarblock.md)'
|
|
1899
2458
|
: DESIGN_AVVISAD_RE.test(state.stopp.kalla ?? '')
|
|
1900
2459
|
? `designkonflikt (${state.stopp.kalla})`
|
|
1901
|
-
:
|
|
2460
|
+
: arIntervjuStopp(state.stopp)
|
|
2461
|
+
? `intervju krävs (${state.stopp.kalla})`
|
|
2462
|
+
: 'motsägelse (impact.md)'
|
|
1902
2463
|
}
|
|
1903
2464
|
// ADR-0144 beslut 6: delningsroten visar hur många delar som är klara,
|
|
1904
2465
|
// samma räkning som nasta-steg (delningslage), före täckningsposten.
|
|
@@ -1957,7 +2518,7 @@ if (cmd === 'add-run') {
|
|
|
1957
2518
|
console.log(rader.join('\n'))
|
|
1958
2519
|
} else {
|
|
1959
2520
|
console.error(
|
|
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)',
|
|
2521
|
+
'okänt kommando (add-run, next-n, ny-korning, ny-del, proveniens, sparr, utslag, set-status, stopp, respec, add-design, add-design-review, antaganden, set-split-gid, approve, synced, rot, paraply, bluf, nasta-steg, kalibrering, experiment)',
|
|
1961
2522
|
)
|
|
1962
2523
|
process.exit(2)
|
|
1963
2524
|
}
|
|
@@ -1985,6 +2546,10 @@ function tolkaSatt(par) {
|
|
|
1985
2546
|
}
|
|
1986
2547
|
return [nyckel, ra === 'true']
|
|
1987
2548
|
}
|
|
2549
|
+
if (nyckel === 'design_mode' && ra !== 'draft' && ra !== 'intervju') {
|
|
2550
|
+
console.error(`experiment: design_mode tar bara draft eller intervju, fick "${ra}"`)
|
|
2551
|
+
process.exit(2)
|
|
2552
|
+
}
|
|
1988
2553
|
return [nyckel, ra]
|
|
1989
2554
|
}
|
|
1990
2555
|
|
|
@@ -2034,7 +2599,9 @@ function delKataloger(dir) {
|
|
|
2034
2599
|
|
|
2035
2600
|
/** Lägger undan <bas>.md som <bas>-superseded-<datum>.md, numrerad svans vid
|
|
2036
2601
|
* kollision (-2, -3 …), aldrig en överskrivning: arkivet raderar inget.
|
|
2037
|
-
* Delad av respec (impact.md och design.md)
|
|
2602
|
+
* Delad av respec (impact.md, design.md och design-review.md), add-design
|
|
2603
|
+
* (design.md och design-review.md), add-design-review (design-review.md),
|
|
2604
|
+
* paraply (spec.md) och add-design:s designkonfliktstopp (impact.md).
|
|
2038
2605
|
* Returnerar true när en fil flyttades. */
|
|
2039
2606
|
function laggUndan(d, bas) {
|
|
2040
2607
|
const fil = path.join(d, `${bas}.md`)
|
|
@@ -2050,6 +2617,28 @@ function laggUndan(d, bas) {
|
|
|
2050
2617
|
return true
|
|
2051
2618
|
}
|
|
2052
2619
|
|
|
2620
|
+
/** antaganden (ADR-0156 beslut 5): fyndräkningen i design-review.md som text
|
|
2621
|
+
* för Granskning-raden. Ett fynd är en listpunkt (`- ` eller `* `) under
|
|
2622
|
+
* `## Findings` på den första punktens indrag; en djupare indragen
|
|
2623
|
+
* underpunkt hör till fyndet ovanför och räknas inte. Den enda punkten
|
|
2624
|
+
* `none` är 0. En saknad fil eller sektion säger det, aldrig en gissad
|
|
2625
|
+
* nolla. */
|
|
2626
|
+
function antalFynd(fil) {
|
|
2627
|
+
let text
|
|
2628
|
+
try {
|
|
2629
|
+
text = readFileSync(fil, 'utf8')
|
|
2630
|
+
} catch {
|
|
2631
|
+
return 'fynd okända, design-review.md saknas'
|
|
2632
|
+
}
|
|
2633
|
+
const rader = sectionLines(text.replace(/\r\n/g, '\n'), 'Findings')
|
|
2634
|
+
if (rader === null) return 'fynd okända, ## Findings saknas'
|
|
2635
|
+
const listpunkter = rader.map((r) => /^([ \t]*)[-*] (.*)$/.exec(r)).filter((m) => m !== null)
|
|
2636
|
+
const indrag = listpunkter[0]?.[1] ?? ''
|
|
2637
|
+
const punkter = listpunkter.filter((m) => m[1] === indrag).map((m) => m[2].trim())
|
|
2638
|
+
const antal = punkter.length === 1 && punkter[0].toLowerCase() === 'none' ? 0 : punkter.length
|
|
2639
|
+
return `${antal} fynd`
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2053
2642
|
/** Repo-roten för en arkivkatalog: föräldern till närmaste `.haus-workflow`
|
|
2054
2643
|
* uppåt i sökvägen (arkivet bor i <rot>/.haus-workflow/requests/). null när
|
|
2055
2644
|
* katalogen inte ligger under någon; add-design vägrar då i stället för att
|
|
@@ -2065,14 +2654,17 @@ function repoRotFor(d) {
|
|
|
2065
2654
|
}
|
|
2066
2655
|
|
|
2067
2656
|
/** add-design: vägransreglerna i ordning (rot, fil, inneslutning, arkivet,
|
|
2068
|
-
* frontmatter, spec-hash, konflikter, status, stopp
|
|
2657
|
+
* frontmatter, spec-hash, konflikter, status, stopp, utkastsektioner,
|
|
2658
|
+
* symboler), sedan undanläggning,
|
|
2069
2659
|
* kopia och state. Inneslutningen prövas på realpath (ADR-0082: en länk
|
|
2070
2660
|
* matchas på målet), och sökvägen som bokförs är realpath så precheck
|
|
2071
2661
|
* --design hittar samma fil oavsett länkar. Ett designkonfliktstopp ur
|
|
2072
2662
|
* impact.md nollas (en omdesign besvarar det, slutgranskning I2) och
|
|
2073
2663
|
* impact.md läggs undan med det; varje annat stopp vägras. Kastar
|
|
2074
|
-
* ArkivFel, skriver inget före sista steget. Returnerar designkvittot
|
|
2075
|
-
*
|
|
2664
|
+
* ArkivFel, skriver inget före sista steget. Returnerar designkvittot, om
|
|
2665
|
+
* en analys lades undan och varningarna (ADR-0156 beslut 4: utkastsektioner
|
|
2666
|
+
* på ett intervjudokument, en symbolkontroll som inte kunde köras), som
|
|
2667
|
+
* anroparen skriver på stderr. */
|
|
2076
2668
|
function laggTillDesign(state, fil) {
|
|
2077
2669
|
const rot = repoRotFor(katalog)
|
|
2078
2670
|
if (rot === null)
|
|
@@ -2131,17 +2723,44 @@ function laggTillDesign(state, fil) {
|
|
|
2131
2723
|
`${statePath()} har status ${state.status}, en prissatt post designas inte om utan respec`,
|
|
2132
2724
|
)
|
|
2133
2725
|
const stopp = state.stopp ?? null
|
|
2134
|
-
|
|
2726
|
+
// P4-24: ett intervjukrav (design-intervju-kravs-<datum>.md) besvaras av
|
|
2727
|
+
// designa, så designen nollar det som designkonfliktstoppet.
|
|
2728
|
+
if (stopp !== null && !arDesignkonflikt(stopp) && !arIntervjuStopp(stopp))
|
|
2135
2729
|
throw new ArkivFel(
|
|
2136
2730
|
`${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
2731
|
)
|
|
2732
|
+
// ADR-0156 beslut 4, efter de billiga statusvägrarna så en vägrad post
|
|
2733
|
+
// aldrig startar git: ett utkast (mode: draft) bär Assumptions, Removed or
|
|
2734
|
+
// renamed och Files touched, samma rader som precheck --design (på ett
|
|
2735
|
+
// intervjudokument varningar). Varje borttagen eller omdöpt symbol greppas
|
|
2736
|
+
// i repot där dokumentet bor, utanför filerna designen rör: en träff
|
|
2737
|
+
// vägrar, en kontroll som inte kunde köras är en varning.
|
|
2738
|
+
const designRot = repoRotForFil(real)
|
|
2739
|
+
const form = designForm(text, { repoRot: designRot })
|
|
2740
|
+
if (form.brister.length > 0) throw new ArkivFel(form.brister.join('\n'))
|
|
2741
|
+
const { traffar, varning } = designSymbolkontroll(text, designRot)
|
|
2742
|
+
if (traffar.length > 0)
|
|
2743
|
+
throw new ArkivFel(
|
|
2744
|
+
traffar
|
|
2745
|
+
.map(
|
|
2746
|
+
(t) =>
|
|
2747
|
+
`add-design vägrar: symbolen \`${t.symbol}\` används fortfarande: ${t.fil}:${t.rad}`,
|
|
2748
|
+
)
|
|
2749
|
+
.join('\n'),
|
|
2750
|
+
)
|
|
2751
|
+
const varningar = [...form.varningar, ...(varning ? [varning] : [])]
|
|
2138
2752
|
laggUndan(katalog, 'design')
|
|
2753
|
+
// Granskningen gällde den ersatta designen och följer den, undanlagd som i
|
|
2754
|
+
// respec; state.design.review följer aldrig med (nytt objekt nedan).
|
|
2755
|
+
laggUndan(katalog, 'design-review')
|
|
2139
2756
|
const mal = path.join(katalog, 'design.md')
|
|
2140
2757
|
try {
|
|
2141
2758
|
copyFileSync(real, mal)
|
|
2142
2759
|
} catch (e) {
|
|
2143
2760
|
throw new ArkivFel(`kunde inte kopiera ${real} till ${mal}: ${e.message}`)
|
|
2144
2761
|
}
|
|
2762
|
+
// Ett nytt objekt: en ny designversion har ingen granskning än, så en
|
|
2763
|
+
// tidigare state.design.review följer aldrig med (ADR-0156 beslut 3).
|
|
2145
2764
|
const design = {
|
|
2146
2765
|
path: real,
|
|
2147
2766
|
file: 'design.md',
|
|
@@ -2155,14 +2774,14 @@ function laggTillDesign(state, fil) {
|
|
|
2155
2774
|
// en ny analys är skyldig oavsett omdesignens bytes, och en byteidentisk
|
|
2156
2775
|
// omdesign hade annars lämnat motsägelseanalysen proveniensgiltig. runs[]
|
|
2157
2776
|
// rörs inte, som i respec.
|
|
2158
|
-
const impactUndanlagd = stopp
|
|
2777
|
+
const impactUndanlagd = arDesignkonflikt(stopp) ? laggUndan(katalog, 'impact') : false
|
|
2159
2778
|
writeStateOrThrow({
|
|
2160
2779
|
...state,
|
|
2161
2780
|
design,
|
|
2162
2781
|
status: 'designed',
|
|
2163
2782
|
...(stopp !== null ? { stopp: null } : {}),
|
|
2164
2783
|
})
|
|
2165
|
-
return { design, impactUndanlagd }
|
|
2784
|
+
return { design, impactUndanlagd, varningar }
|
|
2166
2785
|
}
|
|
2167
2786
|
|
|
2168
2787
|
/** paraply (ADR-0144 beslut 5): vägransreglerna i ordning (delningsdel,
|
|
@@ -2280,6 +2899,9 @@ function delKlar(state, d) {
|
|
|
2280
2899
|
* | ------------------------------------- | ------------------------------------------------------------- |
|
|
2281
2900
|
* | bunt-rot (ingen spec.md, del-N finns) | The experiment area: `experiments/`, buntregeln; |
|
|
2282
2901
|
* | | bluf:s `bunt (N delar)` |
|
|
2902
|
+
* | korning.pid lever | ADR-0155 beslut 3: bedom.mjs bokför sin process med |
|
|
2903
|
+
* | | korning-start; raden säger pågår, ett dött pid prefixar |
|
|
2904
|
+
* | | `står` med `avbruten körning, ` och raden räknas som vanligt |
|
|
2283
2905
|
* | spärr står (sparrSkal) | bedöm steg 6 OLIKA "Hänvisa sedan till `döm <post>`"; `döm` |
|
|
2284
2906
|
* | stopp KONCEPT/DIAGNOS | speca steg 6 "Kandidatens katalog kan fortfarande läggas på |
|
|
2285
2907
|
* | | brädan … men `bedöm` hoppar över den"; stopputfall → kunddialog |
|
|
@@ -2287,6 +2909,8 @@ function delKlar(state, d) {
|
|
|
2287
2909
|
* | med orsak `design conflict: …` | routes it to `designa <adress>`"; add-design nollar stoppet |
|
|
2288
2910
|
* | | och lägger undan impact.md, ett kvitto ur designa steg 5 |
|
|
2289
2911
|
* | | ersätter det (då gäller raden nedan) |
|
|
2912
|
+
* | stopp ESKALERA ur | designa Draft mode (P4-24): designern svarade INTERVJU |
|
|
2913
|
+
* | design-intervju-kravs-<datum>.md | KRÄVS; designa (intervjun) löser det, add-design nollar det |
|
|
2290
2914
|
* | stopp TILLBAKA_TILL_SPEC | speca steg 6 (ADR-0118) "När valet avgjorts skrivs svaret |
|
|
2291
2915
|
* | | under `## Avgjorda val` …, sedan `respec` och ny dispatch"; |
|
|
2292
2916
|
* | | designa steg 5, ett kvitto design-avvisad-<datum>.md |
|
|
@@ -2307,7 +2931,8 @@ function delKlar(state, d) {
|
|
|
2307
2931
|
* | | Closing ISÄR-raden "`designed` ↔ ✅ Spec ok → `bedöm`" |
|
|
2308
2932
|
* | assessed utan poängvarv | bedöm steg 3–7: analysen finns, poängen saknas |
|
|
2309
2933
|
* | 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 (⚖️)
|
|
2934
|
+
* | finns (delningslage) | prissatta)"; alla klara (delKlar) → sync föräldern (⚖️), |
|
|
2935
|
+
* | | utom när rotens kvitto board har kärnan ⚖️ (#693) |
|
|
2311
2936
|
* | assessed, scorerns DELA_UPP | stopputfall → "ny analysrunda per del … `split` satt" |
|
|
2312
2937
|
* | assessed, scorerns OVER_SKALAN, del | ADR-0144 beslut 3: deluppgift utan Score; split.gid säger |
|
|
2313
2938
|
* | (split.parent satt) | synkad, annars sync och set-split-gid |
|
|
@@ -2317,7 +2942,8 @@ function delKlar(state, d) {
|
|
|
2317
2942
|
* | | godkänn/underkänn från ⚖️ |
|
|
2318
2943
|
* | priced, del (split.parent satt) | ADR-0144 beslut 3 och 6: deluppgift med Score; split.gid |
|
|
2319
2944
|
* | | säger synkad, annars sync och set-split-gid |
|
|
2320
|
-
* | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund
|
|
2945
|
+
* | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund; ADR-0153 beslut 3: |
|
|
2946
|
+
* | | kvittot board med kärnan ⚖️ säger synkad, annars om inte gjort |
|
|
2321
2947
|
* | guard_run | `guard_run` reserveras för guard-kommandot: inget väntar |
|
|
2322
2948
|
* | allt annat | `okänt läge: <status>`, aldrig en gissning |
|
|
2323
2949
|
*
|
|
@@ -2339,8 +2965,13 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2339
2965
|
const vantarDom = sparrSkal(state).length > 0
|
|
2340
2966
|
const scorerStopp =
|
|
2341
2967
|
scorerUtfall !== null && SCORE_STOP.includes(scorerUtfall.outcome) ? scorerUtfall : null
|
|
2968
|
+
// ADR-0155 beslut 3: ett kvitto vars pid lever är en pågående körning; ett
|
|
2969
|
+
// dött är en avbruten (bedom.mjs hann aldrig köra korning-slut).
|
|
2970
|
+
const korningPid = Number.isInteger(state.korning?.pid) ? state.korning.pid : null
|
|
2971
|
+
const korningLever = korningPid !== null && isAlive(korningPid)
|
|
2972
|
+
const avbruten = korningPid !== null && !korningLever ? 'avbruten körning, ' : ''
|
|
2342
2973
|
const star = [
|
|
2343
|
-
status,
|
|
2974
|
+
avbruten + status,
|
|
2344
2975
|
stopp ? `stopp ${stopp.utfall} (${stopp.kalla})` : null,
|
|
2345
2976
|
!stopp && scorerStopp ? `${scorerStopp.outcome} (${scorerStopp.fil})` : null,
|
|
2346
2977
|
olikaOlyft ? 'OLIKA olyft' : vantarDom ? 'omkörning olyft' : null,
|
|
@@ -2369,6 +3000,14 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2369
3000
|
? `synkad som deluppgift ${state.split.gid}${score}, se föräldern ${state.split.parent}${offert}`
|
|
2370
3001
|
: `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
3002
|
}
|
|
3003
|
+
// ADR-0153 beslut 3: kvittot ur archive.mjs synced med kärnan ⚖️ säger att
|
|
3004
|
+
// ⚖️-synken är gjord. Allt annat (null, saknat, en annan status) säger inte det.
|
|
3005
|
+
const kvitto = state.board
|
|
3006
|
+
const synkadVag =
|
|
3007
|
+
kvitto !== null &&
|
|
3008
|
+
typeof kvitto === 'object' &&
|
|
3009
|
+
typeof kvitto.offer_status === 'string' &&
|
|
3010
|
+
alfabetiskKarna(kvitto.offer_status) === alfabetiskKarna('⚖️ Poängsättning redo för granskning')
|
|
2372
3011
|
const tillbakaTillSpec = (val) =>
|
|
2373
3012
|
rad(
|
|
2374
3013
|
`avgör valet: ${val}`,
|
|
@@ -2376,6 +3015,8 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2376
3015
|
)
|
|
2377
3016
|
|
|
2378
3017
|
if (antalDelar > 0) return rad('se delarna', 'se delarna', `bunt (${antalDelar} delar)`)
|
|
3018
|
+
if (korningLever)
|
|
3019
|
+
return rad('processen', 'vänta; bedöm igen när bedom.mjs är klar', `pågår (pid ${korningPid})`)
|
|
2379
3020
|
if (vantarDom) return rad('dom (döm)', `döm ${post}`)
|
|
2380
3021
|
if (stopp) {
|
|
2381
3022
|
const val = stopp.orsak ?? stopp.kalla
|
|
@@ -2383,6 +3024,13 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2383
3024
|
// Slutgranskning I2: en designkonflikt besvaras av en omdesign, inte av
|
|
2384
3025
|
// en respec; add-design nollar stoppet.
|
|
2385
3026
|
if (arDesignkonflikt(stopp)) return rad('omdesign (designa)', `designa ${adress}`)
|
|
3027
|
+
// P4-24: designern bad om en intervju; designa löser stoppet.
|
|
3028
|
+
if (arIntervjuStopp(stopp))
|
|
3029
|
+
return rad(
|
|
3030
|
+
'människa',
|
|
3031
|
+
`designa ${adress} (intervju; se ${stopp.kalla})`,
|
|
3032
|
+
`${avbruten}stopp ${stopp.utfall} ur ${stopp.kalla}`,
|
|
3033
|
+
)
|
|
2386
3034
|
if (stopp.utfall === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(val)
|
|
2387
3035
|
if (stopp.utfall === 'ESKALERA')
|
|
2388
3036
|
return rad(`avgör valet: ${val}`, 'människa dömer, ingen ny specrunda')
|
|
@@ -2410,6 +3058,14 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2410
3058
|
)
|
|
2411
3059
|
: rad('godkännande av spec på brädan', godkannEller)
|
|
2412
3060
|
}
|
|
3061
|
+
// ADR-0156 beslut 3: en granskning som stoppade avslutar kedjan på designed
|
|
3062
|
+
// och går till det interaktiva designa; ett utslag skickas aldrig om.
|
|
3063
|
+
if (status === 'designed' && state.design?.review?.verdict === 'stopp')
|
|
3064
|
+
return rad(
|
|
3065
|
+
'människa',
|
|
3066
|
+
`designa ${adress} (intervju; se design-review.md)`,
|
|
3067
|
+
'designed, granskningen stoppade',
|
|
3068
|
+
)
|
|
2413
3069
|
if (status === 'designed') return rad('körning av bedöm', `bedöm ${adress}`)
|
|
2414
3070
|
if (status === 'assessed') {
|
|
2415
3071
|
if (nScore === 0) return rad('körning av bedöm', `bedöm ${adress} (poängsättning saknas)`)
|
|
@@ -2421,11 +3077,15 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2421
3077
|
// klar (delKlar) flyttar synken den till ⚖️.
|
|
2422
3078
|
if (delning !== null && delning.klara < delning.av)
|
|
2423
3079
|
return rad('se delarna', `se delarna (${delning.klara} av ${delning.av} delar prissatta)`)
|
|
3080
|
+
// #693: synken som flyttar föräldern bokförs med synced på roten;
|
|
3081
|
+
// kvittot läses som i priced-grenen (ADR-0153 beslut 3).
|
|
2424
3082
|
if (delning !== null)
|
|
2425
|
-
return
|
|
2426
|
-
'granskning av poäng på brädan',
|
|
2427
|
-
|
|
2428
|
-
|
|
3083
|
+
return synkadVag
|
|
3084
|
+
? rad('granskning av poäng på brädan', godkannEller)
|
|
3085
|
+
: rad(
|
|
3086
|
+
'granskning av poäng på brädan',
|
|
3087
|
+
`sync föräldern till brädan (request-sync ⚖️), sedan ${godkannEller}`,
|
|
3088
|
+
)
|
|
2429
3089
|
return rad('ny analysrunda per del', 'dela posten (split), sedan bedöm per del')
|
|
2430
3090
|
}
|
|
2431
3091
|
if (o === 'OVER_SKALAN')
|
|
@@ -2450,10 +3110,14 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2450
3110
|
state.split.gid ? 'granskning av föräldern på brädan' : 'sync av deluppgiften',
|
|
2451
3111
|
deluppgift(false),
|
|
2452
3112
|
)
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
3113
|
+
// ADR-0153 beslut 3: synkadVag (ovan) läser kvittot; utan det "om inte gjort".
|
|
3114
|
+
const godkannKlar = `godkänn ${gid} (📤 Klar – till kund) eller underkänn ${gid} <skäl>`
|
|
3115
|
+
return synkadVag
|
|
3116
|
+
? rad('granskning av poäng på brädan', godkannKlar)
|
|
3117
|
+
: rad(
|
|
3118
|
+
'granskning av poäng på brädan',
|
|
3119
|
+
`sync till brädan (request-sync ⚖️) om inte gjort, sedan ${godkannKlar}`,
|
|
3120
|
+
)
|
|
2457
3121
|
}
|
|
2458
3122
|
if (status === 'guard_run') return rad('inget', 'inget')
|
|
2459
3123
|
return rad('–', '–', `okänt läge: ${status}`)
|
|
@@ -2589,6 +3253,22 @@ function gallandeDokumentFil(state, typ, n) {
|
|
|
2589
3253
|
return liftingUtslag(state, typ, n)?.galler_fil ?? `${typ}-pricing-v${n}.json`
|
|
2590
3254
|
}
|
|
2591
3255
|
|
|
3256
|
+
/** Täckningsposten (ADR-0159, ändrar ADR-0119 beslut 4): `coverage_post` ur
|
|
3257
|
+
* den poängfil som gäller, `gallandeDokumentFil(state, 'score', maxN)`, så
|
|
3258
|
+
* att granskarens `galler_fil` styr på samma sätt som för poäng och utfall.
|
|
3259
|
+
* null när ingen poängrunda finns, när den gällande filen ännu inte skrivits
|
|
3260
|
+
* (den stämplas ofta före agentens fil; nästa omräkning fyller i den) och
|
|
3261
|
+
* när filen saknar ett objekt i fältet. Finns filen men är trasig kastar
|
|
3262
|
+
* readPricingFileOrThrow ArkivFel, samma arkivfel som andra läsare i filen. */
|
|
3263
|
+
function gallandeTackning(state, d = katalog) {
|
|
3264
|
+
const n = maxN(state.runs ?? [], 'score')
|
|
3265
|
+
if (n === 0) return null
|
|
3266
|
+
const fil = gallandeDokumentFil(state, 'score', n)
|
|
3267
|
+
if (!existsSync(path.join(d, fil))) return null
|
|
3268
|
+
const cp = readPricingFileOrThrow(fil, d).coverage_post
|
|
3269
|
+
return cp !== null && typeof cp === 'object' && !Array.isArray(cp) ? cp : null
|
|
3270
|
+
}
|
|
3271
|
+
|
|
2592
3272
|
/** Läser en gällande pricing-fil (classification eller score) ur katalogen
|
|
2593
3273
|
* och returnerar den PARSADE, validerade datan. Arkivet PÅSTÅR via sin
|
|
2594
3274
|
* runs-rad (eller ett viktningsutslags `galler_fil`) att filen finns och är
|