@haus-tech/haus-workflow 5.0.1 → 5.2.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 +19 -0
- package/library/catalog/manifest.json +67 -61
- package/library/global/agents/request-assessor.md +22 -6
- package/library/global/agents/request-scorer.md +3 -1
- package/library/global/agents/request-specifier-eskalerad.md +8 -0
- package/library/global/agents/request-specifier.md +8 -3
- package/library/global/skills/request/SKILL.md +700 -221
- package/library/global/skills/request/scripts/archive.mjs +920 -59
- package/library/global/skills/request/scripts/compare-scores.mjs +119 -13
- package/library/global/skills/request/scripts/contract-check.mjs +302 -40
- package/library/global/skills/request/scripts/kontroll.mjs +640 -0
- package/library/global/skills/request/scripts/korning.mjs +1635 -0
- package/library/global/skills/request/scripts/lib.mjs +541 -5
- package/library/global/skills/request/scripts/precheck.mjs +148 -5
- package/library/global/skills/request/scripts/report.mjs +122 -16
- package/library/global/skills/request/scripts/svep.mjs +737 -0
- package/library/global/skills/request-assess-impact/SKILL.md +58 -4
- package/library/global/skills/request-score/SKILL.md +57 -11
- package/library/global/skills/request-score/references/poangskala.md +3 -0
- package/library/global/skills/request-sync/SKILL.md +11 -12
- package/library/global/skills/request-write-spec/SKILL.md +13 -4
- package/library/global/skills/request-write-spec/references/specformat.md +48 -0
- package/package.json +1 -1
|
@@ -8,17 +8,32 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Del 1: add-run, next-n, sparr. Del 2 (Task 7): utslag (granskardomar),
|
|
10
10
|
* set-status (statusövergångar, med spärr på priced) och kalibrering
|
|
11
|
-
* (läs-enbart svepvy över kalibreringsnotiser).
|
|
11
|
+
* (läs-enbart svepvy över kalibreringsnotiser). Del 3 (plan requestmotor-14001,
|
|
12
|
+
* Task 6): stopp (stopputfall utan scorer, ur spec.md eller impact.md), respec
|
|
13
|
+
* (nollställning inför en ny spec) och bluf (tabellvy över en katalog och dess
|
|
14
|
+
* delar), samt effort ur agentdefinitionen i add-run och ett state.json-skelett
|
|
15
|
+
* från ny-korning. Del 4 (plan utvardera-parallellt-svep, Task 1): experiment,
|
|
16
|
+
* den enda skrivaren av experiment.json på experimentytan, låst och atomisk,
|
|
17
|
+
* så samtidiga svep-processer aldrig tappar varandras rader (#493). Del 5
|
|
18
|
+
* (#530): nasta-steg, läs-enbart tabell över var posten står, vad en människa
|
|
19
|
+
* väntas göra och vilket /request-kommando som kör vidare, ur SKILL.md:s
|
|
20
|
+
* egna grindar (regeltabellen står ovanför `nastaStegFor`).
|
|
12
21
|
*
|
|
13
22
|
* Användning:
|
|
14
23
|
* node archive.mjs add-run <katalog> --file F --skill S --agent A --model M [flaggor]
|
|
15
24
|
* node archive.mjs next-n <katalog> --typ classification [--typ score]
|
|
16
|
-
* node archive.mjs ny-korning <modellkatalog>
|
|
25
|
+
* node archive.mjs ny-korning <modellkatalog> [--request-id G]
|
|
26
|
+
* node archive.mjs ny-del <körningskatalog> --typ bundle|split --part N --of M --request-id G
|
|
17
27
|
* node archive.mjs proveniens <körningskatalog> --modell M --konto K --underlag F [--datum D]
|
|
18
28
|
* node archive.mjs sparr <katalog>
|
|
19
29
|
* node archive.mjs utslag <katalog> --typ viktning|kalibrering|sakandring --beslutad-av N [...]
|
|
20
30
|
* node archive.mjs set-status <katalog> <status>
|
|
31
|
+
* node archive.mjs stopp <katalog> --utfall KONCEPT|DIAGNOS|TILLBAKA_TILL_SPEC|ESKALERA --kalla spec.md|impact.md|granskarblock.md [--orsak <text>]
|
|
32
|
+
* node archive.mjs respec <katalog>
|
|
33
|
+
* node archive.mjs bluf <katalog>
|
|
34
|
+
* node archive.mjs nasta-steg <katalog>
|
|
21
35
|
* node archive.mjs kalibrering <arkivrot>
|
|
36
|
+
* node archive.mjs experiment <experimentkatalog> [--satt K=V]... [--variant M]... [--korning M/kN --status S [--orsak T] [--pid N]]
|
|
22
37
|
* Exit 0 = ok, 1 = spärrad (sparr/set-status priced), 2 = användnings-/tillståndsfel.
|
|
23
38
|
*/
|
|
24
39
|
import {
|
|
@@ -38,8 +53,13 @@ import path from 'node:path'
|
|
|
38
53
|
import { createHash } from 'node:crypto'
|
|
39
54
|
import {
|
|
40
55
|
markerStrippedSha256,
|
|
56
|
+
stripHausMarker,
|
|
41
57
|
claudeDir,
|
|
42
58
|
canonicalModel,
|
|
59
|
+
kandidatIn,
|
|
60
|
+
avvikelseEtikett,
|
|
61
|
+
prisavgorandeVal,
|
|
62
|
+
findUpward,
|
|
43
63
|
CLASS_UTFALL,
|
|
44
64
|
SCORE_OUTCOMES,
|
|
45
65
|
} from './lib.mjs'
|
|
@@ -48,7 +68,7 @@ const [cmd, katalog, ...rest] = process.argv.slice(2)
|
|
|
48
68
|
|
|
49
69
|
if (!cmd || !katalog) {
|
|
50
70
|
console.error(
|
|
51
|
-
'användning: archive.mjs <add-run|next-n|ny-korning|proveniens|sparr|utslag|set-status|kalibrering> <katalog> [flaggor]',
|
|
71
|
+
'användning: archive.mjs <add-run|next-n|ny-korning|ny-del|proveniens|sparr|utslag|set-status|stopp|respec|bluf|nasta-steg|kalibrering|experiment> <katalog> [flaggor]',
|
|
52
72
|
)
|
|
53
73
|
process.exit(2)
|
|
54
74
|
}
|
|
@@ -81,6 +101,35 @@ const requireFlag = (name) => {
|
|
|
81
101
|
}
|
|
82
102
|
const has = (name) => rest.includes(`--${name}`)
|
|
83
103
|
|
|
104
|
+
/** Fail-closed flaggkontroll, samma princip som add-run och next-n tillämpar
|
|
105
|
+
* inline: varje token i `rest` måste höra till en KÄND flagga, annars är det
|
|
106
|
+
* ett fel (exit 2), aldrig ett tyst struntvärde. Värdeflaggor konsumerar
|
|
107
|
+
* nästa token, booleska flaggor bara sig själva. */
|
|
108
|
+
const avvisaOkandaFlaggor = (kommando, vardeflaggor, boolflaggor = []) => {
|
|
109
|
+
const konsumerat = new Set()
|
|
110
|
+
rest.forEach((v, i) => {
|
|
111
|
+
if (!v.startsWith('--')) return
|
|
112
|
+
const namn = v.slice(2)
|
|
113
|
+
if (vardeflaggor.includes(namn)) {
|
|
114
|
+
konsumerat.add(i)
|
|
115
|
+
konsumerat.add(i + 1)
|
|
116
|
+
} else if (boolflaggor.includes(namn)) {
|
|
117
|
+
konsumerat.add(i)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
const ovantat = rest.filter((_, i) => !konsumerat.has(i))
|
|
121
|
+
if (ovantat.length > 0) {
|
|
122
|
+
const giltiga = [...vardeflaggor, ...boolflaggor].map((n) => `--${n}`)
|
|
123
|
+
console.error(
|
|
124
|
+
`${kommando}: okänd eller ej konsumerad flagga/argument (${ovantat.join(' ')}), ` +
|
|
125
|
+
(giltiga.length === 0
|
|
126
|
+
? 'kommandot tar inga flaggor'
|
|
127
|
+
: `giltiga flaggor: ${giltiga.join(', ')}`),
|
|
128
|
+
)
|
|
129
|
+
process.exit(2)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
84
133
|
/** Sökvägen till en katalogs state.json. Parametern har `katalog` som
|
|
85
134
|
* default så varje befintlig anropare är oförändrad; `proveniens` behöver
|
|
86
135
|
* den för att kunna stämpla delkatalogerna med samma kod som föräldern, i
|
|
@@ -158,6 +207,16 @@ const readStateOrThrow = (d = katalog) => {
|
|
|
158
207
|
}
|
|
159
208
|
assertRowArrayField(parsed, 'runs', d)
|
|
160
209
|
assertRowArrayField(parsed, 'granskarutslag', d)
|
|
210
|
+
// `stopp` är null, saknat (äldre arkiv) eller ett objekt { utfall, kalla,
|
|
211
|
+
// date }. Allt annat är trasigt: pricedBlockers och bluf läser fältet, och
|
|
212
|
+
// en sträng där hade tyst lästs som "inget stopp".
|
|
213
|
+
if (
|
|
214
|
+
parsed.stopp !== undefined &&
|
|
215
|
+
parsed.stopp !== null &&
|
|
216
|
+
(typeof parsed.stopp !== 'object' || Array.isArray(parsed.stopp))
|
|
217
|
+
) {
|
|
218
|
+
throw new ArkivFel(`${statePath(d)} har ett stopp-fält som varken är null eller ett objekt`)
|
|
219
|
+
}
|
|
161
220
|
return parsed
|
|
162
221
|
}
|
|
163
222
|
|
|
@@ -180,31 +239,73 @@ const loadStateOrExit = () => {
|
|
|
180
239
|
* skrivfel (t.ex. EACCES) i stället för att låta undantaget nå toppen som
|
|
181
240
|
* en okontrollerad krasch med exit 1 — i det här kontraktet betyder exit 1
|
|
182
241
|
* spärrad, och ett misslyckat skrivförsök är inte det. */
|
|
183
|
-
const writeStateOrThrow = (s, d = katalog) =>
|
|
242
|
+
const writeStateOrThrow = (s, d = katalog) => writeJsonAtomiskOrThrow(s, d, 'state.json')
|
|
243
|
+
|
|
244
|
+
/** Samma rename-dans för vilken JSON-fil som helst i en katalog: state.json
|
|
245
|
+
* och experiment.json delar en enda skrivväg i stället för två kopior som
|
|
246
|
+
* kan glida isär (filnamnet är den enda skillnaden). */
|
|
247
|
+
const writeJsonAtomiskOrThrow = (s, d, fil) => {
|
|
248
|
+
const mal = path.join(d, fil)
|
|
184
249
|
const rand = Math.random().toString(36).slice(2, 8)
|
|
185
|
-
const tmp = path.join(d,
|
|
250
|
+
const tmp = path.join(d, `.${fil}.tmp-${process.pid}-${rand}`)
|
|
186
251
|
try {
|
|
187
252
|
writeFileSync(tmp, JSON.stringify(s, null, 2) + '\n')
|
|
188
253
|
// rename(2) byter inod, så temp-filens läge (satt av umask vid writeFileSync)
|
|
189
254
|
// skulle annars tyst ersätta en avsiktligt hårdare rättighet på den
|
|
190
|
-
// befintliga
|
|
191
|
-
// läget om filen redan fanns; en ny
|
|
255
|
+
// befintliga filen (t.ex. 0600) med standardläget. Bevara det gamla
|
|
256
|
+
// läget om filen redan fanns; en ny fil har inget läge att bevara.
|
|
192
257
|
try {
|
|
193
|
-
chmodSync(tmp, statSync(
|
|
258
|
+
chmodSync(tmp, statSync(mal).mode)
|
|
194
259
|
} catch {
|
|
195
|
-
// ingen befintlig
|
|
260
|
+
// ingen befintlig fil (ny post), inget läge att bevara.
|
|
196
261
|
}
|
|
197
|
-
renameSync(tmp,
|
|
262
|
+
renameSync(tmp, mal)
|
|
198
263
|
} catch (e) {
|
|
199
264
|
try {
|
|
200
265
|
unlinkSync(tmp)
|
|
201
266
|
} catch {
|
|
202
267
|
// temp-filen skapades aldrig eller är redan borta — inget att städa.
|
|
203
268
|
}
|
|
204
|
-
throw new ArkivFel(`kunde inte skriva ${
|
|
269
|
+
throw new ArkivFel(`kunde inte skriva ${mal}: ${e.message}`)
|
|
205
270
|
}
|
|
206
271
|
}
|
|
207
272
|
|
|
273
|
+
/** Läser experiment.json i katalogen d. Saknad fil är GILTIG (ny post, tom
|
|
274
|
+
* behållare), trasig JSON eller en icke-objekt-fil är ett arkivfel. De två
|
|
275
|
+
* fält kommandot skriver strukturerat valideras på samma sätt som runs i
|
|
276
|
+
* state.json: `varianter` måste vara en array av strängar och `korningar`
|
|
277
|
+
* ett objekt, annars sprids skräp tyst vid nästa skrivning. Alla andra
|
|
278
|
+
* fält (även äldre `baselinemodell`/`kandidatmodell`) lämnas orörda. */
|
|
279
|
+
const readExperimentOrThrow = (d) => {
|
|
280
|
+
const fil = path.join(d, 'experiment.json')
|
|
281
|
+
if (!existsSync(fil)) return { state: {}, fanns: false }
|
|
282
|
+
let parsed
|
|
283
|
+
try {
|
|
284
|
+
parsed = JSON.parse(readFileSync(fil, 'utf8'))
|
|
285
|
+
} catch (e) {
|
|
286
|
+
throw new ArkivFel(`${fil} är trasig JSON: ${e.message}`)
|
|
287
|
+
}
|
|
288
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
289
|
+
const fick = parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed
|
|
290
|
+
throw new ArkivFel(`${fil} är inte ett objekt (fick ${fick})`)
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
parsed.varianter !== undefined &&
|
|
294
|
+
(!Array.isArray(parsed.varianter) || parsed.varianter.some((v) => typeof v !== 'string'))
|
|
295
|
+
) {
|
|
296
|
+
throw new ArkivFel(`${fil} har ett varianter-fält som inte är en array av strängar`)
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
parsed.korningar !== undefined &&
|
|
300
|
+
(parsed.korningar === null ||
|
|
301
|
+
typeof parsed.korningar !== 'object' ||
|
|
302
|
+
Array.isArray(parsed.korningar))
|
|
303
|
+
) {
|
|
304
|
+
throw new ArkivFel(`${fil} har ett korningar-fält som inte är ett objekt`)
|
|
305
|
+
}
|
|
306
|
+
return { state: parsed, fanns: true }
|
|
307
|
+
}
|
|
308
|
+
|
|
208
309
|
/** Synkron sömn (blockerar tråden) för låsets pollning. Standardknepet för
|
|
209
310
|
* att sova utan async i ett rent CLI-script: Atomics.wait somnar tills
|
|
210
311
|
* timeouten går ut eftersom ingen annan agent notifierar den här cellen. */
|
|
@@ -461,6 +562,16 @@ const SCORE_STOP = [
|
|
|
461
562
|
'ESKALERA',
|
|
462
563
|
]
|
|
463
564
|
|
|
565
|
+
/** `stopp`-kommandots utfall och källor. Kandidatmarkören i spec.md
|
|
566
|
+
* (KONCEPT/DIAGNOS, ADR-0105), motsägelse-etiketten i impact.md
|
|
567
|
+
* (TILLBAKA_TILL_SPEC, ADR-0106) och raden Prisavgörande öppet val i
|
|
568
|
+
* granskarblock.md (TILLBAKA_TILL_SPEC eller ESKALERA, ADR-0118) är de tre
|
|
569
|
+
* källor som kan bära ett stopp UTAN scorer. DELA_UPP, OVER_SKALAN och
|
|
570
|
+
* INPUT_SAKNAS är scorerns utfall och når arkivet via prisfilen, aldrig via
|
|
571
|
+
* stopp; ESKALERA når arkivet på båda vägarna. */
|
|
572
|
+
const STOPP_UTFALL = ['KONCEPT', 'DIAGNOS', 'TILLBAKA_TILL_SPEC', 'ESKALERA']
|
|
573
|
+
const STOPP_KALLOR = ['spec.md', 'impact.md', 'granskarblock.md']
|
|
574
|
+
|
|
464
575
|
/** Tak för antal körningar per modell. Skyddet är mot en trasig loop, inte mot
|
|
465
576
|
* Tims arbetssätt: 999 körningar av samma modell på samma post är alltid ett
|
|
466
577
|
* fel, aldrig en avsikt. */
|
|
@@ -488,6 +599,13 @@ const PROVENIENS_REFERENSER = [
|
|
|
488
599
|
* gett en tyst, obegriplig status i stället för ett fel. */
|
|
489
600
|
const STATUSAR = ['spec_draft', 'spec_synced', 'assessed', 'priced', 'guard_run']
|
|
490
601
|
|
|
602
|
+
/** Körningsstatus i experiment.json, exakt de literaler SKILL.md:s
|
|
603
|
+
* återupptagningsregel och svep.mjs läser. */
|
|
604
|
+
const EXPERIMENT_STATUSAR = ['klar', 'ofärdig', 'stopp', 'väntar på dom']
|
|
605
|
+
/** En körningsnyckel är <modell>/k<N>, samma form som katalogen under
|
|
606
|
+
* experimentkatalogen. */
|
|
607
|
+
const KORNING_NYCKEL_RE = /^[^/]+\/k\d+$/
|
|
608
|
+
|
|
491
609
|
/** Giltiga `--double-run`-värden (SKILL.md runs-kontraktet: "bekräftad" |
|
|
492
610
|
* "olika" | "tolerans" | null). `add-run` skrev tidigare VILKEN STRÄNG SOM
|
|
493
611
|
* HELST rakt in i fältet, samma klass av brist som `STATUSAR` ovan stänger
|
|
@@ -507,6 +625,53 @@ const GALLANDE_RE = (typ) => new RegExp(`^${typ}-pricing-v(\\d+)\\.json$`)
|
|
|
507
625
|
const maxN = (runs, typ) =>
|
|
508
626
|
Math.max(0, ...runs.map((r) => Number(GALLANDE_RE(typ).exec(r.file ?? '')?.[1] ?? 0)))
|
|
509
627
|
|
|
628
|
+
/** Första rubrikraden i ett specdokument, utan markdown-tecken; null om ingen. */
|
|
629
|
+
const forstaRubrik = (text) => {
|
|
630
|
+
const rad = String(text)
|
|
631
|
+
.split('\n')
|
|
632
|
+
.map((l) => l.trim())
|
|
633
|
+
.find((l) => /^#{1,6}\s+\S/.test(l) || /^\*\*[^*]+\*\*\s*$/.test(l))
|
|
634
|
+
return rad
|
|
635
|
+
? rad
|
|
636
|
+
.replace(/^#{1,6}\s+/, '')
|
|
637
|
+
.replace(/^\*\*|\*\*$/g, '')
|
|
638
|
+
.trim()
|
|
639
|
+
: null
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Kebab-slug: gemener, åäö till aao, allt annat än [a-z0-9] blir bindestreck. */
|
|
643
|
+
const slugAv = (titel) =>
|
|
644
|
+
titel
|
|
645
|
+
.toLowerCase()
|
|
646
|
+
.replace(/å/g, 'a')
|
|
647
|
+
.replace(/ä/g, 'a')
|
|
648
|
+
.replace(/ö/g, 'o')
|
|
649
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
650
|
+
.replace(/^-+|-+$/g, '')
|
|
651
|
+
|
|
652
|
+
/** Skelettet ny-korning och ny-del skriver, delat på ett ställe: samma form
|
|
653
|
+
* som SKILL.md:s state.json plus `experiment: true` (aldrig priced) och
|
|
654
|
+
* `stopp: null`. `bundle`/`split` överstyrs bara när anroparen ger dem, så
|
|
655
|
+
* ny-korning (ingen del) och ny-del (precis en av dem) delar exakt samma
|
|
656
|
+
* funktion i stället för två skrivvägar som kan glida isär. */
|
|
657
|
+
const skelett = (requestId, { bundle, split } = {}) => ({
|
|
658
|
+
experiment: true,
|
|
659
|
+
request_id: requestId,
|
|
660
|
+
slug: null,
|
|
661
|
+
former_slug: null,
|
|
662
|
+
title: null,
|
|
663
|
+
status: 'spec_draft',
|
|
664
|
+
bundle: bundle ?? { parent_slug: null, part: null, of: null },
|
|
665
|
+
split: split ?? { parent: null, part: null, of: null },
|
|
666
|
+
approval: null,
|
|
667
|
+
granskarutslag: [],
|
|
668
|
+
note: null,
|
|
669
|
+
stopp: null,
|
|
670
|
+
respec_varv: 0,
|
|
671
|
+
coverage_post: null,
|
|
672
|
+
runs: [],
|
|
673
|
+
})
|
|
674
|
+
|
|
510
675
|
if (cmd === 'add-run') {
|
|
511
676
|
// Samma fail-closed-princip som `next-n` redan tillämpar (se dess kommentar
|
|
512
677
|
// ovan): varje token i `rest` måste höra till en KÄND flagga, annars är det
|
|
@@ -534,30 +699,17 @@ if (cmd === 'add-run') {
|
|
|
534
699
|
'pairs-with',
|
|
535
700
|
]
|
|
536
701
|
const ADD_RUN_BOOLFLAGGOR = ['partial-rerun']
|
|
537
|
-
|
|
538
|
-
rest.forEach((v, i) => {
|
|
539
|
-
if (!v.startsWith('--')) return
|
|
540
|
-
const namn = v.slice(2)
|
|
541
|
-
if (ADD_RUN_VARDEFLAGGOR.includes(namn)) {
|
|
542
|
-
konsumerat.add(i)
|
|
543
|
-
konsumerat.add(i + 1)
|
|
544
|
-
} else if (ADD_RUN_BOOLFLAGGOR.includes(namn)) {
|
|
545
|
-
konsumerat.add(i)
|
|
546
|
-
}
|
|
547
|
-
})
|
|
548
|
-
const ovantat = rest.filter((_, i) => !konsumerat.has(i))
|
|
549
|
-
if (ovantat.length > 0) {
|
|
550
|
-
console.error(
|
|
551
|
-
`add-run: okänd eller ej konsumerad flagga/argument (${ovantat.join(' ')}), ` +
|
|
552
|
-
`giltiga flaggor: ${[...ADD_RUN_VARDEFLAGGOR, ...ADD_RUN_BOOLFLAGGOR].map((n) => `--${n}`).join(', ')}`,
|
|
553
|
-
)
|
|
554
|
-
process.exit(2)
|
|
555
|
-
}
|
|
702
|
+
avvisaOkandaFlaggor('add-run', ADD_RUN_VARDEFLAGGOR, ADD_RUN_BOOLFLAGGOR)
|
|
556
703
|
|
|
557
704
|
const file = flag('file')
|
|
558
705
|
const skill = flag('skill')
|
|
559
706
|
const agent = flag('agent')
|
|
560
|
-
|
|
707
|
+
// --model förblir obligatorisk (plan requestmotor-14001, align: modellfallback
|
|
708
|
+
// struken; under utvärdera körs agenterna med override, så definitionens
|
|
709
|
+
// modell är per konstruktion inte den som kördes). Värdet kanoniseras:
|
|
710
|
+
// ett kontextsuffix som `[1m]` är samma modell (ADR-0099).
|
|
711
|
+
const modelRaw = flag('model')
|
|
712
|
+
const model = modelRaw === null ? null : canonicalModel(modelRaw)
|
|
561
713
|
if (!file || !skill || !agent || !model) {
|
|
562
714
|
console.error('add-run kräver --file, --skill, --agent, --model')
|
|
563
715
|
process.exit(2)
|
|
@@ -580,7 +732,10 @@ if (cmd === 'add-run') {
|
|
|
580
732
|
}
|
|
581
733
|
referens[kv.slice(0, eq)] = kv.slice(eq + 1)
|
|
582
734
|
}
|
|
583
|
-
|
|
735
|
+
// Effort ÄR agentdefinitionens (till skillnad från modellen, se ovan): en
|
|
736
|
+
// utelämnad --effort läses ur <claudeDir>/agents/<agent>.md. Explicit flagga
|
|
737
|
+
// vinner alltid.
|
|
738
|
+
const effort = flag('effort') ?? effortFranAgent(agent)
|
|
584
739
|
const note = flag('note')
|
|
585
740
|
const doubleRun = flag('double-run')
|
|
586
741
|
// Samma vitlista som lässidan (`sparrSkal`, `DOUBLE_RUN_VALUES`) delar,
|
|
@@ -651,9 +806,37 @@ if (cmd === 'add-run') {
|
|
|
651
806
|
// hoppa över den, arkivera aldrig en påhittad hash.
|
|
652
807
|
}
|
|
653
808
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
809
|
+
// #497: en ny spec.md/impact.md ersätter den förra läggningen av samma fil.
|
|
810
|
+
// Den föregående raden pekas ut, i stället för att två likadana rader
|
|
811
|
+
// står olänkade och läsaren måste gissa vilken som är på disk. Exakt
|
|
812
|
+
// filnamn med flit, inte SPEC_RE/IMPACT_RE: de mönstren matchar även
|
|
813
|
+
// t.ex. spec-avvisad-v1.md, som aldrig ska plockas in i supersede-kedjan.
|
|
814
|
+
const ersattbar = file === 'spec.md' || file === 'impact.md'
|
|
815
|
+
const runs = (state.runs ?? []).map((r) =>
|
|
816
|
+
ersattbar && r.file === file && (r.superseded_by ?? null) === null
|
|
817
|
+
? { ...r, superseded_by: file }
|
|
818
|
+
: r,
|
|
819
|
+
)
|
|
820
|
+
// #495: titeln följer det dokument som faktiskt ligger på disk.
|
|
821
|
+
let titelFalt = {}
|
|
822
|
+
if (file === 'spec.md' && existsSync(path.join(katalog, 'spec.md'))) {
|
|
823
|
+
const rubrik = forstaRubrik(readFileSync(path.join(katalog, 'spec.md'), 'utf8'))
|
|
824
|
+
if (rubrik) titelFalt = { title: rubrik, slug: slugAv(rubrik) }
|
|
825
|
+
}
|
|
826
|
+
// Täckningspost (ADR-0119, #508): senaste pricing-filen som bär fältet
|
|
827
|
+
// vinner, precis som titeln följer spec.md på disk. En pricing-fil som
|
|
828
|
+
// (ännu) inte finns är inte add-runs jobb att fälla, den stämplas ofta
|
|
829
|
+
// innan agenten skrivit filen. Finns den redan är trasig JSON eller ett
|
|
830
|
+
// icke-objekt ett arkivfel som andra anropare i den här filen, fångas
|
|
831
|
+
// inte här: readPricingFileOrThrow kastar oskyddad.
|
|
832
|
+
if (PRICING_RE.test(file) && existsSync(path.join(katalog, file))) {
|
|
833
|
+
const data = readPricingFileOrThrow(file, katalog)
|
|
834
|
+
if (data.coverage_post !== null && typeof data.coverage_post === 'object')
|
|
835
|
+
titelFalt = { ...titelFalt, coverage_post: data.coverage_post }
|
|
836
|
+
}
|
|
837
|
+
const nyState = { ...state, ...titelFalt, runs: [...runs, row] }
|
|
838
|
+
writeStateOrThrow(nyState)
|
|
839
|
+
rowCount = nyState.runs.length
|
|
657
840
|
} catch (e) {
|
|
658
841
|
releaseLock(lockPath)
|
|
659
842
|
if (e instanceof ArkivFel) {
|
|
@@ -957,6 +1140,8 @@ if (cmd === 'add-run') {
|
|
|
957
1140
|
// katalog är HÄR modellkatalogen (experiments/<slug>/<modell>), inte en
|
|
958
1141
|
// postkatalog. Den skapas om den saknas: första körningen av en modell ska
|
|
959
1142
|
// inte kräva ett förberedande steg.
|
|
1143
|
+
avvisaOkandaFlaggor('ny-korning', ['request-id'])
|
|
1144
|
+
const requestId = flag('request-id')
|
|
960
1145
|
try {
|
|
961
1146
|
mkdirSync(katalog, { recursive: true })
|
|
962
1147
|
} catch (e) {
|
|
@@ -971,16 +1156,196 @@ if (cmd === 'add-run') {
|
|
|
971
1156
|
// två samtidiga processer skulle dela körningskatalog. Det är hela
|
|
972
1157
|
// poängen med det här subkommandot.
|
|
973
1158
|
mkdirSync(p)
|
|
974
|
-
console.log(p)
|
|
975
|
-
process.exit(0)
|
|
976
1159
|
} catch (e) {
|
|
977
1160
|
if (e.code === 'EEXIST') continue
|
|
978
1161
|
console.error(`kunde inte skapa ${p}: ${e.message}`)
|
|
979
1162
|
process.exit(2)
|
|
980
1163
|
}
|
|
1164
|
+
// Skelettet skrivs av kommandot, inte av orkestratorn: IPC 14001 visade
|
|
1165
|
+
// körningar helt utan state.json på körningsnivå, så proveniens hade
|
|
1166
|
+
// ingenting att stämpla.
|
|
1167
|
+
try {
|
|
1168
|
+
writeStateOrThrow(skelett(requestId), p)
|
|
1169
|
+
} catch (e) {
|
|
1170
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1171
|
+
// Katalogen finns redan när skrivningen faller, och sökvägen får INTE
|
|
1172
|
+
// skrivas på stdout: anroparen skulle läsa den som en färdig körning.
|
|
1173
|
+
console.error(`${e.message}; katalogen ${p} är skapad men saknar state.json`)
|
|
1174
|
+
process.exit(2)
|
|
1175
|
+
}
|
|
1176
|
+
console.log(p)
|
|
1177
|
+
process.exit(0)
|
|
981
1178
|
}
|
|
982
1179
|
console.error(`fler än ${MAX_KORNINGAR} körningar i ${katalog}, något är fel`)
|
|
983
1180
|
process.exit(2)
|
|
1181
|
+
} else if (cmd === 'ny-del') {
|
|
1182
|
+
// katalog är körningskatalogen (k<N>) eller en buntdel (DELA_UPP inuti en
|
|
1183
|
+
// del ger del-N/del-M). Skelettet skrivs av kommandot, aldrig av den inre
|
|
1184
|
+
// processen: samma skäl som ny-korning (ADR-0107 beslut 4).
|
|
1185
|
+
avvisaOkandaFlaggor('ny-del', ['typ', 'part', 'of', 'request-id'])
|
|
1186
|
+
const typ = requireFlag('typ')
|
|
1187
|
+
const part = Number(requireFlag('part'))
|
|
1188
|
+
const of = Number(requireFlag('of'))
|
|
1189
|
+
const requestId = requireFlag('request-id')
|
|
1190
|
+
if (!['bundle', 'split'].includes(typ)) {
|
|
1191
|
+
console.error(`--typ måste vara bundle eller split (fick "${typ}")`)
|
|
1192
|
+
process.exit(2)
|
|
1193
|
+
}
|
|
1194
|
+
if (!Number.isInteger(part) || !Number.isInteger(of) || part < 1 || of < 1 || part > of) {
|
|
1195
|
+
console.error(`--part och --of måste vara heltal >= 1 med part <= of (fick ${part}/${of})`)
|
|
1196
|
+
process.exit(2)
|
|
1197
|
+
}
|
|
1198
|
+
const p = path.join(katalog, `del-${part}`)
|
|
1199
|
+
try {
|
|
1200
|
+
mkdirSync(p)
|
|
1201
|
+
} catch (e) {
|
|
1202
|
+
console.error(e.code === 'EEXIST' ? `${p} finns redan` : `kunde inte skapa ${p}: ${e.message}`)
|
|
1203
|
+
process.exit(2)
|
|
1204
|
+
}
|
|
1205
|
+
const ref = { part, of }
|
|
1206
|
+
const st = skelett(
|
|
1207
|
+
requestId,
|
|
1208
|
+
typ === 'bundle'
|
|
1209
|
+
? { bundle: { parent_slug: requestId, ...ref } }
|
|
1210
|
+
: { split: { parent: requestId, ...ref } },
|
|
1211
|
+
)
|
|
1212
|
+
try {
|
|
1213
|
+
writeStateOrThrow(st, p)
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
if (!(e instanceof ArkivFel)) throw e
|
|
1216
|
+
console.error(`${e.message}; katalogen ${p} är skapad men saknar state.json`)
|
|
1217
|
+
process.exit(2)
|
|
1218
|
+
}
|
|
1219
|
+
console.log(p)
|
|
1220
|
+
process.exit(0)
|
|
1221
|
+
} else if (cmd === 'experiment') {
|
|
1222
|
+
// katalog är HÄR experimentkatalogen (experiments/<slug>). experiment.json
|
|
1223
|
+
// är den enda filen på experimentytan som flera samtidiga processer
|
|
1224
|
+
// skriver (#493: sonnet/k4 försvann i en läs-modifiera-skriv utan lås), så
|
|
1225
|
+
// den enda skrivaren är det här kommandot, under samma lås som state.json-
|
|
1226
|
+
// kommandona. acquireLock låser i `katalog`, alltså i experimentkatalogen.
|
|
1227
|
+
avvisaOkandaFlaggor('experiment', ['satt', 'variant', 'korning', 'status', 'orsak', 'pid'])
|
|
1228
|
+
const sattPar = flags('satt').map(tolkaSatt)
|
|
1229
|
+
const nyaVarianter = flags('variant')
|
|
1230
|
+
const korningsflaggor = flags('korning')
|
|
1231
|
+
const status = flag('status')
|
|
1232
|
+
const orsak = flag('orsak')
|
|
1233
|
+
// #516: startraden (`ofärdig`, "startad, inte avslutad") bär startarens pid
|
|
1234
|
+
// medan körningen pågår, så ett andra svep kan skilja en körning som lever
|
|
1235
|
+
// från en som dog. Slutstatusen skrivs utan --pid och ersätter raden helt,
|
|
1236
|
+
// pid:n försvinner alltså av sig själv när processen är klar.
|
|
1237
|
+
const pidText = flag('pid')
|
|
1238
|
+
if (korningsflaggor.length > 1) {
|
|
1239
|
+
console.error('experiment: högst en --korning per anrop')
|
|
1240
|
+
process.exit(2)
|
|
1241
|
+
}
|
|
1242
|
+
const korning = korningsflaggor[0] ?? null
|
|
1243
|
+
if (korning === null && (status !== null || orsak !== null || pidText !== null)) {
|
|
1244
|
+
console.error('experiment: --status, --orsak och --pid kräver --korning')
|
|
1245
|
+
process.exit(2)
|
|
1246
|
+
}
|
|
1247
|
+
if (pidText !== null && !/^\d+$/.test(pidText)) {
|
|
1248
|
+
console.error(`experiment: --pid måste vara ett heltal (fick "${pidText}")`)
|
|
1249
|
+
process.exit(2)
|
|
1250
|
+
}
|
|
1251
|
+
const pid = pidText === null ? null : Number(pidText)
|
|
1252
|
+
if (korning !== null) {
|
|
1253
|
+
if (!KORNING_NYCKEL_RE.test(korning)) {
|
|
1254
|
+
console.error(`experiment: --korning ska ha formen <modell>/k<N>, fick "${korning}"`)
|
|
1255
|
+
process.exit(2)
|
|
1256
|
+
}
|
|
1257
|
+
if (status === null) {
|
|
1258
|
+
console.error('experiment: --korning kräver --status')
|
|
1259
|
+
process.exit(2)
|
|
1260
|
+
}
|
|
1261
|
+
if (!EXPERIMENT_STATUSAR.includes(status)) {
|
|
1262
|
+
console.error(`okänd status: ${status} (giltiga: ${EXPERIMENT_STATUSAR.join(', ')})`)
|
|
1263
|
+
process.exit(2)
|
|
1264
|
+
}
|
|
1265
|
+
// Katalogen måste finnas: en statusrad för en körning som inte finns
|
|
1266
|
+
// är ett stavfel i anroparen, inte en körning.
|
|
1267
|
+
const korningsdir = path.join(katalog, korning)
|
|
1268
|
+
let finns = false
|
|
1269
|
+
try {
|
|
1270
|
+
finns = statSync(korningsdir).isDirectory()
|
|
1271
|
+
} catch {
|
|
1272
|
+
// saknas
|
|
1273
|
+
}
|
|
1274
|
+
if (!finns) {
|
|
1275
|
+
console.error(`experiment: körningskatalogen ${korningsdir} finns inte`)
|
|
1276
|
+
process.exit(2)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// Katalogen skapas om den saknas, precis som ny-korning gör med sin: första
|
|
1280
|
+
// steget i ett svep ska inte kräva ett förberedande mkdir. Låset ligger i
|
|
1281
|
+
// katalogen, så den måste finnas innan acquireLock.
|
|
1282
|
+
try {
|
|
1283
|
+
mkdirSync(katalog, { recursive: true })
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
console.error(`kunde inte skapa experimentkatalogen ${katalog}: ${e.message}`)
|
|
1286
|
+
process.exit(2)
|
|
1287
|
+
}
|
|
1288
|
+
const lockPath = acquireLock()
|
|
1289
|
+
let andringar = 0
|
|
1290
|
+
let fannsFil = false
|
|
1291
|
+
try {
|
|
1292
|
+
const { state, fanns } = readExperimentOrThrow(katalog)
|
|
1293
|
+
fannsFil = fanns
|
|
1294
|
+
let nasta = { ...state }
|
|
1295
|
+
// --satt är append-only som proveniens: ett fält skrivs en gång, samma
|
|
1296
|
+
// värde igen är en no-op, ett annat värde är ett fel. Alla fält prövas
|
|
1297
|
+
// före skrivningen, så ett vägrat anrop lämnar ingenting efter sig.
|
|
1298
|
+
for (const [nyckel, varde] of sattPar) {
|
|
1299
|
+
if (nasta[nyckel] === undefined) {
|
|
1300
|
+
nasta = { ...nasta, [nyckel]: varde }
|
|
1301
|
+
andringar++
|
|
1302
|
+
} else if (nasta[nyckel] !== varde) {
|
|
1303
|
+
throw new ArkivFel(
|
|
1304
|
+
`experiment: fältet ${nyckel} har redan värdet ${JSON.stringify(nasta[nyckel])}, ` +
|
|
1305
|
+
`begärt ${JSON.stringify(varde)}; ett fält skrivs bara en gång`,
|
|
1306
|
+
)
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// UNION med bevarad ordning: befintliga först, invokationens nya efter.
|
|
1310
|
+
// En headless-process med en enmodellslista rör alltså aldrig de andra.
|
|
1311
|
+
const befintliga = nasta.varianter ?? []
|
|
1312
|
+
const tillagda = nyaVarianter.filter(
|
|
1313
|
+
(v, i, alla) => !befintliga.includes(v) && alla.indexOf(v) === i,
|
|
1314
|
+
)
|
|
1315
|
+
if (tillagda.length > 0) {
|
|
1316
|
+
nasta = { ...nasta, varianter: [...befintliga, ...tillagda] }
|
|
1317
|
+
andringar += tillagda.length
|
|
1318
|
+
}
|
|
1319
|
+
// Bara den egna raden skrivs; andra körningars rader kopieras orörda.
|
|
1320
|
+
if (korning !== null) {
|
|
1321
|
+
const rad = {
|
|
1322
|
+
status,
|
|
1323
|
+
...(orsak === null ? {} : { orsak }),
|
|
1324
|
+
...(pid === null ? {} : { pid }),
|
|
1325
|
+
}
|
|
1326
|
+
const befintlig = nasta.korningar?.[korning]
|
|
1327
|
+
if (JSON.stringify(befintlig) !== JSON.stringify(rad)) {
|
|
1328
|
+
nasta = { ...nasta, korningar: { ...(nasta.korningar ?? {}), [korning]: rad } }
|
|
1329
|
+
andringar++
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (andringar > 0 || !fanns) writeJsonAtomiskOrThrow(nasta, katalog, 'experiment.json')
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
releaseLock(lockPath)
|
|
1335
|
+
if (e instanceof ArkivFel) {
|
|
1336
|
+
console.error(e.message)
|
|
1337
|
+
process.exit(2)
|
|
1338
|
+
}
|
|
1339
|
+
throw e
|
|
1340
|
+
}
|
|
1341
|
+
releaseLock(lockPath)
|
|
1342
|
+
// En nyskapad fil är en ändring även utan begärda fält, därför räknas
|
|
1343
|
+
// "oförändrad" bara när filen redan fanns.
|
|
1344
|
+
console.log(
|
|
1345
|
+
andringar === 0 && fannsFil
|
|
1346
|
+
? 'OK: experiment oförändrad'
|
|
1347
|
+
: `OK: experiment (${andringar} ändringar)`,
|
|
1348
|
+
)
|
|
984
1349
|
} else if (cmd === 'proveniens') {
|
|
985
1350
|
// katalog är HÄR körningskatalogen (experiments/<slug>/<modell>/k<N>).
|
|
986
1351
|
// Stämplingen stod tidigare bara som prosa i SKILL.md steg 4 och blev
|
|
@@ -1039,29 +1404,27 @@ if (cmd === 'add-run') {
|
|
|
1039
1404
|
// nyckelmängden ur modelUsage (suffix som `[1m]` strippas) — det är den
|
|
1040
1405
|
// startaren jämför mot varvets begärda modell. Saknas filen skrivs inget
|
|
1041
1406
|
// fält: en påhittad tom lista ser ut som "inga modeller anropades".
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1407
|
+
// Filen ligger på körningsnivå medan stämplingen ibland görs per del-N
|
|
1408
|
+
// (plan requestmotor-14001), så den söks uppåt från katalogen, aldrig
|
|
1409
|
+
// utanför experimentytan och högst tre nivåer.
|
|
1410
|
+
const korningFil = findUpward(katalog, 'korning.json')
|
|
1411
|
+
if (korningFil !== null) {
|
|
1412
|
+
try {
|
|
1413
|
+
const korning = JSON.parse(readFileSync(korningFil, 'utf8'))
|
|
1414
|
+
const nycklar = Object.keys(korning.modelUsage ?? {})
|
|
1415
|
+
if (nycklar.length > 0)
|
|
1416
|
+
stampel.fakturerade_modeller = [...new Set(nycklar.map(canonicalModel))].sort()
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
console.error(`kunde inte läsa ${korningFil} för fakturerade_modeller: ${e.message}`)
|
|
1050
1419
|
process.exit(2)
|
|
1051
1420
|
}
|
|
1052
1421
|
}
|
|
1053
1422
|
|
|
1054
1423
|
// Delarna är samma körning: samma modell, samma datum, samma hashar. De
|
|
1055
1424
|
// stämplas var för sig i sin egen state.json, aldrig genom att rapporten
|
|
1056
|
-
// låter delkolumnen ärva förälderns värden vid läsning.
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
.map((e) => path.join(katalog, e.name))
|
|
1060
|
-
// En delkatalog utan state.json är inte en körning ännu och hoppas över;
|
|
1061
|
-
// att fälla hela stämplingen på den vore att låta ett halvfärdigt DELA_UPP
|
|
1062
|
-
// blockera proveniensen för allt annat.
|
|
1063
|
-
.filter((d) => existsSync(path.join(d, 'state.json')))
|
|
1064
|
-
.sort()
|
|
1425
|
+
// låter delkolumnen ärva förälderns värden vid läsning. Rekursivt: ett
|
|
1426
|
+
// DELA_UPP inuti en buntdel ger del-N/del-M (align F).
|
|
1427
|
+
const delar = delKataloger(katalog)
|
|
1065
1428
|
|
|
1066
1429
|
const lockPath = acquireLock()
|
|
1067
1430
|
try {
|
|
@@ -1076,8 +1439,19 @@ if (cmd === 'add-run') {
|
|
|
1076
1439
|
attSkriva.push([d, state])
|
|
1077
1440
|
continue
|
|
1078
1441
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1442
|
+
// Djup jämförelse per fält: `fakturerade_modeller` är en array, och en
|
|
1443
|
+
// referensjämförelse hade fått varje omanrop att "skilja sig" i just
|
|
1444
|
+
// det fältet.
|
|
1445
|
+
// Unionen av nycklar: ett fält som fanns i den lagrade stämpeln men
|
|
1446
|
+
// saknas i den nya (korning.json hittas inte längre) är också drift.
|
|
1447
|
+
const nycklar = new Set([
|
|
1448
|
+
...Object.keys(stampel),
|
|
1449
|
+
...Object.keys(befintlig === null ? {} : befintlig),
|
|
1450
|
+
])
|
|
1451
|
+
const skiljer = [...nycklar].filter(
|
|
1452
|
+
(k) =>
|
|
1453
|
+
JSON.stringify(befintlig === null ? undefined : befintlig[k]) !==
|
|
1454
|
+
JSON.stringify(stampel[k]),
|
|
1081
1455
|
)
|
|
1082
1456
|
if (skiljer.length > 0) {
|
|
1083
1457
|
throw new ArkivFel(
|
|
@@ -1100,13 +1474,493 @@ if (cmd === 'add-run') {
|
|
|
1100
1474
|
}
|
|
1101
1475
|
throw e
|
|
1102
1476
|
}
|
|
1477
|
+
} else if (cmd === 'stopp') {
|
|
1478
|
+
// Stopputfall UTAN scorer (ADR-0105/0106/0118): en kandidatspec routar direkt
|
|
1479
|
+
// till KONCEPT/DIAGNOS, en analys med motsägelse direkt till
|
|
1480
|
+
// TILLBAKA_TILL_SPEC, och ett prisavgörande öppet val i granskarblock.md
|
|
1481
|
+
// direkt till TILLBAKA_TILL_SPEC eller ESKALERA. Kommandot litar aldrig på
|
|
1482
|
+
// anroparens påstående: källan måste bära det.
|
|
1483
|
+
avvisaOkandaFlaggor('stopp', ['utfall', 'kalla', 'orsak'])
|
|
1484
|
+
const utfall = requireFlag('utfall')
|
|
1485
|
+
const kalla = requireFlag('kalla')
|
|
1486
|
+
const orsak = flag('orsak')
|
|
1487
|
+
if (!STOPP_UTFALL.includes(utfall)) {
|
|
1488
|
+
console.error(`okänt --utfall: ${utfall} (giltiga: ${STOPP_UTFALL.join(', ')})`)
|
|
1489
|
+
process.exit(2)
|
|
1490
|
+
}
|
|
1491
|
+
if (!STOPP_KALLOR.includes(kalla)) {
|
|
1492
|
+
console.error(`okänd --kalla: ${kalla} (giltiga: ${STOPP_KALLOR.join(', ')})`)
|
|
1493
|
+
process.exit(2)
|
|
1494
|
+
}
|
|
1495
|
+
const kallaPath = path.join(katalog, kalla)
|
|
1496
|
+
let text
|
|
1497
|
+
try {
|
|
1498
|
+
text = readFileSync(kallaPath, 'utf8')
|
|
1499
|
+
} catch (e) {
|
|
1500
|
+
console.error(`kunde inte läsa källan ${kallaPath}: ${e.message}`)
|
|
1501
|
+
process.exit(2)
|
|
1502
|
+
}
|
|
1503
|
+
if (kalla === 'spec.md') {
|
|
1504
|
+
const kandidat = kandidatIn(text)
|
|
1505
|
+
if (kandidat === null) {
|
|
1506
|
+
console.error(
|
|
1507
|
+
`${kallaPath} bär ingen kandidatmarkör (hel rubrikrad Koncept-kandidat/Diagnos-kandidat), ` +
|
|
1508
|
+
`kan inte stoppas som ${utfall}`,
|
|
1509
|
+
)
|
|
1510
|
+
process.exit(2)
|
|
1511
|
+
}
|
|
1512
|
+
if (kandidat.utfall !== utfall) {
|
|
1513
|
+
console.error(
|
|
1514
|
+
`${kallaPath} bär markören för ${kandidat.utfall} (rad ${kandidat.line + 1}), inte ${utfall}`,
|
|
1515
|
+
)
|
|
1516
|
+
process.exit(2)
|
|
1517
|
+
}
|
|
1518
|
+
} else if (kalla === 'granskarblock.md') {
|
|
1519
|
+
// ADR-0118: bara ett ja på raden Prisavgörande öppet val är ett stopp.
|
|
1520
|
+
if (prisavgorandeVal(text) === null) {
|
|
1521
|
+
console.error(
|
|
1522
|
+
`${kallaPath} bär inget prisavgörande öppet val (raden "Prisavgörande öppet val: ja: …"), ` +
|
|
1523
|
+
'kan inte stoppas',
|
|
1524
|
+
)
|
|
1525
|
+
process.exit(2)
|
|
1526
|
+
}
|
|
1527
|
+
if (!['TILLBAKA_TILL_SPEC', 'ESKALERA'].includes(utfall)) {
|
|
1528
|
+
console.error(
|
|
1529
|
+
`ett prisavgörande val i ${kalla} stoppar som TILLBAKA_TILL_SPEC eller ESKALERA, inte ${utfall}`,
|
|
1530
|
+
)
|
|
1531
|
+
process.exit(2)
|
|
1532
|
+
}
|
|
1533
|
+
if (utfall === 'ESKALERA' && !orsak) {
|
|
1534
|
+
console.error(
|
|
1535
|
+
'ESKALERA kräver --orsak: en människa ska se varför posten inte gick tillbaka till specen en gång till',
|
|
1536
|
+
)
|
|
1537
|
+
process.exit(2)
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
const etikett = avvikelseEtikett(text)
|
|
1541
|
+
if (etikett !== 'motsägelse') {
|
|
1542
|
+
console.error(
|
|
1543
|
+
`${kallaPath} bär ${etikett === null ? 'ingen avvikelseetikett' : `etiketten "${etikett}"`} ` +
|
|
1544
|
+
'på raden "Avvikelse mot specen:", bara "motsägelse" är ett stopp',
|
|
1545
|
+
)
|
|
1546
|
+
process.exit(2)
|
|
1547
|
+
}
|
|
1548
|
+
if (utfall !== 'TILLBAKA_TILL_SPEC') {
|
|
1549
|
+
console.error(`en motsägelse i ${kalla} stoppar som TILLBAKA_TILL_SPEC, inte ${utfall}`)
|
|
1550
|
+
process.exit(2)
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const lockPath = acquireLock()
|
|
1555
|
+
let skrivet = false
|
|
1556
|
+
try {
|
|
1557
|
+
const state = readStateOrThrow()
|
|
1558
|
+
const befintlig = state.stopp ?? null
|
|
1559
|
+
if (befintlig !== null) {
|
|
1560
|
+
// Write-once på utfall+kalla; datumet och orsaken ingår inte, ett
|
|
1561
|
+
// identiskt omanrop en annan dag (eller med en annan orsakstext) är en
|
|
1562
|
+
// no-op.
|
|
1563
|
+
const begart = { utfall, kalla }
|
|
1564
|
+
const skiljer = Object.keys(begart).filter((k) => befintlig[k] !== begart[k])
|
|
1565
|
+
if (skiljer.length > 0) {
|
|
1566
|
+
throw new ArkivFel(
|
|
1567
|
+
`${statePath()} har redan ett stopp (${befintlig.utfall} ur ${befintlig.kalla}) som skiljer sig ` +
|
|
1568
|
+
`från ${utfall} ur ${kalla} (${skiljer.join(', ')}); arkivet är append-only, kör respec för att nolla`,
|
|
1569
|
+
)
|
|
1570
|
+
}
|
|
1571
|
+
} else {
|
|
1572
|
+
state.stopp = { utfall, kalla, date: today(), ...(orsak ? { orsak } : {}) }
|
|
1573
|
+
// impact.md: posten gick genom bedöm och är analyserad. spec.md och
|
|
1574
|
+
// granskarblock.md: kandidaten/det prisavgörande valet passerade aldrig
|
|
1575
|
+
// bedöms grind, posten stannar i spec_draft som förut.
|
|
1576
|
+
if (kalla === 'impact.md') state.status = 'assessed'
|
|
1577
|
+
writeStateOrThrow(state)
|
|
1578
|
+
skrivet = true
|
|
1579
|
+
}
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
releaseLock(lockPath)
|
|
1582
|
+
if (e instanceof ArkivFel) {
|
|
1583
|
+
console.error(e.message)
|
|
1584
|
+
process.exit(2)
|
|
1585
|
+
}
|
|
1586
|
+
throw e
|
|
1587
|
+
}
|
|
1588
|
+
releaseLock(lockPath)
|
|
1589
|
+
console.log(
|
|
1590
|
+
skrivet ? `OK: stopp ${utfall} ur ${kalla}` : `OK: stopp ${utfall} ur ${kalla} oförändrat`,
|
|
1591
|
+
)
|
|
1592
|
+
} else if (cmd === 'respec') {
|
|
1593
|
+
// Speca steg 5 som kommando: en ny spec över befintlig katalog invaliderar
|
|
1594
|
+
// analysen, godkännandet, domarna och stoppet. runs[] rörs aldrig, arkivet
|
|
1595
|
+
// är append-only; den gamla analysen döps om, raderas inte.
|
|
1596
|
+
avvisaOkandaFlaggor('respec', [])
|
|
1597
|
+
const lockPath = acquireLock()
|
|
1598
|
+
let antal = 0
|
|
1599
|
+
try {
|
|
1600
|
+
const state = readStateOrThrow()
|
|
1601
|
+
const impact = path.join(katalog, 'impact.md')
|
|
1602
|
+
if (existsSync(impact)) {
|
|
1603
|
+
const bas = path.join(katalog, `impact-superseded-${today()}`)
|
|
1604
|
+
let mal = `${bas}.md`
|
|
1605
|
+
for (let n = 2; existsSync(mal); n++) mal = `${bas}-${n}.md`
|
|
1606
|
+
try {
|
|
1607
|
+
renameSync(impact, mal)
|
|
1608
|
+
} catch (e) {
|
|
1609
|
+
throw new ArkivFel(`kunde inte döpa om ${impact} till ${mal}: ${e.message}`)
|
|
1610
|
+
}
|
|
1611
|
+
antal++
|
|
1612
|
+
}
|
|
1613
|
+
const nytt = { ...state }
|
|
1614
|
+
if (nytt.status !== 'spec_draft') {
|
|
1615
|
+
nytt.status = 'spec_draft'
|
|
1616
|
+
antal++
|
|
1617
|
+
}
|
|
1618
|
+
if (nytt.approval !== null) {
|
|
1619
|
+
nytt.approval = null
|
|
1620
|
+
antal++
|
|
1621
|
+
}
|
|
1622
|
+
if (!Array.isArray(nytt.granskarutslag) || nytt.granskarutslag.length > 0) {
|
|
1623
|
+
nytt.granskarutslag = []
|
|
1624
|
+
antal++
|
|
1625
|
+
}
|
|
1626
|
+
if (nytt.stopp !== null) {
|
|
1627
|
+
nytt.stopp = null
|
|
1628
|
+
antal++
|
|
1629
|
+
}
|
|
1630
|
+
// Monoton räknare (#505): varje respec-anrop är ett nytt varv oavsett om
|
|
1631
|
+
// övriga fält redan var normaliserade, till skillnad från fälten ovan
|
|
1632
|
+
// finns inget "redan rätt värde" att jämföra mot, så det här villkoret
|
|
1633
|
+
// är alltid sant.
|
|
1634
|
+
nytt.respec_varv = (state.respec_varv ?? 0) + 1
|
|
1635
|
+
antal++
|
|
1636
|
+
if (antal > 0) writeStateOrThrow(nytt)
|
|
1637
|
+
} catch (e) {
|
|
1638
|
+
releaseLock(lockPath)
|
|
1639
|
+
if (e instanceof ArkivFel) {
|
|
1640
|
+
console.error(e.message)
|
|
1641
|
+
process.exit(2)
|
|
1642
|
+
}
|
|
1643
|
+
throw e
|
|
1644
|
+
}
|
|
1645
|
+
releaseLock(lockPath)
|
|
1646
|
+
console.log(`OK: respec (${antal} ändringar)`)
|
|
1647
|
+
} else if (cmd === 'bluf') {
|
|
1648
|
+
// Läs-enbart tabellvy över katalogen och dess delar. Poängen tas ur
|
|
1649
|
+
// prisfilerna och stoppet ur state.json, aldrig ur `note` eller prosa: den
|
|
1650
|
+
// handskrivna BLUF:en i IPC 14001 felrapporterade.
|
|
1651
|
+
avvisaOkandaFlaggor('bluf', [])
|
|
1652
|
+
const SAKNAS = '–'
|
|
1653
|
+
const cell = (v) => String(v ?? SAKNAS).replace(/\|/g, '\\|')
|
|
1654
|
+
const radFor = (d, post) => {
|
|
1655
|
+
const state = readStateOrThrow(d)
|
|
1656
|
+
const runs = state.runs ?? []
|
|
1657
|
+
const direktaDelar = delKataloger(d).filter((p) => path.dirname(p) === d)
|
|
1658
|
+
let poang = SAKNAS
|
|
1659
|
+
let utfall = SAKNAS
|
|
1660
|
+
let konfidens = SAKNAS
|
|
1661
|
+
let anm = SAKNAS
|
|
1662
|
+
const nScore = maxN(runs, 'score')
|
|
1663
|
+
if (nScore > 0) {
|
|
1664
|
+
const fil = gallandeDokumentFil(state, 'score', nScore)
|
|
1665
|
+
const data = readPricingFileOrThrow(fil, d)
|
|
1666
|
+
poang = data.score ?? SAKNAS
|
|
1667
|
+
utfall = data.outcome ?? SAKNAS
|
|
1668
|
+
konfidens = data.confidence ?? SAKNAS
|
|
1669
|
+
const rad = [...runs].reverse().find((r) => r.file === fil)
|
|
1670
|
+
// En OLIKA som en dom redan lyft (liftingUtslag pekade ut gällande fil
|
|
1671
|
+
// ovan) väntar inte längre på någon; bara en olyft OLIKA får raden.
|
|
1672
|
+
if (rad?.double_run === 'olika' && !liftedByUtslag(state, 'score', nScore)) {
|
|
1673
|
+
anm = 'OLIKA, väntar på dom'
|
|
1674
|
+
const run2 = fil.replace(/\.json$/, '-run2.json')
|
|
1675
|
+
if (existsSync(path.join(d, run2))) {
|
|
1676
|
+
const b = readPricingFileOrThrow(run2, d)
|
|
1677
|
+
poang = `${poang} / ${b.score ?? SAKNAS}`
|
|
1678
|
+
}
|
|
1679
|
+
} else if (rad?.double_run === 'tolerans') {
|
|
1680
|
+
anm = 'tolerans'
|
|
1681
|
+
}
|
|
1682
|
+
} else if (!existsSync(path.join(d, 'spec.md')) && direktaDelar.length > 0) {
|
|
1683
|
+
poang = `bunt (${direktaDelar.length} delar)`
|
|
1684
|
+
utfall = poang
|
|
1685
|
+
} else if (state.stopp == null) {
|
|
1686
|
+
poang = 'ofärdig'
|
|
1687
|
+
}
|
|
1688
|
+
// Täckningspost (ADR-0119, #508): synlig i anm så granskaren ser att en
|
|
1689
|
+
// egen post väntar, utan att leta i pricing-filen. Beräknas separat och
|
|
1690
|
+
// läggs till sist (final-review M6) så att den varken tystas av
|
|
1691
|
+
// OLIKA/tolerans-texten ovan eller av ett stopp nedan.
|
|
1692
|
+
const taeckning = state.coverage_post?.yta ? `täckningspost: ${state.coverage_post.yta}` : null
|
|
1693
|
+
// Stopp trumfar OLIKA och tolerans: posten är stoppad oavsett vad
|
|
1694
|
+
// prisfilen säger, det tidigare anm-innehållet (OLIKA/tolerans) faller
|
|
1695
|
+
// bort. Täckningsposten är inte OLIKA/tolerans-text och överlever detta.
|
|
1696
|
+
if (state.stopp != null) {
|
|
1697
|
+
utfall = state.stopp.utfall
|
|
1698
|
+
anm =
|
|
1699
|
+
state.stopp.kalla === 'spec.md'
|
|
1700
|
+
? 'kandidat (spec.md)'
|
|
1701
|
+
: state.stopp.kalla === 'granskarblock.md'
|
|
1702
|
+
? 'prisavgörande val (granskarblock.md)'
|
|
1703
|
+
: 'motsägelse (impact.md)'
|
|
1704
|
+
}
|
|
1705
|
+
if (taeckning) anm = anm === SAKNAS ? taeckning : `${anm}; ${taeckning}`
|
|
1706
|
+
return `| ${[post, poang, utfall, konfidens, anm].map(cell).join(' | ')} |`
|
|
1707
|
+
}
|
|
1708
|
+
const rot = path.resolve(katalog)
|
|
1709
|
+
const rader = ['| post | poäng | utfall | konfidens | anm |', '| --- | --- | --- | --- | --- |']
|
|
1710
|
+
try {
|
|
1711
|
+
rader.push(radFor(rot, path.basename(rot)))
|
|
1712
|
+
for (const d of delKataloger(rot)) rader.push(radFor(d, path.relative(rot, d)))
|
|
1713
|
+
} catch (e) {
|
|
1714
|
+
if (e instanceof ArkivFel) {
|
|
1715
|
+
console.error(e.message)
|
|
1716
|
+
process.exit(2)
|
|
1717
|
+
}
|
|
1718
|
+
throw e
|
|
1719
|
+
}
|
|
1720
|
+
console.log(rader.join('\n'))
|
|
1721
|
+
} else if (cmd === 'nasta-steg') {
|
|
1722
|
+
// #530: läs-enbart tabell över var posten står, vad en människa väntas
|
|
1723
|
+
// göra och vilket /request-kommando som kör vidare. Samma regler som
|
|
1724
|
+
// SKILL.md:s grindar, härledda i `nastaStegFor` nedan, aldrig handskrivna:
|
|
1725
|
+
// en handskriven avslutning tappade exakt den raden i skarp körning.
|
|
1726
|
+
avvisaOkandaFlaggor('nasta-steg', [])
|
|
1727
|
+
const cell = (v) => String(v).replace(/\|/g, '\\|')
|
|
1728
|
+
const radFor = (d, post) => {
|
|
1729
|
+
const state = readStateOrThrow(d)
|
|
1730
|
+
const direktaDelar = delKataloger(d).filter((p) => path.dirname(p) === d)
|
|
1731
|
+
const arBuntRot = !existsSync(path.join(d, 'spec.md')) && direktaDelar.length > 0
|
|
1732
|
+
const { star, vantar, nasta } = nastaStegFor(state, {
|
|
1733
|
+
post,
|
|
1734
|
+
antalDelar: arBuntRot ? direktaDelar.length : 0,
|
|
1735
|
+
scorerUtfall: gallandeScorerUtfall(state, d),
|
|
1736
|
+
})
|
|
1737
|
+
return `| ${[post, star, vantar, nasta].map(cell).join(' | ')} |`
|
|
1738
|
+
}
|
|
1739
|
+
const rot = path.resolve(katalog)
|
|
1740
|
+
const rader = ['| post | står | väntar på | nästa |', '| --- | --- | --- | --- |']
|
|
1741
|
+
try {
|
|
1742
|
+
rader.push(radFor(rot, path.basename(rot)))
|
|
1743
|
+
for (const d of delKataloger(rot)) rader.push(radFor(d, path.relative(rot, d)))
|
|
1744
|
+
} catch (e) {
|
|
1745
|
+
if (e instanceof ArkivFel) {
|
|
1746
|
+
console.error(e.message)
|
|
1747
|
+
process.exit(2)
|
|
1748
|
+
}
|
|
1749
|
+
throw e
|
|
1750
|
+
}
|
|
1751
|
+
console.log(rader.join('\n'))
|
|
1103
1752
|
} else {
|
|
1104
1753
|
console.error(
|
|
1105
|
-
'okänt kommando (add-run, next-n, ny-korning, proveniens, sparr, utslag, set-status, kalibrering)',
|
|
1754
|
+
'okänt kommando (add-run, next-n, ny-korning, ny-del, proveniens, sparr, utslag, set-status, stopp, respec, bluf, nasta-steg, kalibrering, experiment)',
|
|
1106
1755
|
)
|
|
1107
1756
|
process.exit(2)
|
|
1108
1757
|
}
|
|
1109
1758
|
|
|
1759
|
+
/** Tolkar ett --satt-par "nyckel=värde". Första "=" delar; `eskalerad` är
|
|
1760
|
+
* det enda booleska fältet och tar bara true/false, allt annat är en sträng.
|
|
1761
|
+
* `varianter` och `korningar` har egna flaggor med egen semantik (union,
|
|
1762
|
+
* egen rad) och får inte skrivas rakt av här. */
|
|
1763
|
+
function tolkaSatt(par) {
|
|
1764
|
+
const i = par.indexOf('=')
|
|
1765
|
+
if (i < 1) {
|
|
1766
|
+
console.error(`experiment: --satt ska ha formen nyckel=värde, fick "${par}"`)
|
|
1767
|
+
process.exit(2)
|
|
1768
|
+
}
|
|
1769
|
+
const nyckel = par.slice(0, i)
|
|
1770
|
+
const ra = par.slice(i + 1)
|
|
1771
|
+
if (nyckel === 'varianter' || nyckel === 'korningar') {
|
|
1772
|
+
console.error(`experiment: ${nyckel} skrivs med --variant respektive --korning, inte --satt`)
|
|
1773
|
+
process.exit(2)
|
|
1774
|
+
}
|
|
1775
|
+
if (nyckel === 'eskalerad') {
|
|
1776
|
+
if (ra !== 'true' && ra !== 'false') {
|
|
1777
|
+
console.error(`experiment: eskalerad tar bara true eller false, fick "${ra}"`)
|
|
1778
|
+
process.exit(2)
|
|
1779
|
+
}
|
|
1780
|
+
return [nyckel, ra === 'true']
|
|
1781
|
+
}
|
|
1782
|
+
return [nyckel, ra]
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/** Effort ur agentdefinitionens frontmatter: raden `effort: <värde>` mellan
|
|
1786
|
+
* de inledande `---`-raderna, markörrader (haus_managed:) tål den. Saknas
|
|
1787
|
+
* filen eller raden blir det null och en VARNING på stderr: IPC 14001 hade
|
|
1788
|
+
* effort null på alla rader utan att någon såg det. Läser aldrig brödtexten. */
|
|
1789
|
+
function effortFranAgent(agent) {
|
|
1790
|
+
const p = path.join(claudeDir(), 'agents', `${agent}.md`)
|
|
1791
|
+
let text
|
|
1792
|
+
try {
|
|
1793
|
+
text = stripHausMarker(readFileSync(p, 'utf8'))
|
|
1794
|
+
} catch (e) {
|
|
1795
|
+
console.error(`VARNING: kunde inte läsa ${p} för effort (${e.message}), effort blir null`)
|
|
1796
|
+
return null
|
|
1797
|
+
}
|
|
1798
|
+
const lines = text.split('\n')
|
|
1799
|
+
if (lines[0]?.trim() === '---') {
|
|
1800
|
+
for (let i = 1; i < lines.length && lines[i].trim() !== '---'; i++) {
|
|
1801
|
+
const m = /^effort:\s*(\S+)$/.exec(lines[i])
|
|
1802
|
+
if (m) return m[1]
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
console.error(`VARNING: ${p} saknar en effort-rad i sin frontmatter, effort blir null`)
|
|
1806
|
+
return null
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/** Delkataloger under `dir`, REKURSIVT och i numerisk ordning: del-N som har
|
|
1810
|
+
* en state.json, djupet först (del-1, del-1/del-1, del-1/del-2, del-2, del-10).
|
|
1811
|
+
* En del utan state.json är inte en körning ännu och hoppas över, liksom
|
|
1812
|
+
* allt under den. Delad av proveniens och bluf så de två aldrig kan se
|
|
1813
|
+
* olika delar. */
|
|
1814
|
+
function delKataloger(dir) {
|
|
1815
|
+
let entries
|
|
1816
|
+
try {
|
|
1817
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
1818
|
+
} catch (e) {
|
|
1819
|
+
throw new ArkivFel(`kunde inte lista ${dir}: ${e.message}`)
|
|
1820
|
+
}
|
|
1821
|
+
return entries
|
|
1822
|
+
.filter((e) => e.isDirectory() && /^del-\d+$/.test(e.name))
|
|
1823
|
+
.sort((a, b) => Number(a.name.slice(4)) - Number(b.name.slice(4)))
|
|
1824
|
+
.map((e) => path.join(dir, e.name))
|
|
1825
|
+
.filter((d) => existsSync(path.join(d, 'state.json')))
|
|
1826
|
+
.flatMap((d) => [d, ...delKataloger(d)])
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/** Scorerns gällande utfall ur prisfilen, eller null när inget poängvarv
|
|
1830
|
+
* finns. Läses bara för att `nasta-steg` inte ska säga "sync till brädan"
|
|
1831
|
+
* om en post vars gällande varv är DELA_UPP eller OVER_SKALAN: de utfallen
|
|
1832
|
+
* når arkivet via prisfilen, aldrig via `stopp` (se STOPP_UTFALL). Samma
|
|
1833
|
+
* fil som `bluf` och `pricedBlockers` läser, via samma hjälpare. */
|
|
1834
|
+
function gallandeScorerUtfall(state, d) {
|
|
1835
|
+
const n = maxN(state.runs ?? [], 'score')
|
|
1836
|
+
if (n === 0) return null
|
|
1837
|
+
const fil = gallandeDokumentFil(state, 'score', n)
|
|
1838
|
+
return { fil, outcome: readPricingFileOrThrow(fil, d).outcome ?? null }
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/** Regeltabellen bakom `nasta-steg` (#530). Varje regel är en avläsning av
|
|
1842
|
+
* en grind som redan står i SKILL.md; kolumnen "väntar på" är det en
|
|
1843
|
+
* MÄNNISKA måste göra, "nästa" det /request-kommando som kör vidare.
|
|
1844
|
+
* Första träff vinner, uppifrån:
|
|
1845
|
+
*
|
|
1846
|
+
* | läge | källa i SKILL.md |
|
|
1847
|
+
* | ------------------------------------- | ------------------------------------------------------------- |
|
|
1848
|
+
* | bunt-rot (ingen spec.md, del-N finns) | Experimentytan, buntregeln; bluf:s `bunt (N delar)` |
|
|
1849
|
+
* | spärr står (sparrSkal) | bedöm steg 6 OLIKA "Hänvisa sedan till `döm <post>`"; `döm` |
|
|
1850
|
+
* | stopp KONCEPT/DIAGNOS | speca steg 6 "Kandidatens katalog kan fortfarande läggas på |
|
|
1851
|
+
* | | brädan … men `bedöm` hoppar över den"; stopputfall → kunddialog |
|
|
1852
|
+
* | stopp TILLBAKA_TILL_SPEC | speca steg 6 (ADR-0118) "När valet avgjorts skrivs svaret |
|
|
1853
|
+
* | | under `## Avgjorda val` …, sedan `respec` och ny dispatch" |
|
|
1854
|
+
* | stopp ESKALERA | speca steg 6 "en människa dömer, posten går aldrig en tredje |
|
|
1855
|
+
* | | specrunda"; stopputfall ESKALERA → visa skälet och stanna |
|
|
1856
|
+
* | experiment | Experimentytan: "kan inte förväxlas med en prispost"; |
|
|
1857
|
+
* | | Hårda regler "Experimentdata når aldrig Asana" |
|
|
1858
|
+
* | spec_draft utan approval | speca steg 7 "Erbjud att lägga specarna på brädan (🔍)"; |
|
|
1859
|
+
* | | godkänn/underkänn verkar från 🔍 |
|
|
1860
|
+
* | spec_synced utan approval | godkänn/underkänn-lägesgrinden: 🔍 → ✅ Spec ok |
|
|
1861
|
+
* | spec_draft/spec_synced med approval | bedöm-grinden: "Endast `✅ Spec ok` … släpper igenom", |
|
|
1862
|
+
* | | "Post utan bräda: `godkänn` antecknar approval manual" |
|
|
1863
|
+
* | assessed utan poängvarv | bedöm steg 3–7: analysen finns, poängen saknas |
|
|
1864
|
+
* | assessed, scorerns DELA_UPP | stopputfall → "ny analysrunda per del … `split` satt" |
|
|
1865
|
+
* | assessed, scorerns OVER_SKALAN | stopputfall → "offereras separat; delas aldrig vidare" |
|
|
1866
|
+
* | assessed, scorerns övriga stopp | stopputfall → kunddialog / spec-steget / visa skälet, stanna |
|
|
1867
|
+
* | assessed (poäng) | bedöm steg 8: sync via request-sync tar ✅ → ⚖️, sedan |
|
|
1868
|
+
* | | godkänn/underkänn från ⚖️ |
|
|
1869
|
+
* | priced | godkänn-grinden: ⚖️ → 📤 Klar – till kund |
|
|
1870
|
+
* | guard_run | `guard_run` reserveras för guard-kommandot: inget väntar |
|
|
1871
|
+
* | allt annat | `okänt läge: <status>`, aldrig en gissning |
|
|
1872
|
+
*
|
|
1873
|
+
* `står` är statusen plus det som avviker från grundläget: stoppet,
|
|
1874
|
+
* scorerns stopputfall, olyft OLIKA, `respec_varv` > 0, manuell approval
|
|
1875
|
+
* och experimentmarkören. Funktionen är ren (ingen I/O) så tabellen går
|
|
1876
|
+
* att testa rad för rad. */
|
|
1877
|
+
function nastaStegFor(state, { post, antalDelar, scorerUtfall }) {
|
|
1878
|
+
const status = state.status
|
|
1879
|
+
const gid = state.request_id ?? '<gid>'
|
|
1880
|
+
const adress = state.request_id ?? post
|
|
1881
|
+
const stopp = state.stopp ?? null
|
|
1882
|
+
const runs = state.runs ?? []
|
|
1883
|
+
const nScore = maxN(runs, 'score')
|
|
1884
|
+
const gallandeRad =
|
|
1885
|
+
nScore > 0 ? [...runs].reverse().find((r) => r.file === `score-pricing-v${nScore}.json`) : null
|
|
1886
|
+
const olikaOlyft = gallandeRad?.double_run === 'olika' && !liftedByUtslag(state, 'score', nScore)
|
|
1887
|
+
const vantarDom = sparrSkal(state).length > 0
|
|
1888
|
+
const scorerStopp =
|
|
1889
|
+
scorerUtfall !== null && SCORE_STOP.includes(scorerUtfall.outcome) ? scorerUtfall : null
|
|
1890
|
+
const star = [
|
|
1891
|
+
status,
|
|
1892
|
+
stopp ? `stopp ${stopp.utfall} (${stopp.kalla})` : null,
|
|
1893
|
+
!stopp && scorerStopp ? `${scorerStopp.outcome} (${scorerStopp.fil})` : null,
|
|
1894
|
+
olikaOlyft ? 'OLIKA olyft' : vantarDom ? 'omkörning olyft' : null,
|
|
1895
|
+
(state.respec_varv ?? 0) > 0 ? `respec_varv ${state.respec_varv}` : null,
|
|
1896
|
+
state.approval?.source === 'manual' ? 'approval manual' : null,
|
|
1897
|
+
state.experiment === true ? 'experiment' : null,
|
|
1898
|
+
]
|
|
1899
|
+
.filter((v) => v !== null)
|
|
1900
|
+
.join(', ')
|
|
1901
|
+
const rad = (vantar, nasta, egenStar = star) => ({ star: egenStar, vantar, nasta })
|
|
1902
|
+
const godkannEller = `godkänn ${gid} eller underkänn ${gid} <skäl>`
|
|
1903
|
+
const kunddialog = () =>
|
|
1904
|
+
rad(
|
|
1905
|
+
'kunddialog',
|
|
1906
|
+
'kunddialog; lägg posten på brädan via request-sync (🔍) om den saknas där, bedöm hoppar över den',
|
|
1907
|
+
)
|
|
1908
|
+
const tillbakaTillSpec = (val) =>
|
|
1909
|
+
rad(
|
|
1910
|
+
`avgör valet: ${val}`,
|
|
1911
|
+
`avgör valet, skriv det under ## Avgjorda val i kallmanifest.md, sedan archive.mjs respec och speca ${adress} om`,
|
|
1912
|
+
)
|
|
1913
|
+
|
|
1914
|
+
if (antalDelar > 0) return rad('se delarna', 'se delarna', `bunt (${antalDelar} delar)`)
|
|
1915
|
+
if (vantarDom) return rad('dom (döm)', `döm ${post}`)
|
|
1916
|
+
if (stopp) {
|
|
1917
|
+
const val = stopp.orsak ?? stopp.kalla
|
|
1918
|
+
if (stopp.utfall === 'KONCEPT' || stopp.utfall === 'DIAGNOS') return kunddialog()
|
|
1919
|
+
if (stopp.utfall === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(val)
|
|
1920
|
+
if (stopp.utfall === 'ESKALERA')
|
|
1921
|
+
return rad(`avgör valet: ${val}`, 'människa dömer, ingen ny specrunda')
|
|
1922
|
+
}
|
|
1923
|
+
if (state.experiment === true)
|
|
1924
|
+
return rad('inget (experiment, når aldrig Asana)', 'utvärdera rapport')
|
|
1925
|
+
const godkand = state.approval !== null && state.approval !== undefined
|
|
1926
|
+
if (status === 'spec_draft' || status === 'spec_synced') {
|
|
1927
|
+
if (godkand) return rad('körning av bedöm', `bedöm ${adress}`)
|
|
1928
|
+
return status === 'spec_draft'
|
|
1929
|
+
? rad(
|
|
1930
|
+
'godkännande av spec på brädan',
|
|
1931
|
+
`sync till brädan (request-sync 🔍), sedan ${godkannEller}`,
|
|
1932
|
+
)
|
|
1933
|
+
: rad('godkännande av spec på brädan', godkannEller)
|
|
1934
|
+
}
|
|
1935
|
+
if (status === 'assessed') {
|
|
1936
|
+
if (nScore === 0) return rad('körning av bedöm', `bedöm ${adress} (poängsättning saknas)`)
|
|
1937
|
+
if (scorerStopp) {
|
|
1938
|
+
const o = scorerStopp.outcome
|
|
1939
|
+
if (o === 'DELA_UPP')
|
|
1940
|
+
return rad('ny analysrunda per del', 'dela posten (split), sedan bedöm per del')
|
|
1941
|
+
if (o === 'OVER_SKALAN')
|
|
1942
|
+
return rad('manuell offert', 'offereras separat, delas aldrig vidare')
|
|
1943
|
+
if (o === 'KONCEPT' || o === 'DIAGNOS') return kunddialog()
|
|
1944
|
+
if (o === 'TILLBAKA_TILL_SPEC') return tillbakaTillSpec(scorerStopp.fil)
|
|
1945
|
+
return rad(
|
|
1946
|
+
`människa: ${o} ur ${scorerStopp.fil}`,
|
|
1947
|
+
`visa skälet och stanna (detaljer ${adress} poäng)`,
|
|
1948
|
+
)
|
|
1949
|
+
}
|
|
1950
|
+
return rad(
|
|
1951
|
+
'granskning av poäng på brädan',
|
|
1952
|
+
`sync till brädan (request-sync ⚖️), sedan ${godkannEller}`,
|
|
1953
|
+
)
|
|
1954
|
+
}
|
|
1955
|
+
if (status === 'priced')
|
|
1956
|
+
return rad(
|
|
1957
|
+
'granskning av poäng på brädan',
|
|
1958
|
+
`sync till brädan (request-sync ⚖️) om inte gjort, sedan godkänn ${gid} (📤 Klar – till kund) eller underkänn ${gid} <skäl>`,
|
|
1959
|
+
)
|
|
1960
|
+
if (status === 'guard_run') return rad('inget', 'inget')
|
|
1961
|
+
return rad('–', '–', `okänt läge: ${status}`)
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1110
1964
|
/** Det SENASTE viktningsutslaget (om något) som lyfter en axel vid ett givet
|
|
1111
1965
|
* gällande n — namnger axeln, har ett `n` som är EXAKT det gällande n:et
|
|
1112
1966
|
* (inte "minst lika högt"), och bär en `galler_fil`.
|
|
@@ -1250,8 +2104,8 @@ function gallandeDokumentFil(state, typ, n) {
|
|
|
1250
2104
|
* grinden rapporterade att inget blockerar. Kastar ArkivFel; anroparen
|
|
1251
2105
|
* (set-status) fångar det och avslutar med exit 2, skilt från den vanliga
|
|
1252
2106
|
* spärr-vägran (exit 1). */
|
|
1253
|
-
function readPricingFileOrThrow(fil) {
|
|
1254
|
-
const p = path.join(
|
|
2107
|
+
function readPricingFileOrThrow(fil, d = katalog) {
|
|
2108
|
+
const p = path.join(d, fil)
|
|
1255
2109
|
let raw
|
|
1256
2110
|
try {
|
|
1257
2111
|
raw = readFileSync(p, 'utf8')
|
|
@@ -1345,6 +2199,13 @@ function pricedBlockers(state) {
|
|
|
1345
2199
|
if (state.experiment === true) {
|
|
1346
2200
|
skal.push('experimentvariant blir aldrig priced')
|
|
1347
2201
|
}
|
|
2202
|
+
// Ett stopputfall skrivet av `stopp` (kandidat ur spec.md, motsägelse ur
|
|
2203
|
+
// impact.md) gäller tills `respec` nollar det. Bara HÄR, aldrig i
|
|
2204
|
+
// `sparrSkal`: sparr betyder "väntar på dom" och styr utvärderas
|
|
2205
|
+
// återupptagning; ett stopp där hade låst posten för alltid.
|
|
2206
|
+
if (state.stopp !== null && typeof state.stopp === 'object') {
|
|
2207
|
+
skal.push(`stopputfall ${state.stopp.utfall} ur ${state.stopp.kalla}`)
|
|
2208
|
+
}
|
|
1348
2209
|
const runs = state.runs ?? []
|
|
1349
2210
|
const nClass = maxN(runs, 'classification')
|
|
1350
2211
|
if (nClass > 0) {
|