@haus-tech/haus-workflow 5.12.1 → 6.0.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 +55 -0
- package/README.md +7 -4
- package/dist/cli.js +7183 -4310
- 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 +7 -3
- package/library/global/skills/haus-workflow/references/init.md +7 -4
- package/library/global/skills/request/SKILL.md +487 -104
- package/library/global/skills/request/scripts/archive.mjs +697 -48
- 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 +1 -1
|
@@ -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,34 @@ 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, #508): senaste
|
|
898
|
-
// vinner, precis som titeln följer spec.md på disk.
|
|
1001
|
+
// Täckningspost (ADR-0119, #508): senaste HUVUDFILEN som bär fältet
|
|
1002
|
+
// vinner, precis som titeln följer spec.md på disk. -run2 och -avvisad
|
|
1003
|
+
// sätter den aldrig, oavsett stämplingsordning: under LIKA_TOLERANS blir
|
|
1004
|
+
// den ersatta körningen -run2 och stämplas ofta efter huvudfilen, och
|
|
1005
|
+
// dess yta nådde då state, bluf och request-syncs täckningsdeluppgift
|
|
1006
|
+
// (Convendum 1219234151224225, 2026-10-14). Bara poängaxelns PRISfil
|
|
1007
|
+
// (GALLANDE_RE, samma mönster som maxN läser): en guardfil har en egen
|
|
1008
|
+
// N-sekvens och dess utfall synkas aldrig (SKILL.md, arkivträdet), så
|
|
1009
|
+
// den får inte ersätta den prissatta postens täckning. En huvudfil som
|
|
899
1010
|
// (ännu) inte finns är inte add-runs jobb att fälla, den stämplas ofta
|
|
900
1011
|
// innan agenten skrivit filen. Finns den redan är trasig JSON eller ett
|
|
901
1012
|
// icke-objekt ett arkivfel som andra anropare i den här filen, fångas
|
|
902
1013
|
// inte här: readPricingFileOrThrow kastar oskyddad.
|
|
903
|
-
if (
|
|
1014
|
+
if (GALLANDE_RE('score').test(file) && existsSync(path.join(katalog, file))) {
|
|
904
1015
|
const data = readPricingFileOrThrow(file, katalog)
|
|
905
1016
|
if (data.coverage_post !== null && typeof data.coverage_post === 'object')
|
|
906
1017
|
titelFalt = { ...titelFalt, coverage_post: data.coverage_post }
|
|
907
1018
|
}
|
|
1019
|
+
// ADR-0153 beslut 3: brädkvittot betyder att den gällande prisrundan
|
|
1020
|
+
// ligger på brädan. En ny rundas huvudfil gör ett tidigare kvitto
|
|
1021
|
+
// inaktuellt, så nasta-steg säger "om inte gjort" igen. -run2, -avvisad,
|
|
1022
|
+
// spec.md och impact.md rör aldrig kvittot.
|
|
1023
|
+
if (HUVUDFIL_RE.test(file) && state.board !== null && state.board !== undefined)
|
|
1024
|
+
titelFalt = { ...titelFalt, board: null }
|
|
908
1025
|
const nyState = { ...state, ...titelFalt, runs: [...runs, row] }
|
|
909
1026
|
writeStateOrThrow(nyState)
|
|
910
1027
|
rowCount = nyState.runs.length
|
|
1028
|
+
if (HUVUDFIL_RE.test(file)) delStateForRot = state
|
|
911
1029
|
} catch (e) {
|
|
912
1030
|
releaseLock(lockPath)
|
|
913
1031
|
if (e instanceof ArkivFel) {
|
|
@@ -917,7 +1035,8 @@ if (cmd === 'add-run') {
|
|
|
917
1035
|
throw e
|
|
918
1036
|
}
|
|
919
1037
|
releaseLock(lockPath)
|
|
920
|
-
|
|
1038
|
+
const rotSuffix = nollaRotensKvitto(delStateForRot)
|
|
1039
|
+
console.log(`OK: runs-rad för ${file} (${rowCount} rader)${rotSuffix}`)
|
|
921
1040
|
} else if (cmd === 'next-n') {
|
|
922
1041
|
// `flags('typ')` bara SAMLAR IN varje värde som direkt följer ett `--typ`
|
|
923
1042
|
// — den validerar aldrig att RESTEN av `rest` är just sådana par. Ett
|
|
@@ -1253,19 +1372,96 @@ if (cmd === 'add-run') {
|
|
|
1253
1372
|
// katalog är körningskatalogen (k<N>) eller en buntdel (DELA_UPP inuti en
|
|
1254
1373
|
// del ger del-N/del-M). Skelettet skrivs av kommandot, aldrig av den inre
|
|
1255
1374
|
// processen: samma skäl som ny-korning (ADR-0107 beslut 4).
|
|
1375
|
+
//
|
|
1376
|
+
// #599: under en produktionspost (state.json finns och saknar
|
|
1377
|
+
// experiment: true) skriver `--typ split` en produktionsdel i stället:
|
|
1378
|
+
// ingen experimentmarkör (set-status priced vägrar den annars), split.parent
|
|
1379
|
+
// är förälderns gid (katalognamnet när gid saknas), förälderns approval
|
|
1380
|
+
// ärvs (delen ärver den godkända specen, ADR-0144), och split valideras som
|
|
1381
|
+
// heltal vid skrivning, så ett handskrivet "of": "2" aldrig fäller
|
|
1382
|
+
// delningslage för hela trädet efteråt. --request-id är då valfri och måste
|
|
1383
|
+
// vara förälderns id.
|
|
1256
1384
|
avvisaOkandaFlaggor('ny-del', ['typ', 'part', 'of', 'request-id'])
|
|
1257
1385
|
const typ = requireFlag('typ')
|
|
1258
|
-
const
|
|
1259
|
-
const
|
|
1260
|
-
const
|
|
1386
|
+
const partRa = requireFlag('part')
|
|
1387
|
+
const ofRa = requireFlag('of')
|
|
1388
|
+
const heltal = (v) => (/^[1-9]\d{0,2}$/.test(v) ? Number(v) : NaN)
|
|
1389
|
+
const part = heltal(partRa)
|
|
1390
|
+
const of = heltal(ofRa)
|
|
1261
1391
|
if (!['bundle', 'split'].includes(typ)) {
|
|
1262
1392
|
console.error(`--typ måste vara bundle eller split (fick "${typ}")`)
|
|
1263
1393
|
process.exit(2)
|
|
1264
1394
|
}
|
|
1265
|
-
if (!Number.isInteger(part) || !Number.isInteger(of) || part
|
|
1266
|
-
console.error(
|
|
1395
|
+
if (!Number.isInteger(part) || !Number.isInteger(of) || part > of) {
|
|
1396
|
+
console.error(
|
|
1397
|
+
`--part och --of måste vara heltal 1-999 med part <= of (fick "${partRa}"/"${ofRa}")`,
|
|
1398
|
+
)
|
|
1267
1399
|
process.exit(2)
|
|
1268
1400
|
}
|
|
1401
|
+
let foralder = null
|
|
1402
|
+
if (existsSync(path.join(katalog, 'state.json'))) {
|
|
1403
|
+
// Förälderns lås hålls från läsningen till delens state.json är skriven:
|
|
1404
|
+
// två samtidiga ny-del med olika --of kan annars båda passera
|
|
1405
|
+
// syskonkontrollen. Varje utgång nedan är process.exit, så låset släpps
|
|
1406
|
+
// i exit-hooken.
|
|
1407
|
+
const lockPath = acquireLock(katalog)
|
|
1408
|
+
process.on('exit', () => releaseLock(lockPath))
|
|
1409
|
+
try {
|
|
1410
|
+
foralder = readStateOrThrow(katalog)
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1413
|
+
console.error(e.message)
|
|
1414
|
+
process.exit(2)
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const produktion = foralder !== null && foralder.experiment !== true
|
|
1418
|
+
let requestId
|
|
1419
|
+
let arvtGodkannande = null
|
|
1420
|
+
if (produktion) {
|
|
1421
|
+
const vagra = (msg) => {
|
|
1422
|
+
console.error(`ny-del vägrar: ${msg}`)
|
|
1423
|
+
process.exit(2)
|
|
1424
|
+
}
|
|
1425
|
+
if (typ !== 'split')
|
|
1426
|
+
vagra('--typ bundle under en produktionspost (produktionsbuntar skrivs av speca)')
|
|
1427
|
+
if (foralder.split?.parent != null)
|
|
1428
|
+
vagra('föräldern är själv en delningsdel (en del delas aldrig vidare, den blir OVER_SKALAN)')
|
|
1429
|
+
if (foralder.approval === null || foralder.approval === undefined)
|
|
1430
|
+
vagra('föräldern saknar godkännande (delning följer bedöm, som kräver ett godkännande)')
|
|
1431
|
+
// Delningen följer scorerns DELA_UPP (bedöm-routningen, ADR-0144): samma
|
|
1432
|
+
// avläsning som nasta-steg gör, aldrig en handdelning av en annan post.
|
|
1433
|
+
let utfall
|
|
1434
|
+
try {
|
|
1435
|
+
utfall = gallandeScorerUtfall(foralder, katalog)?.outcome ?? null
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1438
|
+
vagra(e.message)
|
|
1439
|
+
}
|
|
1440
|
+
if (foralder.status !== 'assessed' || utfall !== 'DELA_UPP')
|
|
1441
|
+
vagra(
|
|
1442
|
+
`föräldern står inte på scorerns DELA_UPP (status ${foralder.status}, utfall ${utfall ?? 'inget'})`,
|
|
1443
|
+
)
|
|
1444
|
+
if (of < 2) vagra(`en delning har minst två delar (fick --of ${of})`)
|
|
1445
|
+
const foralderId = foralder.request_id ?? path.basename(path.resolve(katalog))
|
|
1446
|
+
const givet = flag('request-id')
|
|
1447
|
+
if (givet !== null && givet !== foralderId)
|
|
1448
|
+
vagra(`--request-id ${givet} är inte förälderns id ${foralderId}`)
|
|
1449
|
+
for (const syskon of delKataloger(katalog).filter((d) => path.dirname(d) === katalog)) {
|
|
1450
|
+
let st
|
|
1451
|
+
try {
|
|
1452
|
+
st = readStateOrThrow(syskon)
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1455
|
+
vagra(e.message)
|
|
1456
|
+
}
|
|
1457
|
+
if (st.split?.parent != null && st.split.of !== of)
|
|
1458
|
+
vagra(`${path.basename(syskon)} har split.of ${st.split.of}, inte ${of}`)
|
|
1459
|
+
}
|
|
1460
|
+
requestId = foralderId
|
|
1461
|
+
arvtGodkannande = foralder.approval
|
|
1462
|
+
} else {
|
|
1463
|
+
requestId = requireFlag('request-id')
|
|
1464
|
+
}
|
|
1269
1465
|
const p = path.join(katalog, `del-${part}`)
|
|
1270
1466
|
try {
|
|
1271
1467
|
mkdirSync(p)
|
|
@@ -1274,12 +1470,17 @@ if (cmd === 'add-run') {
|
|
|
1274
1470
|
process.exit(2)
|
|
1275
1471
|
}
|
|
1276
1472
|
const ref = { part, of }
|
|
1277
|
-
const st =
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1473
|
+
const st = produktion
|
|
1474
|
+
? {
|
|
1475
|
+
...skelett(null, { split: { parent: requestId, ...ref }, experiment: false }),
|
|
1476
|
+
approval: arvtGodkannande,
|
|
1477
|
+
}
|
|
1478
|
+
: skelett(
|
|
1479
|
+
requestId,
|
|
1480
|
+
typ === 'bundle'
|
|
1481
|
+
? { bundle: { parent_slug: requestId, ...ref } }
|
|
1482
|
+
: { split: { parent: requestId, ...ref } },
|
|
1483
|
+
)
|
|
1283
1484
|
try {
|
|
1284
1485
|
writeStateOrThrow(st, p)
|
|
1285
1486
|
} catch (e) {
|
|
@@ -1555,15 +1756,16 @@ if (cmd === 'add-run') {
|
|
|
1555
1756
|
avvisaOkandaFlaggor('stopp', ['utfall', 'kalla', 'orsak'])
|
|
1556
1757
|
const utfall = requireFlag('utfall')
|
|
1557
1758
|
const kalla = requireFlag('kalla')
|
|
1558
|
-
|
|
1759
|
+
let orsak = flag('orsak')
|
|
1559
1760
|
if (!STOPP_UTFALL.includes(utfall)) {
|
|
1560
1761
|
console.error(`okänt --utfall: ${utfall} (giltiga: ${STOPP_UTFALL.join(', ')})`)
|
|
1561
1762
|
process.exit(2)
|
|
1562
1763
|
}
|
|
1563
1764
|
const arDesignKvitto = DESIGN_AVVISAD_RE.test(kalla)
|
|
1564
|
-
|
|
1765
|
+
const arIntervjuKvitto = DESIGN_INTERVJU_RE.test(kalla)
|
|
1766
|
+
if (!STOPP_KALLOR.includes(kalla) && !arDesignKvitto && !arIntervjuKvitto) {
|
|
1565
1767
|
console.error(
|
|
1566
|
-
`okänd --kalla: ${kalla} (giltiga: ${STOPP_KALLOR.join(', ')}, design-avvisad-<datum>.md)`,
|
|
1768
|
+
`okänd --kalla: ${kalla} (giltiga: ${STOPP_KALLOR.join(', ')}, design-avvisad-<datum>.md, design-intervju-kravs-<datum>.md)`,
|
|
1567
1769
|
)
|
|
1568
1770
|
process.exit(2)
|
|
1569
1771
|
}
|
|
@@ -1611,6 +1813,23 @@ if (cmd === 'add-run') {
|
|
|
1611
1813
|
)
|
|
1612
1814
|
process.exit(2)
|
|
1613
1815
|
}
|
|
1816
|
+
} else if (arIntervjuKvitto) {
|
|
1817
|
+
// P4-24: kvittot är designerns svar, och bara en första rad
|
|
1818
|
+
// `INTERVJU KRÄVS: <skäl>` är ett stopp. Det går till en människa
|
|
1819
|
+
// (designa, intervjun), aldrig tillbaka till specen.
|
|
1820
|
+
const forsta = text.replace(/\r\n/g, '\n').trimStart().split('\n')[0].trim()
|
|
1821
|
+
if (!/^INTERVJU KRÄVS:\s*\S/u.test(forsta)) {
|
|
1822
|
+
console.error(`${kallaPath} börjar inte med "INTERVJU KRÄVS: <skäl>", kan inte stoppas`)
|
|
1823
|
+
process.exit(2)
|
|
1824
|
+
}
|
|
1825
|
+
if (utfall !== 'ESKALERA') {
|
|
1826
|
+
console.error(`ett intervjukrav i ${kalla} stoppar som ESKALERA, inte ${utfall}`)
|
|
1827
|
+
process.exit(2)
|
|
1828
|
+
}
|
|
1829
|
+
if (!orsak) {
|
|
1830
|
+
console.error('ESKALERA kräver --orsak: en människa ska se varför designern inte skrev något')
|
|
1831
|
+
process.exit(2)
|
|
1832
|
+
}
|
|
1614
1833
|
} else if (kalla === 'granskarblock.md') {
|
|
1615
1834
|
// ADR-0118: bara ett ja på raden Prisavgörande öppet val är ett stopp.
|
|
1616
1835
|
if (prisavgorandeVal(text) === null) {
|
|
@@ -1645,6 +1864,26 @@ if (cmd === 'add-run') {
|
|
|
1645
1864
|
console.error(`en motsägelse i ${kalla} stoppar som TILLBAKA_TILL_SPEC, inte ${utfall}`)
|
|
1646
1865
|
process.exit(2)
|
|
1647
1866
|
}
|
|
1867
|
+
// #595: designkonflikten (texten efter etiketten börjar med `design
|
|
1868
|
+
// conflict:`) avgör routningen till designa (arDesignkonflikt), så den
|
|
1869
|
+
// läses ur filen, aldrig bara ur flaggan. Utan --orsak blir filens text
|
|
1870
|
+
// orsaken; en --orsak som motsäger filen (en annan konflikttext, en
|
|
1871
|
+
// specorsak mot en designkonflikt, eller en designkonflikt filen inte
|
|
1872
|
+
// bär) vägras. En vanlig motsägelse behåller anroparens fria orsak.
|
|
1873
|
+
const filText = avvikelseText(text)
|
|
1874
|
+
const filKonflikt = filText !== null && /^design conflict:/i.test(filText) ? filText : null
|
|
1875
|
+
const givenKonflikt = orsak !== null && /^design conflict:/i.test(orsak.trim())
|
|
1876
|
+
const motsager =
|
|
1877
|
+
orsak !== null && (filKonflikt !== null ? orsak.trim() !== filKonflikt : givenKonflikt)
|
|
1878
|
+
if (motsager) {
|
|
1879
|
+
console.error(
|
|
1880
|
+
`--orsak motsäger impact.md: filen säger ${
|
|
1881
|
+
filKonflikt === null ? 'ingen designkonflikt' : `"${filKonflikt}"`
|
|
1882
|
+
}, flaggan "${orsak}"`,
|
|
1883
|
+
)
|
|
1884
|
+
process.exit(2)
|
|
1885
|
+
}
|
|
1886
|
+
if (filKonflikt !== null) orsak = filKonflikt
|
|
1648
1887
|
}
|
|
1649
1888
|
|
|
1650
1889
|
const lockPath = acquireLock()
|
|
@@ -1703,18 +1942,28 @@ if (cmd === 'add-run') {
|
|
|
1703
1942
|
)
|
|
1704
1943
|
} else if (cmd === 'respec') {
|
|
1705
1944
|
// Speca steg 5 som kommando: en ny spec över befintlig katalog invaliderar
|
|
1706
|
-
// analysen, designen, godkännandet, domarna
|
|
1945
|
+
// analysen, designen, godkännandet, domarna, stoppet och brädkvittot
|
|
1946
|
+
// (ADR-0153 beslut 3: kvittot gällde en prisrunda som inte längre gäller). runs[] rörs
|
|
1707
1947
|
// aldrig, arkivet är append-only; den gamla analysen och designen döps
|
|
1708
1948
|
// om, raderas inte.
|
|
1709
1949
|
avvisaOkandaFlaggor('respec', [])
|
|
1710
1950
|
const lockPath = acquireLock()
|
|
1711
1951
|
let antal = 0
|
|
1952
|
+
let delState = null
|
|
1712
1953
|
try {
|
|
1713
1954
|
const state = readStateOrThrow()
|
|
1955
|
+
// ADR-0155 beslut 3: en levande produktionskörning skriver mot samma
|
|
1956
|
+
// katalog; vägra innan något läggs undan. Ett dött pid rensas nedan.
|
|
1957
|
+
if (state.korning && isAlive(state.korning.pid))
|
|
1958
|
+
throw new ArkivFel(`respec vägrar: pågår (pid ${state.korning.pid})`)
|
|
1959
|
+
delState = state
|
|
1714
1960
|
if (laggUndan(katalog, 'impact')) antal++
|
|
1715
1961
|
// Designa (spec 2026-09-24): en ny spec gör den gamla designen inaktuell
|
|
1716
1962
|
// av samma skäl som den gör analysen inaktuell. Läggs undan, raderas inte.
|
|
1717
1963
|
if (laggUndan(katalog, 'design')) antal++
|
|
1964
|
+
// ADR-0156 beslut 3: granskningen gällde den gamla designen och följer
|
|
1965
|
+
// den, undanlagd; state.design.review nollas med state.design nedan.
|
|
1966
|
+
if (laggUndan(katalog, 'design-review')) antal++
|
|
1718
1967
|
const nytt = { ...state }
|
|
1719
1968
|
if (nytt.status !== 'spec_draft') {
|
|
1720
1969
|
nytt.status = 'spec_draft'
|
|
@@ -1736,6 +1985,16 @@ if (cmd === 'add-run') {
|
|
|
1736
1985
|
nytt.design = null
|
|
1737
1986
|
antal++
|
|
1738
1987
|
}
|
|
1988
|
+
if (nytt.board !== null && nytt.board !== undefined) {
|
|
1989
|
+
nytt.board = null
|
|
1990
|
+
antal++
|
|
1991
|
+
}
|
|
1992
|
+
// Ett dött kvitto (levande vägrades ovan) hör till en körning som inte
|
|
1993
|
+
// finns; en ny spec nollar det.
|
|
1994
|
+
if (nytt.korning !== null && nytt.korning !== undefined) {
|
|
1995
|
+
nytt.korning = null
|
|
1996
|
+
antal++
|
|
1997
|
+
}
|
|
1739
1998
|
// Monoton räknare (#505): varje respec-anrop är ett nytt varv oavsett om
|
|
1740
1999
|
// övriga fält redan var normaliserade, till skillnad från fälten ovan
|
|
1741
2000
|
// finns inget "redan rätt värde" att jämföra mot, så det här villkoret
|
|
@@ -1752,7 +2011,7 @@ if (cmd === 'add-run') {
|
|
|
1752
2011
|
throw e
|
|
1753
2012
|
}
|
|
1754
2013
|
releaseLock(lockPath)
|
|
1755
|
-
console.log(`OK: respec (${antal} ändringar)`)
|
|
2014
|
+
console.log(`OK: respec (${antal} ändringar${nollaRotensKvitto(delState)})`)
|
|
1756
2015
|
} else if (cmd === 'add-design') {
|
|
1757
2016
|
// Designa steg 5 (spec 2026-09-24): designdokumentet verifieras, kopieras
|
|
1758
2017
|
// byteidentiskt in som design.md, bokförs i state.design och sätter status
|
|
@@ -1775,8 +2034,113 @@ if (cmd === 'add-run') {
|
|
|
1775
2034
|
throw e
|
|
1776
2035
|
}
|
|
1777
2036
|
releaseLock(lockPath)
|
|
2037
|
+
// ADR-0156 beslut 4: intervjudokumentets saknade utkastsektioner och en
|
|
2038
|
+
// symbolkontroll som inte kunde köras vägrar inte; de syns på stderr.
|
|
2039
|
+
for (const v of kvitto.varningar) console.error(`VARNING: ${v}`)
|
|
1778
2040
|
console.log(`OK: design ${kvitto.design.path} (${kvitto.design.sha256})`)
|
|
1779
2041
|
if (kvitto.impactUndanlagd) console.log('OK: impact.md undanlagd (designkonfliktstoppet löst)')
|
|
2042
|
+
} else if (cmd === 'add-design-review') {
|
|
2043
|
+
// ADR-0156 beslut 3: designgranskarens svar kopieras byteidentiskt in som
|
|
2044
|
+
// design-review.md och bokförs i state.design.review. Ingen runs-rad:
|
|
2045
|
+
// granskningen är intern som designen, kostnaden står i korning.json. En
|
|
2046
|
+
// tidigare granskning läggs undan som design-review-superseded-<datum>.md.
|
|
2047
|
+
// --model lagras som det ges (korning.mjs skickar MODELL:-svansraden).
|
|
2048
|
+
avvisaOkandaFlaggor('add-design-review', ['file', 'verdict', 'model'])
|
|
2049
|
+
const fil = requireFlag('file')
|
|
2050
|
+
const verdict = requireFlag('verdict')
|
|
2051
|
+
const model = requireFlag('model')
|
|
2052
|
+
if (verdict !== 'fortsätt' && verdict !== 'stopp') {
|
|
2053
|
+
console.error(
|
|
2054
|
+
`add-design-review vägrar: --verdict måste vara fortsätt eller stopp, fick "${verdict}"`,
|
|
2055
|
+
)
|
|
2056
|
+
process.exit(2)
|
|
2057
|
+
}
|
|
2058
|
+
const lockPath = acquireLock()
|
|
2059
|
+
let sha256
|
|
2060
|
+
try {
|
|
2061
|
+
const state = readStateOrThrow()
|
|
2062
|
+
if (state.experiment === true) throw new ArkivFel('add-design-review vägrar: experimentkatalog')
|
|
2063
|
+
if (state.design === null || state.design === undefined)
|
|
2064
|
+
throw new ArkivFel('add-design-review vägrar: ingen design att granska')
|
|
2065
|
+
// Bytes läses före undanläggningen, så även katalogens egen
|
|
2066
|
+
// design-review.md går att peka ut utan att kopian faller på en flyttad fil.
|
|
2067
|
+
let bytes
|
|
2068
|
+
try {
|
|
2069
|
+
bytes = readFileSync(path.resolve(fil))
|
|
2070
|
+
} catch (e) {
|
|
2071
|
+
throw new ArkivFel(`add-design-review vägrar: kan inte läsa ${fil}: ${e.message}`)
|
|
2072
|
+
}
|
|
2073
|
+
laggUndan(katalog, 'design-review')
|
|
2074
|
+
const mal = path.join(katalog, 'design-review.md')
|
|
2075
|
+
try {
|
|
2076
|
+
writeFileSync(mal, bytes)
|
|
2077
|
+
} catch (e) {
|
|
2078
|
+
throw new ArkivFel(`kunde inte skriva ${mal}: ${e.message}`)
|
|
2079
|
+
}
|
|
2080
|
+
sha256 = markerStrippedSha256(mal)
|
|
2081
|
+
writeStateOrThrow({
|
|
2082
|
+
...state,
|
|
2083
|
+
design: { ...state.design, review: { verdict, sha256, date: today(), model } },
|
|
2084
|
+
})
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
releaseLock(lockPath)
|
|
2087
|
+
if (e instanceof ArkivFel) {
|
|
2088
|
+
console.error(e.message)
|
|
2089
|
+
process.exit(2)
|
|
2090
|
+
}
|
|
2091
|
+
throw e
|
|
2092
|
+
}
|
|
2093
|
+
releaseLock(lockPath)
|
|
2094
|
+
console.log(`OK: design-review ${verdict} (${sha256})`)
|
|
2095
|
+
} else if (cmd === 'antaganden') {
|
|
2096
|
+
// ADR-0156 beslut 5: läs-enbart lista inför "synka" över varje antagande
|
|
2097
|
+
// utkastet gjorde själv (fråga, valt, ej valt, skäl), granskningens utslag
|
|
2098
|
+
// med antal fynd och spec conflicts, ur design.md och state.json, aldrig
|
|
2099
|
+
// handskriven. Inget lås, ingen skrivning.
|
|
2100
|
+
avvisaOkandaFlaggor('antaganden', [])
|
|
2101
|
+
const ut = []
|
|
2102
|
+
try {
|
|
2103
|
+
let text
|
|
2104
|
+
try {
|
|
2105
|
+
text = readFileSync(path.join(katalog, 'design.md'), 'utf8')
|
|
2106
|
+
} catch {
|
|
2107
|
+
throw new ArkivFel('antaganden vägrar: ingen design')
|
|
2108
|
+
}
|
|
2109
|
+
const state = readStateOrThrow()
|
|
2110
|
+
ut.push(`Designläge: ${designMode(text)}`)
|
|
2111
|
+
const antaganden = designAssumptions(text)
|
|
2112
|
+
if (antaganden === null) ut.push('Antaganden: avsnitt saknas')
|
|
2113
|
+
else if (!Array.isArray(antaganden)) ut.push(`Antaganden: felformade (${antaganden.fel})`)
|
|
2114
|
+
else if (antaganden.length === 0) ut.push('Antaganden: inga')
|
|
2115
|
+
else {
|
|
2116
|
+
ut.push(`Antaganden (${antaganden.length}):`)
|
|
2117
|
+
antaganden.forEach((a, i) => {
|
|
2118
|
+
ut.push(
|
|
2119
|
+
`${i + 1}. ${a.fraga}`,
|
|
2120
|
+
` vald: ${a.vald}`,
|
|
2121
|
+
` ej valda: ${a.ejValda.length > 0 ? a.ejValda.join('; ') : 'inga'}`,
|
|
2122
|
+
` skäl: ${a.skal}`,
|
|
2123
|
+
)
|
|
2124
|
+
})
|
|
2125
|
+
}
|
|
2126
|
+
const review = state.design?.review ?? null
|
|
2127
|
+
ut.push(
|
|
2128
|
+
review === null
|
|
2129
|
+
? 'Granskning: ingen'
|
|
2130
|
+
: `Granskning: ${review.verdict} (${antalFynd(path.join(katalog, 'design-review.md'))})`,
|
|
2131
|
+
)
|
|
2132
|
+
const konflikter = specConflicts(text)
|
|
2133
|
+
if (konflikter === null) ut.push('Spec conflicts: avsnitt saknas')
|
|
2134
|
+
else if (konflikter.length === 0) ut.push('Spec conflicts: none')
|
|
2135
|
+
else ut.push(`Spec conflicts: ${konflikter.length}`, ...konflikter.map((k) => ` ${k}`))
|
|
2136
|
+
} catch (e) {
|
|
2137
|
+
if (e instanceof ArkivFel) {
|
|
2138
|
+
console.error(e.message)
|
|
2139
|
+
process.exit(2)
|
|
2140
|
+
}
|
|
2141
|
+
throw e
|
|
2142
|
+
}
|
|
2143
|
+
console.log(ut.join('\n'))
|
|
1780
2144
|
} else if (cmd === 'set-split-gid') {
|
|
1781
2145
|
// ADR-0144 beslut 6: delens Asana-deluppgift bokförs i split.gid när
|
|
1782
2146
|
// request-sync har skapat den, så en omsynk uppdaterar deluppgiften i
|
|
@@ -1813,6 +2177,186 @@ if (cmd === 'add-run') {
|
|
|
1813
2177
|
}
|
|
1814
2178
|
releaseLock(lockPath)
|
|
1815
2179
|
console.log(oforandrat ? `OK: split-gid ${gid} oförändrat` : `OK: split-gid ${gid}`)
|
|
2180
|
+
} else if (cmd === 'approve') {
|
|
2181
|
+
// ADR-0153 beslut 2: approval skrivs bara här. Grinden (designa eller
|
|
2182
|
+
// bedöm) läser brädan och skickar Offer status som den lästes; allt annat
|
|
2183
|
+
// än kärnan specok vägras, så en grind som inte lästs kan inte noteras.
|
|
2184
|
+
// En tidigare notering skrivs över: grinden läses om varje gång.
|
|
2185
|
+
avvisaOkandaFlaggor('approve', ['source', 'status', 'by'])
|
|
2186
|
+
const source = requireFlag('source')
|
|
2187
|
+
if (source !== 'asana' && source !== 'manual') {
|
|
2188
|
+
console.error(`--source måste vara asana eller manual, fick "${source}"`)
|
|
2189
|
+
process.exit(2)
|
|
2190
|
+
}
|
|
2191
|
+
// En flagga som hör till den andra källan tappas aldrig tyst: den vägras.
|
|
2192
|
+
if (source === 'asana' && has('by')) {
|
|
2193
|
+
console.error('--by hör inte till --source asana')
|
|
2194
|
+
process.exit(2)
|
|
2195
|
+
}
|
|
2196
|
+
if (source === 'manual' && has('status')) {
|
|
2197
|
+
console.error('--status hör inte till --source manual')
|
|
2198
|
+
process.exit(2)
|
|
2199
|
+
}
|
|
2200
|
+
let approval
|
|
2201
|
+
if (source === 'asana') {
|
|
2202
|
+
const status = requireFlag('status')
|
|
2203
|
+
if (alfabetiskKarna(status) !== 'specok') {
|
|
2204
|
+
console.error(`approve vägrar: Offer status "${status}" är inte ✅ Spec ok`)
|
|
2205
|
+
process.exit(2)
|
|
2206
|
+
}
|
|
2207
|
+
approval = { source, status_read: status, date: today() }
|
|
2208
|
+
} else {
|
|
2209
|
+
const by = requireFlag('by')
|
|
2210
|
+
if (by.trim() === '') {
|
|
2211
|
+
console.error('--by får inte vara tom')
|
|
2212
|
+
process.exit(2)
|
|
2213
|
+
}
|
|
2214
|
+
approval = { source, by, date: today() }
|
|
2215
|
+
}
|
|
2216
|
+
const lockPath = acquireLock()
|
|
2217
|
+
try {
|
|
2218
|
+
const state = readStateOrThrow()
|
|
2219
|
+
if (state.experiment === true) throw new ArkivFel('approve vägrar: experimentkatalog')
|
|
2220
|
+
writeStateOrThrow({ ...state, approval })
|
|
2221
|
+
} catch (e) {
|
|
2222
|
+
releaseLock(lockPath)
|
|
2223
|
+
if (e instanceof ArkivFel) {
|
|
2224
|
+
console.error(e.message)
|
|
2225
|
+
process.exit(2)
|
|
2226
|
+
}
|
|
2227
|
+
throw e
|
|
2228
|
+
}
|
|
2229
|
+
releaseLock(lockPath)
|
|
2230
|
+
console.log(source === 'asana' ? 'OK: approval asana' : `OK: approval manual by ${approval.by}`)
|
|
2231
|
+
} else if (cmd === 'korning-start') {
|
|
2232
|
+
// ADR-0155 beslut 3: bedom.mjs bokför sin process här innan den spawnar
|
|
2233
|
+
// prisningen, så nasta-steg kan säga "pågår" och en andra start vägras.
|
|
2234
|
+
// Ett dött pid övertas med en varning; EPERM räknas som levande (isAlive).
|
|
2235
|
+
avvisaOkandaFlaggor('korning-start', ['pid', 'katalog-korning'])
|
|
2236
|
+
const pidTxt = flag('pid')
|
|
2237
|
+
if (pidTxt === null || !/^\d+$/.test(pidTxt) || Number(pidTxt) <= 0) {
|
|
2238
|
+
console.error('korning-start kräver --pid <heltal>')
|
|
2239
|
+
process.exit(2)
|
|
2240
|
+
}
|
|
2241
|
+
const pid = Number(pidTxt)
|
|
2242
|
+
const katalogKorning = flag('katalog-korning')
|
|
2243
|
+
const lockPath = acquireLock()
|
|
2244
|
+
let varning = null
|
|
2245
|
+
try {
|
|
2246
|
+
const state = readStateOrThrow()
|
|
2247
|
+
if (state.experiment === true) throw new ArkivFel('korning-start vägrar: experimentkatalog')
|
|
2248
|
+
if (state.korning) {
|
|
2249
|
+
if (isAlive(state.korning.pid))
|
|
2250
|
+
throw new ArkivFel(`korning-start vägrar: pågår (pid ${state.korning.pid})`)
|
|
2251
|
+
varning = `VARNING: tog över död körning (pid ${state.korning.pid})`
|
|
2252
|
+
}
|
|
2253
|
+
// `started` är en full ISO-tidsstämpel, den enda i state.json (övriga
|
|
2254
|
+
// datum är today()): en körning behöver klockslag för att kunna läsas.
|
|
2255
|
+
writeStateOrThrow({
|
|
2256
|
+
...state,
|
|
2257
|
+
korning: { pid, started: new Date().toISOString(), katalog: katalogKorning },
|
|
2258
|
+
})
|
|
2259
|
+
} catch (e) {
|
|
2260
|
+
releaseLock(lockPath)
|
|
2261
|
+
if (e instanceof ArkivFel) {
|
|
2262
|
+
console.error(e.message)
|
|
2263
|
+
process.exit(2)
|
|
2264
|
+
}
|
|
2265
|
+
throw e
|
|
2266
|
+
}
|
|
2267
|
+
releaseLock(lockPath)
|
|
2268
|
+
if (varning) console.error(varning)
|
|
2269
|
+
console.log(`OK: korning-start pid ${pid}`)
|
|
2270
|
+
} else if (cmd === 'korning-slut') {
|
|
2271
|
+
// ADR-0155 beslut 3: kvittot rensas när bedom.mjs är klar. Utan kvitto är
|
|
2272
|
+
// det ett no-op, aldrig ett fel: slutet ska kunna köras i en finally.
|
|
2273
|
+
avvisaOkandaFlaggor('korning-slut', [])
|
|
2274
|
+
const lockPath = acquireLock()
|
|
2275
|
+
try {
|
|
2276
|
+
const state = readStateOrThrow()
|
|
2277
|
+
if (state.experiment === true) throw new ArkivFel('korning-slut vägrar: experimentkatalog')
|
|
2278
|
+
if (state.korning !== null) writeStateOrThrow({ ...state, korning: null })
|
|
2279
|
+
} catch (e) {
|
|
2280
|
+
releaseLock(lockPath)
|
|
2281
|
+
if (e instanceof ArkivFel) {
|
|
2282
|
+
console.error(e.message)
|
|
2283
|
+
process.exit(2)
|
|
2284
|
+
}
|
|
2285
|
+
throw e
|
|
2286
|
+
}
|
|
2287
|
+
releaseLock(lockPath)
|
|
2288
|
+
console.log('OK: korning-slut')
|
|
2289
|
+
} else if (cmd === 'rot') {
|
|
2290
|
+
// ADR-0154: arkivets enda hem, löst från katalogen (default cwd). `katalog`
|
|
2291
|
+
// är här cwd att lösa från, inte en post; en flagga i första läget är ingen
|
|
2292
|
+
// katalog. Läs-enbart, ingen lås.
|
|
2293
|
+
const flaggaForst = katalog !== undefined && katalog.startsWith('-')
|
|
2294
|
+
const tokens = flaggaForst ? [katalog, ...rest] : rest
|
|
2295
|
+
if (tokens.length > 0) {
|
|
2296
|
+
console.error(
|
|
2297
|
+
`rot: okänd eller ej konsumerad flagga/argument (${tokens.join(' ')}), kommandot tar inga flaggor`,
|
|
2298
|
+
)
|
|
2299
|
+
process.exit(2)
|
|
2300
|
+
}
|
|
2301
|
+
const fran = path.resolve(katalog ?? process.cwd())
|
|
2302
|
+
const r = arkivRot(fran)
|
|
2303
|
+
if (!r) {
|
|
2304
|
+
console.error(`rot vägrar: ingen git-katalog (${fran})`)
|
|
2305
|
+
process.exit(2)
|
|
2306
|
+
}
|
|
2307
|
+
if (r.arkivRot === null) {
|
|
2308
|
+
console.error(
|
|
2309
|
+
`rot vägrar: git-common-dir heter inte .git (${r.commonDir}); kör från superprojektets egen utcheckning`,
|
|
2310
|
+
)
|
|
2311
|
+
process.exit(2)
|
|
2312
|
+
}
|
|
2313
|
+
console.log(`ARKIVROT: ${r.arkivRot}`)
|
|
2314
|
+
console.log(`IGNORERAD: ${r.ignorerad ? 'ja' : 'nej'}`)
|
|
2315
|
+
// Ett herrelöst arkiv i cwd:ns egen utcheckning (en worktree med eget
|
|
2316
|
+
// requests/, Convendums läge mellan släppet och flytten) får en VARNING på
|
|
2317
|
+
// stdout som precheck.mjs, exit fortfarande 0.
|
|
2318
|
+
const topp = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
2319
|
+
cwd: fran,
|
|
2320
|
+
encoding: 'utf8',
|
|
2321
|
+
timeout: 5000,
|
|
2322
|
+
})
|
|
2323
|
+
const toppDir = topp.status === 0 ? String(topp.stdout ?? '').trim() : ''
|
|
2324
|
+
if (toppDir) {
|
|
2325
|
+
const strat = path.join(path.resolve(toppDir), '.haus-workflow', 'requests')
|
|
2326
|
+
if (strat !== path.resolve(r.arkivRot) && existsSync(strat))
|
|
2327
|
+
console.log(`VARNING: arkiv i worktree: ${strat} (flytta per ADR-0154)`)
|
|
2328
|
+
}
|
|
2329
|
+
} else if (cmd === 'synced') {
|
|
2330
|
+
// ADR-0153 beslut 3: brädskrivningen i speca steg 7 och bedöm steg 8 följs
|
|
2331
|
+
// av det här kvittot i state.board, Offer status ordagrant som request-sync
|
|
2332
|
+
// satte den. Varje sync är ett nytt kvitto och skriver över det förra.
|
|
2333
|
+
// Arkivet är fortfarande en cache: Closing-radens brädläge läses ur Asana.
|
|
2334
|
+
avvisaOkandaFlaggor('synced', ['offer-status'])
|
|
2335
|
+
const offerStatus = requireFlag('offer-status')
|
|
2336
|
+
if (offerStatus.trim() === '') {
|
|
2337
|
+
console.error('--offer-status får inte vara tom')
|
|
2338
|
+
process.exit(2)
|
|
2339
|
+
}
|
|
2340
|
+
const board = { offer_status: offerStatus, date: today() }
|
|
2341
|
+
const lockPath = acquireLock()
|
|
2342
|
+
try {
|
|
2343
|
+
const state = readStateOrThrow()
|
|
2344
|
+
if (state.experiment === true) throw new ArkivFel('synced vägrar: experimentkatalog')
|
|
2345
|
+
if (state.split?.parent != null)
|
|
2346
|
+
throw new ArkivFel(
|
|
2347
|
+
'synced vägrar: en delningsdel har ingen egen Offer status (split.gid är kvittot)',
|
|
2348
|
+
)
|
|
2349
|
+
writeStateOrThrow({ ...state, board })
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
releaseLock(lockPath)
|
|
2352
|
+
if (e instanceof ArkivFel) {
|
|
2353
|
+
console.error(e.message)
|
|
2354
|
+
process.exit(2)
|
|
2355
|
+
}
|
|
2356
|
+
throw e
|
|
2357
|
+
}
|
|
2358
|
+
releaseLock(lockPath)
|
|
2359
|
+
console.log(`OK: synced ${offerStatus}`)
|
|
1816
2360
|
} else if (cmd === 'paraply') {
|
|
1817
2361
|
// ADR-0144 beslut 5: förälderns spec skrivs om som en paraplyspec efter ett
|
|
1818
2362
|
// ja. Den gamla spec.md läggs undan (spec-superseded-<datum>.md),
|
|
@@ -1898,7 +2442,9 @@ if (cmd === 'add-run') {
|
|
|
1898
2442
|
? 'prisavgörande val (granskarblock.md)'
|
|
1899
2443
|
: DESIGN_AVVISAD_RE.test(state.stopp.kalla ?? '')
|
|
1900
2444
|
? `designkonflikt (${state.stopp.kalla})`
|
|
1901
|
-
:
|
|
2445
|
+
: arIntervjuStopp(state.stopp)
|
|
2446
|
+
? `intervju krävs (${state.stopp.kalla})`
|
|
2447
|
+
: 'motsägelse (impact.md)'
|
|
1902
2448
|
}
|
|
1903
2449
|
// ADR-0144 beslut 6: delningsroten visar hur många delar som är klara,
|
|
1904
2450
|
// samma räkning som nasta-steg (delningslage), före täckningsposten.
|
|
@@ -1957,7 +2503,7 @@ if (cmd === 'add-run') {
|
|
|
1957
2503
|
console.log(rader.join('\n'))
|
|
1958
2504
|
} else {
|
|
1959
2505
|
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)',
|
|
2506
|
+
'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
2507
|
)
|
|
1962
2508
|
process.exit(2)
|
|
1963
2509
|
}
|
|
@@ -1985,6 +2531,10 @@ function tolkaSatt(par) {
|
|
|
1985
2531
|
}
|
|
1986
2532
|
return [nyckel, ra === 'true']
|
|
1987
2533
|
}
|
|
2534
|
+
if (nyckel === 'design_mode' && ra !== 'draft' && ra !== 'intervju') {
|
|
2535
|
+
console.error(`experiment: design_mode tar bara draft eller intervju, fick "${ra}"`)
|
|
2536
|
+
process.exit(2)
|
|
2537
|
+
}
|
|
1988
2538
|
return [nyckel, ra]
|
|
1989
2539
|
}
|
|
1990
2540
|
|
|
@@ -2034,7 +2584,9 @@ function delKataloger(dir) {
|
|
|
2034
2584
|
|
|
2035
2585
|
/** Lägger undan <bas>.md som <bas>-superseded-<datum>.md, numrerad svans vid
|
|
2036
2586
|
* kollision (-2, -3 …), aldrig en överskrivning: arkivet raderar inget.
|
|
2037
|
-
* Delad av respec (impact.md och design.md)
|
|
2587
|
+
* Delad av respec (impact.md, design.md och design-review.md), add-design
|
|
2588
|
+
* (design.md och design-review.md), add-design-review (design-review.md),
|
|
2589
|
+
* paraply (spec.md) och add-design:s designkonfliktstopp (impact.md).
|
|
2038
2590
|
* Returnerar true när en fil flyttades. */
|
|
2039
2591
|
function laggUndan(d, bas) {
|
|
2040
2592
|
const fil = path.join(d, `${bas}.md`)
|
|
@@ -2050,6 +2602,28 @@ function laggUndan(d, bas) {
|
|
|
2050
2602
|
return true
|
|
2051
2603
|
}
|
|
2052
2604
|
|
|
2605
|
+
/** antaganden (ADR-0156 beslut 5): fyndräkningen i design-review.md som text
|
|
2606
|
+
* för Granskning-raden. Ett fynd är en listpunkt (`- ` eller `* `) under
|
|
2607
|
+
* `## Findings` på den första punktens indrag; en djupare indragen
|
|
2608
|
+
* underpunkt hör till fyndet ovanför och räknas inte. Den enda punkten
|
|
2609
|
+
* `none` är 0. En saknad fil eller sektion säger det, aldrig en gissad
|
|
2610
|
+
* nolla. */
|
|
2611
|
+
function antalFynd(fil) {
|
|
2612
|
+
let text
|
|
2613
|
+
try {
|
|
2614
|
+
text = readFileSync(fil, 'utf8')
|
|
2615
|
+
} catch {
|
|
2616
|
+
return 'fynd okända, design-review.md saknas'
|
|
2617
|
+
}
|
|
2618
|
+
const rader = sectionLines(text.replace(/\r\n/g, '\n'), 'Findings')
|
|
2619
|
+
if (rader === null) return 'fynd okända, ## Findings saknas'
|
|
2620
|
+
const listpunkter = rader.map((r) => /^([ \t]*)[-*] (.*)$/.exec(r)).filter((m) => m !== null)
|
|
2621
|
+
const indrag = listpunkter[0]?.[1] ?? ''
|
|
2622
|
+
const punkter = listpunkter.filter((m) => m[1] === indrag).map((m) => m[2].trim())
|
|
2623
|
+
const antal = punkter.length === 1 && punkter[0].toLowerCase() === 'none' ? 0 : punkter.length
|
|
2624
|
+
return `${antal} fynd`
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2053
2627
|
/** Repo-roten för en arkivkatalog: föräldern till närmaste `.haus-workflow`
|
|
2054
2628
|
* uppåt i sökvägen (arkivet bor i <rot>/.haus-workflow/requests/). null när
|
|
2055
2629
|
* katalogen inte ligger under någon; add-design vägrar då i stället för att
|
|
@@ -2065,14 +2639,17 @@ function repoRotFor(d) {
|
|
|
2065
2639
|
}
|
|
2066
2640
|
|
|
2067
2641
|
/** add-design: vägransreglerna i ordning (rot, fil, inneslutning, arkivet,
|
|
2068
|
-
* frontmatter, spec-hash, konflikter, status, stopp
|
|
2642
|
+
* frontmatter, spec-hash, konflikter, status, stopp, utkastsektioner,
|
|
2643
|
+
* symboler), sedan undanläggning,
|
|
2069
2644
|
* kopia och state. Inneslutningen prövas på realpath (ADR-0082: en länk
|
|
2070
2645
|
* matchas på målet), och sökvägen som bokförs är realpath så precheck
|
|
2071
2646
|
* --design hittar samma fil oavsett länkar. Ett designkonfliktstopp ur
|
|
2072
2647
|
* impact.md nollas (en omdesign besvarar det, slutgranskning I2) och
|
|
2073
2648
|
* impact.md läggs undan med det; varje annat stopp vägras. Kastar
|
|
2074
|
-
* ArkivFel, skriver inget före sista steget. Returnerar designkvittot
|
|
2075
|
-
*
|
|
2649
|
+
* ArkivFel, skriver inget före sista steget. Returnerar designkvittot, om
|
|
2650
|
+
* en analys lades undan och varningarna (ADR-0156 beslut 4: utkastsektioner
|
|
2651
|
+
* på ett intervjudokument, en symbolkontroll som inte kunde köras), som
|
|
2652
|
+
* anroparen skriver på stderr. */
|
|
2076
2653
|
function laggTillDesign(state, fil) {
|
|
2077
2654
|
const rot = repoRotFor(katalog)
|
|
2078
2655
|
if (rot === null)
|
|
@@ -2131,17 +2708,44 @@ function laggTillDesign(state, fil) {
|
|
|
2131
2708
|
`${statePath()} har status ${state.status}, en prissatt post designas inte om utan respec`,
|
|
2132
2709
|
)
|
|
2133
2710
|
const stopp = state.stopp ?? null
|
|
2134
|
-
|
|
2711
|
+
// P4-24: ett intervjukrav (design-intervju-kravs-<datum>.md) besvaras av
|
|
2712
|
+
// designa, så designen nollar det som designkonfliktstoppet.
|
|
2713
|
+
if (stopp !== null && !arDesignkonflikt(stopp) && !arIntervjuStopp(stopp))
|
|
2135
2714
|
throw new ArkivFel(
|
|
2136
2715
|
`${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
2716
|
)
|
|
2717
|
+
// ADR-0156 beslut 4, efter de billiga statusvägrarna så en vägrad post
|
|
2718
|
+
// aldrig startar git: ett utkast (mode: draft) bär Assumptions, Removed or
|
|
2719
|
+
// renamed och Files touched, samma rader som precheck --design (på ett
|
|
2720
|
+
// intervjudokument varningar). Varje borttagen eller omdöpt symbol greppas
|
|
2721
|
+
// i repot där dokumentet bor, utanför filerna designen rör: en träff
|
|
2722
|
+
// vägrar, en kontroll som inte kunde köras är en varning.
|
|
2723
|
+
const designRot = repoRotForFil(real)
|
|
2724
|
+
const form = designForm(text, { repoRot: designRot })
|
|
2725
|
+
if (form.brister.length > 0) throw new ArkivFel(form.brister.join('\n'))
|
|
2726
|
+
const { traffar, varning } = designSymbolkontroll(text, designRot)
|
|
2727
|
+
if (traffar.length > 0)
|
|
2728
|
+
throw new ArkivFel(
|
|
2729
|
+
traffar
|
|
2730
|
+
.map(
|
|
2731
|
+
(t) =>
|
|
2732
|
+
`add-design vägrar: symbolen \`${t.symbol}\` används fortfarande: ${t.fil}:${t.rad}`,
|
|
2733
|
+
)
|
|
2734
|
+
.join('\n'),
|
|
2735
|
+
)
|
|
2736
|
+
const varningar = [...form.varningar, ...(varning ? [varning] : [])]
|
|
2138
2737
|
laggUndan(katalog, 'design')
|
|
2738
|
+
// Granskningen gällde den ersatta designen och följer den, undanlagd som i
|
|
2739
|
+
// respec; state.design.review följer aldrig med (nytt objekt nedan).
|
|
2740
|
+
laggUndan(katalog, 'design-review')
|
|
2139
2741
|
const mal = path.join(katalog, 'design.md')
|
|
2140
2742
|
try {
|
|
2141
2743
|
copyFileSync(real, mal)
|
|
2142
2744
|
} catch (e) {
|
|
2143
2745
|
throw new ArkivFel(`kunde inte kopiera ${real} till ${mal}: ${e.message}`)
|
|
2144
2746
|
}
|
|
2747
|
+
// Ett nytt objekt: en ny designversion har ingen granskning än, så en
|
|
2748
|
+
// tidigare state.design.review följer aldrig med (ADR-0156 beslut 3).
|
|
2145
2749
|
const design = {
|
|
2146
2750
|
path: real,
|
|
2147
2751
|
file: 'design.md',
|
|
@@ -2155,14 +2759,14 @@ function laggTillDesign(state, fil) {
|
|
|
2155
2759
|
// en ny analys är skyldig oavsett omdesignens bytes, och en byteidentisk
|
|
2156
2760
|
// omdesign hade annars lämnat motsägelseanalysen proveniensgiltig. runs[]
|
|
2157
2761
|
// rörs inte, som i respec.
|
|
2158
|
-
const impactUndanlagd = stopp
|
|
2762
|
+
const impactUndanlagd = arDesignkonflikt(stopp) ? laggUndan(katalog, 'impact') : false
|
|
2159
2763
|
writeStateOrThrow({
|
|
2160
2764
|
...state,
|
|
2161
2765
|
design,
|
|
2162
2766
|
status: 'designed',
|
|
2163
2767
|
...(stopp !== null ? { stopp: null } : {}),
|
|
2164
2768
|
})
|
|
2165
|
-
return { design, impactUndanlagd }
|
|
2769
|
+
return { design, impactUndanlagd, varningar }
|
|
2166
2770
|
}
|
|
2167
2771
|
|
|
2168
2772
|
/** paraply (ADR-0144 beslut 5): vägransreglerna i ordning (delningsdel,
|
|
@@ -2280,6 +2884,9 @@ function delKlar(state, d) {
|
|
|
2280
2884
|
* | ------------------------------------- | ------------------------------------------------------------- |
|
|
2281
2885
|
* | bunt-rot (ingen spec.md, del-N finns) | The experiment area: `experiments/`, buntregeln; |
|
|
2282
2886
|
* | | bluf:s `bunt (N delar)` |
|
|
2887
|
+
* | korning.pid lever | ADR-0155 beslut 3: bedom.mjs bokför sin process med |
|
|
2888
|
+
* | | korning-start; raden säger pågår, ett dött pid prefixar |
|
|
2889
|
+
* | | `står` med `avbruten körning, ` och raden räknas som vanligt |
|
|
2283
2890
|
* | spärr står (sparrSkal) | bedöm steg 6 OLIKA "Hänvisa sedan till `döm <post>`"; `döm` |
|
|
2284
2891
|
* | stopp KONCEPT/DIAGNOS | speca steg 6 "Kandidatens katalog kan fortfarande läggas på |
|
|
2285
2892
|
* | | brädan … men `bedöm` hoppar över den"; stopputfall → kunddialog |
|
|
@@ -2287,6 +2894,8 @@ function delKlar(state, d) {
|
|
|
2287
2894
|
* | med orsak `design conflict: …` | routes it to `designa <adress>`"; add-design nollar stoppet |
|
|
2288
2895
|
* | | och lägger undan impact.md, ett kvitto ur designa steg 5 |
|
|
2289
2896
|
* | | ersätter det (då gäller raden nedan) |
|
|
2897
|
+
* | stopp ESKALERA ur | designa Draft mode (P4-24): designern svarade INTERVJU |
|
|
2898
|
+
* | design-intervju-kravs-<datum>.md | KRÄVS; designa (intervjun) löser det, add-design nollar det |
|
|
2290
2899
|
* | stopp TILLBAKA_TILL_SPEC | speca steg 6 (ADR-0118) "När valet avgjorts skrivs svaret |
|
|
2291
2900
|
* | | under `## Avgjorda val` …, sedan `respec` och ny dispatch"; |
|
|
2292
2901
|
* | | designa steg 5, ett kvitto design-avvisad-<datum>.md |
|
|
@@ -2307,7 +2916,8 @@ function delKlar(state, d) {
|
|
|
2307
2916
|
* | | Closing ISÄR-raden "`designed` ↔ ✅ Spec ok → `bedöm`" |
|
|
2308
2917
|
* | assessed utan poängvarv | bedöm steg 3–7: analysen finns, poängen saknas |
|
|
2309
2918
|
* | 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 (⚖️)
|
|
2919
|
+
* | finns (delningslage) | prissatta)"; alla klara (delKlar) → sync föräldern (⚖️), |
|
|
2920
|
+
* | | utom när rotens kvitto board har kärnan ⚖️ (#693) |
|
|
2311
2921
|
* | assessed, scorerns DELA_UPP | stopputfall → "ny analysrunda per del … `split` satt" |
|
|
2312
2922
|
* | assessed, scorerns OVER_SKALAN, del | ADR-0144 beslut 3: deluppgift utan Score; split.gid säger |
|
|
2313
2923
|
* | (split.parent satt) | synkad, annars sync och set-split-gid |
|
|
@@ -2317,7 +2927,8 @@ function delKlar(state, d) {
|
|
|
2317
2927
|
* | | godkänn/underkänn från ⚖️ |
|
|
2318
2928
|
* | priced, del (split.parent satt) | ADR-0144 beslut 3 och 6: deluppgift med Score; split.gid |
|
|
2319
2929
|
* | | säger synkad, annars sync och set-split-gid |
|
|
2320
|
-
* | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund
|
|
2930
|
+
* | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund; ADR-0153 beslut 3: |
|
|
2931
|
+
* | | kvittot board med kärnan ⚖️ säger synkad, annars om inte gjort |
|
|
2321
2932
|
* | guard_run | `guard_run` reserveras för guard-kommandot: inget väntar |
|
|
2322
2933
|
* | allt annat | `okänt läge: <status>`, aldrig en gissning |
|
|
2323
2934
|
*
|
|
@@ -2339,8 +2950,13 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2339
2950
|
const vantarDom = sparrSkal(state).length > 0
|
|
2340
2951
|
const scorerStopp =
|
|
2341
2952
|
scorerUtfall !== null && SCORE_STOP.includes(scorerUtfall.outcome) ? scorerUtfall : null
|
|
2953
|
+
// ADR-0155 beslut 3: ett kvitto vars pid lever är en pågående körning; ett
|
|
2954
|
+
// dött är en avbruten (bedom.mjs hann aldrig köra korning-slut).
|
|
2955
|
+
const korningPid = Number.isInteger(state.korning?.pid) ? state.korning.pid : null
|
|
2956
|
+
const korningLever = korningPid !== null && isAlive(korningPid)
|
|
2957
|
+
const avbruten = korningPid !== null && !korningLever ? 'avbruten körning, ' : ''
|
|
2342
2958
|
const star = [
|
|
2343
|
-
status,
|
|
2959
|
+
avbruten + status,
|
|
2344
2960
|
stopp ? `stopp ${stopp.utfall} (${stopp.kalla})` : null,
|
|
2345
2961
|
!stopp && scorerStopp ? `${scorerStopp.outcome} (${scorerStopp.fil})` : null,
|
|
2346
2962
|
olikaOlyft ? 'OLIKA olyft' : vantarDom ? 'omkörning olyft' : null,
|
|
@@ -2369,6 +2985,14 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2369
2985
|
? `synkad som deluppgift ${state.split.gid}${score}, se föräldern ${state.split.parent}${offert}`
|
|
2370
2986
|
: `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
2987
|
}
|
|
2988
|
+
// ADR-0153 beslut 3: kvittot ur archive.mjs synced med kärnan ⚖️ säger att
|
|
2989
|
+
// ⚖️-synken är gjord. Allt annat (null, saknat, en annan status) säger inte det.
|
|
2990
|
+
const kvitto = state.board
|
|
2991
|
+
const synkadVag =
|
|
2992
|
+
kvitto !== null &&
|
|
2993
|
+
typeof kvitto === 'object' &&
|
|
2994
|
+
typeof kvitto.offer_status === 'string' &&
|
|
2995
|
+
alfabetiskKarna(kvitto.offer_status) === alfabetiskKarna('⚖️ Poängsättning redo för granskning')
|
|
2372
2996
|
const tillbakaTillSpec = (val) =>
|
|
2373
2997
|
rad(
|
|
2374
2998
|
`avgör valet: ${val}`,
|
|
@@ -2376,6 +3000,8 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2376
3000
|
)
|
|
2377
3001
|
|
|
2378
3002
|
if (antalDelar > 0) return rad('se delarna', 'se delarna', `bunt (${antalDelar} delar)`)
|
|
3003
|
+
if (korningLever)
|
|
3004
|
+
return rad('processen', 'vänta; bedöm igen när bedom.mjs är klar', `pågår (pid ${korningPid})`)
|
|
2379
3005
|
if (vantarDom) return rad('dom (döm)', `döm ${post}`)
|
|
2380
3006
|
if (stopp) {
|
|
2381
3007
|
const val = stopp.orsak ?? stopp.kalla
|
|
@@ -2383,6 +3009,13 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2383
3009
|
// Slutgranskning I2: en designkonflikt besvaras av en omdesign, inte av
|
|
2384
3010
|
// en respec; add-design nollar stoppet.
|
|
2385
3011
|
if (arDesignkonflikt(stopp)) return rad('omdesign (designa)', `designa ${adress}`)
|
|
3012
|
+
// P4-24: designern bad om en intervju; designa löser stoppet.
|
|
3013
|
+
if (arIntervjuStopp(stopp))
|
|
3014
|
+
return rad(
|
|
3015
|
+
'människa',
|
|
3016
|
+
`designa ${adress} (intervju; se ${stopp.kalla})`,
|
|
3017
|
+
`${avbruten}stopp ${stopp.utfall} ur ${stopp.kalla}`,
|
|
3018
|
+
)
|
|
2386
3019
|
if (stopp.utfall === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(val)
|
|
2387
3020
|
if (stopp.utfall === 'ESKALERA')
|
|
2388
3021
|
return rad(`avgör valet: ${val}`, 'människa dömer, ingen ny specrunda')
|
|
@@ -2410,6 +3043,14 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2410
3043
|
)
|
|
2411
3044
|
: rad('godkännande av spec på brädan', godkannEller)
|
|
2412
3045
|
}
|
|
3046
|
+
// ADR-0156 beslut 3: en granskning som stoppade avslutar kedjan på designed
|
|
3047
|
+
// och går till det interaktiva designa; ett utslag skickas aldrig om.
|
|
3048
|
+
if (status === 'designed' && state.design?.review?.verdict === 'stopp')
|
|
3049
|
+
return rad(
|
|
3050
|
+
'människa',
|
|
3051
|
+
`designa ${adress} (intervju; se design-review.md)`,
|
|
3052
|
+
'designed, granskningen stoppade',
|
|
3053
|
+
)
|
|
2413
3054
|
if (status === 'designed') return rad('körning av bedöm', `bedöm ${adress}`)
|
|
2414
3055
|
if (status === 'assessed') {
|
|
2415
3056
|
if (nScore === 0) return rad('körning av bedöm', `bedöm ${adress} (poängsättning saknas)`)
|
|
@@ -2421,11 +3062,15 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2421
3062
|
// klar (delKlar) flyttar synken den till ⚖️.
|
|
2422
3063
|
if (delning !== null && delning.klara < delning.av)
|
|
2423
3064
|
return rad('se delarna', `se delarna (${delning.klara} av ${delning.av} delar prissatta)`)
|
|
3065
|
+
// #693: synken som flyttar föräldern bokförs med synced på roten;
|
|
3066
|
+
// kvittot läses som i priced-grenen (ADR-0153 beslut 3).
|
|
2424
3067
|
if (delning !== null)
|
|
2425
|
-
return
|
|
2426
|
-
'granskning av poäng på brädan',
|
|
2427
|
-
|
|
2428
|
-
|
|
3068
|
+
return synkadVag
|
|
3069
|
+
? rad('granskning av poäng på brädan', godkannEller)
|
|
3070
|
+
: rad(
|
|
3071
|
+
'granskning av poäng på brädan',
|
|
3072
|
+
`sync föräldern till brädan (request-sync ⚖️), sedan ${godkannEller}`,
|
|
3073
|
+
)
|
|
2429
3074
|
return rad('ny analysrunda per del', 'dela posten (split), sedan bedöm per del')
|
|
2430
3075
|
}
|
|
2431
3076
|
if (o === 'OVER_SKALAN')
|
|
@@ -2450,10 +3095,14 @@ function nastaStegFor(state, { post, antalDelar, scorerUtfall, delning = null })
|
|
|
2450
3095
|
state.split.gid ? 'granskning av föräldern på brädan' : 'sync av deluppgiften',
|
|
2451
3096
|
deluppgift(false),
|
|
2452
3097
|
)
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
3098
|
+
// ADR-0153 beslut 3: synkadVag (ovan) läser kvittot; utan det "om inte gjort".
|
|
3099
|
+
const godkannKlar = `godkänn ${gid} (📤 Klar – till kund) eller underkänn ${gid} <skäl>`
|
|
3100
|
+
return synkadVag
|
|
3101
|
+
? rad('granskning av poäng på brädan', godkannKlar)
|
|
3102
|
+
: rad(
|
|
3103
|
+
'granskning av poäng på brädan',
|
|
3104
|
+
`sync till brädan (request-sync ⚖️) om inte gjort, sedan ${godkannKlar}`,
|
|
3105
|
+
)
|
|
2457
3106
|
}
|
|
2458
3107
|
if (status === 'guard_run') return rad('inget', 'inget')
|
|
2459
3108
|
return rad('–', '–', `okänt läge: ${status}`)
|