@haus-tech/haus-workflow 4.7.0 → 4.9.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 +32 -0
- package/dist/cli.js +2696 -899
- package/docs/before-you-start.md +57 -0
- package/docs/glossary.md +56 -0
- package/library/catalog/manifest.json +415 -338
- package/library/catalog/validation-rules.json +6 -2
- package/library/global/agents/request-classifier.md +1 -1
- package/library/global/agents/request-scorer.md +1 -1
- package/library/global/settings-fragments/hooks.json +1 -1
- package/library/global/skills/haus-workflow/references/new.md +18 -11
- package/library/global/skills/request/SKILL.md +315 -23
- package/library/global/skills/request/scripts/compare-scores.mjs +57 -7
- package/library/global/skills/request/scripts/contract-check.mjs +6 -17
- package/library/global/skills/request/scripts/lib.mjs +15 -0
- package/library/global/skills/request/scripts/quote-repair.mjs +166 -0
- package/package.json +5 -2
|
@@ -140,7 +140,9 @@
|
|
|
140
140
|
"myid",
|
|
141
141
|
"oidc",
|
|
142
142
|
"azure-ad",
|
|
143
|
-
"saml2"
|
|
143
|
+
"saml2",
|
|
144
|
+
"auth",
|
|
145
|
+
"next-auth"
|
|
144
146
|
],
|
|
145
147
|
"intentsRequiredTags": [
|
|
146
148
|
"stripe",
|
|
@@ -150,7 +152,9 @@
|
|
|
150
152
|
"myid",
|
|
151
153
|
"oidc",
|
|
152
154
|
"azure-ad",
|
|
153
|
-
"saml2"
|
|
155
|
+
"saml2",
|
|
156
|
+
"auth",
|
|
157
|
+
"next-auth"
|
|
154
158
|
],
|
|
155
159
|
"alwaysAllowedTags": [
|
|
156
160
|
"haus",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: request-classifier
|
|
3
3
|
description: Kör request-classify-skillen isolerat på spec + påverkansanalys och returnerar klassnings-JSON enligt skillens kontrakt. Dispatchas av /request-skillen (uppgift bedöm, parallellt med request-scorer). Saknar medvetet sök- och shellverktyg (Grep/Glob/Bash); Read finns kvar för skillens referensfil och är promptbegränsad till skillkatalogen — därför redovisas varje läst sökväg i svaret. Underlaget kommer bytetroget i prompten; output är JSON-kuvertet plus svansraderna, inget annat.
|
|
4
4
|
model: fable
|
|
5
|
-
effort:
|
|
5
|
+
effort: high
|
|
6
6
|
tools: Skill, Read
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: request-scorer
|
|
3
3
|
description: Kör request-score-skillen isolerat på spec + påverkansanalys och returnerar poäng-JSON med inringning enligt skillens kontrakt. Dispatchas av /request-skillen (uppgift bedöm, parallellt med request-classifier). Saknar medvetet sök- och shellverktyg (Grep/Glob/Bash); Read finns kvar för skillens referensfil och är promptbegränsad till skillkatalogen — därför redovisas varje läst sökväg i svaret. Underlaget kommer bytetroget i prompten; output är JSON-kuvertet plus svansraderna, inget annat.
|
|
4
4
|
model: fable
|
|
5
|
-
effort:
|
|
5
|
+
effort: high
|
|
6
6
|
tools: Skill, Read
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -15,11 +15,12 @@ the agent; collect answers with AskUserQuestion and pass everything as flags.
|
|
|
15
15
|
before anything else: "Add the new project as a member of this workspace, or scaffold it
|
|
16
16
|
standalone elsewhere?"
|
|
17
17
|
|
|
18
|
-
- **Add as workspace member** —
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
- **Add as workspace member** — pass `--dir <workspace-root>` in Phase 3 so the project
|
|
19
|
+
lands beside the other member repos (folder name: the project name) instead of nested
|
|
20
|
+
inside whatever repo this session is standing in. The cwd is the only other placement
|
|
21
|
+
lever, and it is not yours to change. After Phase 3 creates the repo, tell the user to
|
|
22
|
+
add an entry for it to `haus.workspace.yaml` / `repos.manifest.json` themselves — this
|
|
23
|
+
flow only scaffolds and does not write either file (no CLI command mutates them yet).
|
|
23
24
|
- **Standalone** — proceed exactly as below, unaffected by the workspace.
|
|
24
25
|
|
|
25
26
|
Skip this ask entirely when Step 1.5 found Project or Not-set-up scope — proceed straight
|
|
@@ -65,13 +66,16 @@ Check the registry first: `haus new --list --json`.
|
|
|
65
66
|
**Stack in the registry (fast path):**
|
|
66
67
|
|
|
67
68
|
1. Preview:
|
|
68
|
-
`haus new <stack> <name> --json --yes --dry-run [--org <org>] [--no-github]`
|
|
69
|
+
`haus new <stack> <name> --json --yes --dry-run [--dir <path>] [--org <org>] [--no-github]`
|
|
69
70
|
and show the planned steps in plain language. Phase 2's approval already covers
|
|
70
71
|
running this for real — no further confirmation needed.
|
|
71
72
|
2. Execute the same command without `--dry-run`. Parse the JSON:
|
|
72
73
|
- `ok: true` → note project path, repo URL, and `installHint`.
|
|
73
|
-
- `ok: false` → report the
|
|
74
|
-
resume commands.
|
|
74
|
+
- `ok: false` → report the `detail` of every step whose status is not `done`,
|
|
75
|
+
verbatim — they contain the exact resume commands. A step may be `failed` (it did
|
|
76
|
+
not run), `skipped` (it did not happen, e.g. setup was declined), or `partial`
|
|
77
|
+
(it ran and part of it did not land, e.g. the config files were copied but the
|
|
78
|
+
follow-up push failed). Do NOT retry destructively; the project directory is never
|
|
75
79
|
auto-deleted.
|
|
76
80
|
|
|
77
81
|
**Stack not in the registry:**
|
|
@@ -109,9 +113,12 @@ stay there.
|
|
|
109
113
|
|
|
110
114
|
## Phase 5 — Build the walking skeleton
|
|
111
115
|
|
|
112
|
-
1. Install dependencies
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
1. Install dependencies. On the fast path the JSON's `installHint` is written for the
|
|
117
|
+
parent directory — it starts with `cd <name>`, so running it verbatim from inside
|
|
118
|
+
the project (where Phase 4 left you) fails. From in here, run the bare command for the
|
|
119
|
+
chosen package manager instead: `yarn` / `npm install` / `pnpm install`. Off-registry
|
|
120
|
+
there is no hint; run the same bare command. The test loop needs them installed either
|
|
121
|
+
way, so confirm the install actually succeeded before continuing.
|
|
115
122
|
2. Read the new project's `CLAUDE.md` and `.haus-workflow/WORKFLOW.md`; they are
|
|
116
123
|
authoritative from here on.
|
|
117
124
|
3. Decide the project's file/folder structure BEFORE dispatching any build agent:
|
|
@@ -21,6 +21,7 @@ respektive skill. Här bor bara routing, kontextbärande, arkivet och dispatchen
|
|
|
21
21
|
| `godkänn <nr eller gid>` | Flytta Offer status framåt via request-sync — se grindavsnittet |
|
|
22
22
|
| `underkänn <nr eller gid> <skäl>` | Skäl som Asana-kommentar + iterationsräknare via request-sync |
|
|
23
23
|
| `detaljer <gid, slug eller nr> [steg]` | Skriv ut arkiverad JSON/dokument för posten |
|
|
24
|
+
| `utvärdera <post…> [modell]` | A/B-kör hela flödet per modellvariant till experimentytan; `sammanställ` ger svepvyn |
|
|
24
25
|
| `hjälp` | Förklara uppgifterna, rör inga filer |
|
|
25
26
|
|
|
26
27
|
## Hårda regler
|
|
@@ -50,6 +51,11 @@ respektive skill. Här bor bara routing, kontextbärande, arkivet och dispatchen
|
|
|
50
51
|
- **Arkivet är cache, Asana är sanningen.** Vid konflikt vinner brädan.
|
|
51
52
|
- **Godkännande antas aldrig.** Att en spec finns i arkivet betyder inte att den
|
|
52
53
|
är granskad. Se grinden under `bedöm`.
|
|
54
|
+
- **Experimentdata når aldrig Asana.** `utvärdera` skriver aldrig via
|
|
55
|
+
request-sync; dess enda Asana-kontakt är läsande hämtning av önskemålet i
|
|
56
|
+
huvudkonversationen. `experiments/`-trädet är inte postkataloger: sync,
|
|
57
|
+
batch och guard läser bara `.haus/requests/<gid-eller-slug>/` på toppnivån
|
|
58
|
+
och rör aldrig `experiments/`.
|
|
53
59
|
|
|
54
60
|
## Arkivet: `.haus/requests/`
|
|
55
61
|
|
|
@@ -94,6 +100,7 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
|
|
|
94
100
|
"bundle": { "parent_slug": null, "part": null, "of": null },
|
|
95
101
|
"split": { "parent": null, "part": null, "of": null },
|
|
96
102
|
"approval": null,
|
|
103
|
+
"granskarutslag": [],
|
|
97
104
|
"note": null,
|
|
98
105
|
"runs": []
|
|
99
106
|
}
|
|
@@ -106,14 +113,36 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
|
|
|
106
113
|
`{ "source": "asana" | "manual", "by": "<namn, krävs vid manual>",
|
|
107
114
|
"status_read": "<värde>", "date": "YYYY-MM-DD" }`.
|
|
108
115
|
Anteckningen är spårbarhet, aldrig själva godkännandet.
|
|
116
|
+
- `granskarutslag` är en lista, en rad per viktningsutslag (granskaren
|
|
117
|
+
konstaterar att en körning hade rätt, utan att någon sakuppgift ändras).
|
|
118
|
+
`bedöm` steg 6 APPENDAR en rad, skriver ALDRIG över tidigare rader:
|
|
119
|
+
`{ "varv": N, "date": "YYYY-MM-DD", "beslut": "<granskarens beslut,
|
|
120
|
+
ordagrant>" }`. Tom lista (`[]`) när inget viktningsutslag ännu skett.
|
|
121
|
+
Listan är ett revisionsspår och läses medvetet ALDRIG tillbaka in i någon
|
|
122
|
+
prompt: utslaget är granskarens dom över körningarna, och når den agenterna
|
|
123
|
+
styr den nästa bedömning i stället för att dokumentera den föregående.
|
|
124
|
+
Nollas (töms) vid respec, av samma skäl som `approval` nollas (se `speca`
|
|
125
|
+
steg 5): ett utslag mot en ersatt spec är inte längre giltigt. Bor ENDAST
|
|
126
|
+
i `state.json`. Ett sakändringsutslag går i stället in i `impact.md` som
|
|
127
|
+
Granskarutslag-blocket (se `bedöm` steg 6), aldrig hit.
|
|
109
128
|
- `runs[]` får en rad per dispatch, även spec- och analyskörningar:
|
|
110
129
|
`{ "file": "spec.md" | "impact.md" | "<run>.json", "skill": "...",
|
|
111
130
|
"agent": "...", "model": "<ur MODELL-raden>", "effort": "<ur agentdefinitionen>",
|
|
112
131
|
"date": "YYYY-MM-DD",
|
|
113
132
|
"reference_sha256": { "<referensfil>": "<sha256>" },
|
|
114
133
|
"underlag_sha256": { "spec.md": "<sha256>", "impact.md": "<sha256>" },
|
|
115
|
-
"double_run": "bekräftad" | "olika" | null,
|
|
116
|
-
"superseded_by": null
|
|
134
|
+
"double_run": "bekräftad" | "olika" | "tolerans" | null,
|
|
135
|
+
"superseded_by": null, "partial_rerun": false, "pairs_with": null,
|
|
136
|
+
"quote_repairs": <antal> }`.
|
|
137
|
+
`quote_repairs` är frivilligt (utelämnas när det är 0): antal RÄTTAT-rader
|
|
138
|
+
från `quote-repair.mjs` för den körningen.
|
|
139
|
+
`partial_rerun` och `pairs_with` är frivilliga och sätts bara på en
|
|
140
|
+
pricing-rad ur ett riktat varv (se `bedöm` steg 6): `partial_rerun: true`
|
|
141
|
+
markerar att raden kom ur en enaxlig omkörning, och `pairs_with` namnger
|
|
142
|
+
den stående axelns fil den paras med. Pekaren bor på den omkörda axelns
|
|
143
|
+
nya rad, aldrig på den stående axelns äldre rad, som aldrig skrivs om.
|
|
144
|
+
Saknas (eller står `false`/`null`) på spec- och analysrader och på rader
|
|
145
|
+
ur ett fullt varv.
|
|
117
146
|
`underlag_sha256` är underlagsproveniensen: hashen för de dokument körningen
|
|
118
147
|
faktiskt såg. Precheck vägrar bedöma när analysens spec-hash inte matchar
|
|
119
148
|
nuvarande `spec.md`, och guard ska vägra diffa mot en körning vars underlag
|
|
@@ -127,10 +156,45 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
|
|
|
127
156
|
eller `~/.claude`). En senare guard-körning jämför hash och vägrar diffa vid
|
|
128
157
|
skevhet ("omklassning krävs, skalan har ändrats").
|
|
129
158
|
- `status` blir ALDRIG `priced` när något stopputfall finns i varvets
|
|
130
|
-
körningar, eller när
|
|
159
|
+
körningar, eller när det GÄLLANDE paret har `double_run: "olika"` olyft.
|
|
160
|
+
Spärren gäller det gällande paret, inte varje enskild rad för sig: har ett
|
|
161
|
+
senare riktat varv landat LIKA eller LIKA_TOLERANS, är spärren lyft för
|
|
162
|
+
hela paret, även om den oeniga axelns äldre rader fortfarande bär
|
|
163
|
+
`double_run: "olika"` (de skrivs aldrig om, se `partial_rerun`/
|
|
164
|
+
`pairs_with` ovan). Läs upplåsningen på den omkörda axelns senaste rad,
|
|
165
|
+
aldrig på en äldre. `double_run: "tolerans"` räknas som bekräftad och
|
|
166
|
+
blockerar inte `priced`.
|
|
131
167
|
- `spec_synced` sätts av speca steg 7 (posten lagd på brädan);
|
|
132
168
|
`guard_run` reserveras för guard-kommandot (senare byggsteg).
|
|
133
169
|
|
|
170
|
+
### Experimentytan: `experiments/`
|
|
171
|
+
|
|
172
|
+
A/B-körningar från `utvärdera` bor i ett eget träd och är aldrig prisposter:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
<repo-rot>/.haus/requests/experiments/<slug>/
|
|
176
|
+
├── experiment.json ticketreferens, datum, kandidatmodell,
|
|
177
|
+
│ modell per agent per varv, manifest-hash, status
|
|
178
|
+
├── jamforelse.md skrivs när båda varven är klara
|
|
179
|
+
├── baseline/ full standardstruktur enligt arkivkontraktet ovan
|
|
180
|
+
└── <kandidatmodell>/ samma struktur; delkataloger del-N/ vid DELA_UPP
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- Katalognamnet `experiments` är reserverat — en spec vars rubrik skulle
|
|
184
|
+
sluggas till `experiments` får suffixet `-post`.
|
|
185
|
+
- Variantkatalogerna följer postkatalogens kontrakt bytetroget (spec.md,
|
|
186
|
+
granskarblock.md, impact.md, pricing-JSON, state.json, `runs[]`-reglerna) —
|
|
187
|
+
därför fungerar `precheck.mjs` och `contract-check.mjs` oförändrade, och
|
|
188
|
+
proveniensen (modell, effort, hashar) följer med gratis.
|
|
189
|
+
- `state.json` i en variantkatalog bär markören `"experiment": true`.
|
|
190
|
+
`approval` förblir `null` och `status` blir ALDRIG `priced` — ett
|
|
191
|
+
experimentresultat kan inte förväxlas med en prispost.
|
|
192
|
+
- `experiment.json` bär `status` per variant: `"klar"` (båda stegen
|
|
193
|
+
arkiverade och jämförelse-underlag komplett), `"ofärdig"` (avbruten,
|
|
194
|
+
subagentdöd, kontraktsbrott eller MODELL-avvikelse), `"stopp"` (arkiverat
|
|
195
|
+
stopputfall).
|
|
196
|
+
- `experiments/` ärver gitignore-skyddet från `.haus/requests/`.
|
|
197
|
+
|
|
134
198
|
## Uppgift: `speca`
|
|
135
199
|
|
|
136
200
|
1. **Underlag.** Rått önskemål inklistrat, eller ett gid — hämta då posten via
|
|
@@ -158,9 +222,10 @@ och poängen sätts per post, så en katalog med flera specar går inte att bed
|
|
|
158
222
|
tomt) i varje berörd state.json.
|
|
159
223
|
**Respec över befintlig katalog invaliderar analysen:** när `spec.md`
|
|
160
224
|
ersätts döps ev. `impact.md` om till `impact-superseded-<datum>.md`,
|
|
161
|
-
status sätts till `spec_draft
|
|
162
|
-
ogranskad per definition,
|
|
163
|
-
|
|
225
|
+
status sätts till `spec_draft`, `approval` nollas och `granskarutslag`
|
|
226
|
+
töms — en ny spec är ogranskad per definition, ett utslag mot en ersatt
|
|
227
|
+
spec är inte längre giltigt, och en gammal analys mot ny spec är exakt
|
|
228
|
+
den skevhet proveniens-kontrollen finns för att stoppa.
|
|
164
229
|
6. **Formkontroll.** Kör `node <skillkatalog>/scripts/precheck.mjs <katalog>`
|
|
165
230
|
per katalog. BRISTER i spec/granskarblock → dispatcha om specifiern EN gång
|
|
166
231
|
enligt omdispatch-regeln; BRISTER i `state.json` är wrapperns egna filer och
|
|
@@ -217,7 +282,19 @@ oenighetsgrind.
|
|
|
217
282
|
5. **Kontraktskontroll.** Strippa svansraderna (`MODELL:`, `LÄSTA:`, ev.
|
|
218
283
|
`AVVIKELSE:`) ur varje svar, skriv den rena JSON:en till en temporär fil och
|
|
219
284
|
kör `node <skillkatalog>/scripts/contract-check.mjs <svar.json> <spec.md> <impact.md> --expect-id <katalogens id>`.
|
|
220
|
-
BROTT →
|
|
285
|
+
BROTT → kör först
|
|
286
|
+
`node <skillkatalog>/scripts/quote-repair.mjs <svar.json> <spec.md> <impact.md>`
|
|
287
|
+
(deterministisk citaträttning: ett nästan-ordagrant belägg ersätts med sin
|
|
288
|
+
entydigt närmaste källmening, allt annat lämnas orört; utan citatbrott är
|
|
289
|
+
scriptet en no-op). Exit 0: kör kontraktskontrollen igen och anteckna
|
|
290
|
+
`quote_repairs: <antal RÄTTAT-rader>` i runs-raden.
|
|
291
|
+
**VARJE RÄTTAT-rad (före → efter) visas dessutom ordagrant för användaren i
|
|
292
|
+
BLUF-svaret**, på samma sätt som en `AVVIKELSE:`-rad alltid visas. En
|
|
293
|
+
räknare går inte att granska: rättningen byter belägg i en bedömning som
|
|
294
|
+
redan är gjord, och den som läser svaret ska kunna se exakt vilket citat som
|
|
295
|
+
byttes mot vilken källmening. Aldrig bara antalet. Kvarstår BROTT efter
|
|
296
|
+
rättningen, eller gav rättningen exit 1 (orättbara citat): dispatcha om just
|
|
297
|
+
den agenten EN gång enligt omdispatch-regeln;
|
|
221
298
|
kvarstår brott, visa dem och stanna utan att arkivera det svaret som giltigt
|
|
222
299
|
varv — men släng det inte: arkivera det som `<typ>-pricing-vN-avvisad.json`
|
|
223
300
|
(utanför N-räkningen) med kontrollens felrader i en runs-rad, så även
|
|
@@ -245,15 +322,124 @@ oenighetsgrind.
|
|
|
245
322
|
`detaljer` läser alltid filen utan suffix.
|
|
246
323
|
- **Någon OLIKA** → behandla posten som ESKALERA. Det beskriver routingen,
|
|
247
324
|
aldrig artefakten: ingen fil ändras, ingen JSON skrivs om, inget
|
|
248
|
-
ESKALERA-värde patchas in. Arkivera båda
|
|
249
|
-
|
|
250
|
-
runs-
|
|
325
|
+
ESKALERA-värde patchas in. Arkivera båda körningarna för båda axlarna
|
|
326
|
+
(den som inte blir axelns huvudfil arkiveras som `-run2`) och stämpla
|
|
327
|
+
runs-raderna PER AXEL, aldrig per
|
|
328
|
+
varv. Det finns exakt en regel, och den går efter vad axelns eget
|
|
329
|
+
jämförelsescript skrev:
|
|
330
|
+
- **Den OENIGA axeln** (den vars script skrev OLIKA) får
|
|
331
|
+
`double_run: "olika"` och `superseded_by: null` på bägge sina
|
|
332
|
+
runs-rader. Körning 1 är huvudfil utan suffix och körning 2 arkiveras
|
|
333
|
+
som `-run2`, men ingen av dem är utslagen: oenigheten är
|
|
334
|
+
oavgjord tills en människa eller ett riktat varv avgör den, och
|
|
335
|
+
`superseded_by` förblir därför `null` på båda.
|
|
336
|
+
- **Den ENIGA axeln** behåller sitt vanliga utfall och sitt vanliga
|
|
337
|
+
`superseded_by`: LIKA ger `double_run: "bekräftad"` med körning 1 som
|
|
338
|
+
suffixfri huvudfil, LIKA_TOLERANS ger `double_run: "tolerans"` med
|
|
339
|
+
verdiktradens utpekade körning (högsta poängen) som suffixfri
|
|
340
|
+
huvudfil och den andra som `-run2` med `superseded_by` mot den, precis
|
|
341
|
+
enligt LIKA_TOLERANS-punkten nedan, oavsett vilken av dem som var
|
|
342
|
+
körning 1. Regeln "högsta poängen gäller" är densamma oavsett vilken
|
|
343
|
+
punkt som routar posten.
|
|
344
|
+
|
|
345
|
+
Arkivet namnger därmed vilken axel som var oenig: `"olika"` står bara på
|
|
346
|
+
den axelns rader. Jämförelsescriptens verdikter lever bara i stdout och
|
|
347
|
+
försvinner med sessionen, så utan den stämplingen kunde ett senare varv
|
|
348
|
+
köra om fel axel och släppa igenom en oavgjord klassflip till pris.
|
|
349
|
+
Var BÅDA axlarna OLIKA bär bägge axlarnas rader `"olika"`.
|
|
350
|
+
Status blir `assessed`. Visa diffen (rad · körning 1 · körning 2)
|
|
251
351
|
för granskaren. Inget `customer`-block ur någon av körningarna får gå
|
|
252
|
-
till request-sync
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
352
|
+
till request-sync medan det GÄLLANDE paret har `double_run: "olika"`
|
|
353
|
+
olyft (samma scope som `priced`-spärren, se state.json-reglerna ovan):
|
|
354
|
+
har ett senare riktat varv landat LIKA eller LIKA_TOLERANS är spärren
|
|
355
|
+
lyft för hela paret, även om den oeniga axelns äldre rader fortfarande
|
|
356
|
+
bär `"olika"`. Läs upplåsningen på den omkörda axelns senaste rad,
|
|
357
|
+
aldrig på en äldre. Välj ALDRIG en av körningarna själv och räkna
|
|
358
|
+
ALDRIG fram ett medelvärde.
|
|
359
|
+
**Återställningen är ett riktat nytt varv:** spärren (`double_run:
|
|
360
|
+
"olika"`) sitter på den oeniga axelns rader, och ett nytt varv med LIKA
|
|
361
|
+
(eller LIKA_TOLERANS) låser upp hela paret. VILKEN axel som körs om är
|
|
362
|
+
ingen bedömning: det är den axel vars runs-rader bär `"olika"`.
|
|
363
|
+
**Den omkörda axeln dubbelkörs ALLTID i ett riktat varv**, oavsett
|
|
364
|
+
triggervillkoren i steg 6: spärren kan bara lyftas av ett
|
|
365
|
+
jämförelseutslag, och en ensam körning ger inget att jämföra mot. En
|
|
366
|
+
klassomkörning som landar SMALL med stående poäng under 4 skulle annars
|
|
367
|
+
aldrig få något utslag och raderna förbli låsta för alltid. Hur MÅNGA
|
|
368
|
+
axlar som körs om beror på vilket slags utslag granskaren gav:
|
|
369
|
+
- **Viktningsutslag** (granskaren konstaterar att en körning hade rätt,
|
|
370
|
+
utan att någon sakuppgift ändras): appendas som en ny rad i
|
|
371
|
+
`state.json.granskarutslag` (se ovan), skriver ALDRIG över en
|
|
372
|
+
tidigare rad, och går ALDRIG in i `impact.md`. Se
|
|
373
|
+
Granskarutslag-punkten nedan för varför en fri text i underlaget vore
|
|
374
|
+
fel. Underlaget är därmed oförändrat, så kör ENDAST den axel om
|
|
375
|
+
(klass eller poäng) som var OLIKA, och bara den omkörda axelns agent
|
|
376
|
+
dispatchas, aldrig båda. Triggerns villkor läses fortfarande mot den
|
|
377
|
+
omkörda axelns nya svar plus den stående axelns gällande fil, aldrig
|
|
378
|
+
mot ett nytt körningspar, men det är inte triggern som avgör om axeln
|
|
379
|
+
dubbelkörs: i ett riktat varv dubbelkörs den alltid. Var BÅDA axlarna
|
|
380
|
+
OLIKA finns ingen enig axel att hålla stilla: kör då BÅDA om ändå,
|
|
381
|
+
`partial_rerun` förblir `false` på bägge nya rader. Vid enaxlig
|
|
382
|
+
omkörning paras den stående axelns gällande fil med det nya resultatet
|
|
383
|
+
(se steg 8) utan att skrivas om. Landar det riktade varvet
|
|
384
|
+
LIKA/LIKA_TOLERANS lyfts spärren för hela paret, antecknat på den
|
|
385
|
+
omkörda axelns nya runs-rad (`partial_rerun: true`, `pairs_with` mot
|
|
386
|
+
den stående filens namn). Pekaren bor på den nya raden, den stående
|
|
387
|
+
axelns rad skrivs aldrig om, så guard och `detaljer` läser
|
|
388
|
+
upplåsningen via `pairs_with` på den nya raden, aldrig tvärtom.
|
|
389
|
+
- **Sakändringsutslag**, eller ett beslut som av annat skäl måste stå i
|
|
390
|
+
underlaget (se Granskarutslag-punkten nedan): appendas i `impact.md`,
|
|
391
|
+
och vid en faktisk sakändring körs assessorn om (ny `impact.md`).
|
|
392
|
+
Underlaget har då ändrats, en körning mot gammalt underlag får aldrig
|
|
393
|
+
paras med en mot nytt, `underlag_sha256` skulle skilja och
|
|
394
|
+
proveniens-kontrollen finns just för den skevheten, så kör BÅDA
|
|
395
|
+
axlarna om, `partial_rerun` `false` på båda nya rader.
|
|
396
|
+
|
|
397
|
+
- **Granskarutslag skrivs som citat, aldrig som prosa.** När granskarens
|
|
398
|
+
beslut ska in i underlaget, alltså när det ändrar eller preciserar en
|
|
399
|
+
sakuppgift i analysen, eller av annat skäl måste stå i underlaget för
|
|
400
|
+
spårbarhet: appendas det sist i `impact.md` som ett avgränsat block
|
|
401
|
+
enligt mallen nedan, beslutet ordagrant citerat, aldrig omformulerat av
|
|
402
|
+
dispatchern och aldrig invävt i analysens löptext. En fritt formulerad
|
|
403
|
+
utslagsmening läses av nästa körning som sakuppgift och styr om
|
|
404
|
+
klassningen, det var precis felet i pilotkörningarna, därför alltid
|
|
405
|
+
mallen, aldrig fri prosa. Kräver beslutet en faktisk sakändring i
|
|
406
|
+
analysen: kör om assessorn (ny `impact.md`). Underlaget har i båda
|
|
407
|
+
fallen ändrats, så `bedöm` kör om BÅDA axlarna (se riktade-varv-stycket
|
|
408
|
+
ovan). Ett rent viktningsutslag, där granskaren bara konstaterar att en
|
|
409
|
+
körning hade rätt utan att röra någon sakuppgift, hör ALDRIG hemma här:
|
|
410
|
+
det appendas i stället som en rad i `state.json.granskarutslag`, se
|
|
411
|
+
samma stycke, just för att underlaget ska förbli oförändrat och den
|
|
412
|
+
riktade omkörningen giltig. Blocket är internt underlag med samma status som
|
|
413
|
+
Till granskaren-blocket och följer aldrig med i kundtext.
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
## Granskarutslag (varv <N>, <YYYY-MM-DD>)
|
|
417
|
+
|
|
418
|
+
> <granskarens beslut, ordagrant>
|
|
419
|
+
|
|
420
|
+
Utslaget gäller endast viktningen mellan körningarna och ändrar inga
|
|
421
|
+
sakuppgifter i analysen ovan.
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- **Klassjämförelsen LIKA men poängjämförelsen skriver LIKA_TOLERANS**
|
|
425
|
+
(poängskillnad ≤ 0,5 med samma outcome och konfidens; klassjämförelsen
|
|
426
|
+
har ingen tolerans, en klassflip är alltid OLIKA och routar posten som
|
|
427
|
+
ESKALERA oavsett poängens tolerans, se föregående punkt) → paret räknas
|
|
428
|
+
som bekräftat. Bytet gäller ENDAST poängaxelns filer: klassaxeln följer
|
|
429
|
+
LIKA-regeln som vanligt, klass-körning 1-filen blir huvudfil utan suffix
|
|
430
|
+
och klass-körning 2 arkiveras som `-run2` med `superseded_by`. För
|
|
431
|
+
poängaxeln blir verdiktradens utpekade körning (högsta poängen)
|
|
432
|
+
huvudfilen `score-pricing-vN.json`, och den andra arkiveras som `-run2`
|
|
433
|
+
med `superseded_by` satt mot huvudfilen, oavsett vilken av dem som var
|
|
434
|
+
körning 1. N är fortfarande gemensamt för de axlar som kördes i varvet
|
|
435
|
+
(här: klass och poäng, båda).
|
|
436
|
+
Huvudfilerna kan alltså komma från olika körningar, till exempel klass ur
|
|
437
|
+
körning 1 och poäng ur körning 2. Det är avsiktligt: båda körningarna har
|
|
438
|
+
passerat kontraktskontrollen och skillnaden ligger inom toleransen. Sätt
|
|
439
|
+
`double_run: "tolerans"` på poängaxelns bägge runs-rader; klassaxelns
|
|
440
|
+
runs-rader får `double_run: "bekräftad"` som vanligt. Toleransen är en
|
|
441
|
+
medveten prisregel: högsta värdet skyddar marginalen, och ett medelvärde
|
|
442
|
+
räknas fortfarande aldrig fram. Visa toleransraderna i BLUF-svaret.
|
|
257
443
|
- Exit 2 från ett jämförelsescript är ett dispatcherfel (ojämförbara
|
|
258
444
|
körningar) — visa felet och stanna, rapportera aldrig LIKA.
|
|
259
445
|
7. **Oenighetsgrind klass ↔ poäng.** Deterministiska regler, i ordning:
|
|
@@ -264,16 +450,25 @@ oenighetsgrind.
|
|
|
264
450
|
ankarbanden — klass TINY/SMALL med poäng ≥ 4, klass MEDIUM med poäng
|
|
265
451
|
≤ 0.75 eller ≥ 13, klass LARGE/OFFERT med poäng ≤ 3. Flaggan är
|
|
266
452
|
information till granskaren — ingen automatisk åtgärd, ingen omkörning.
|
|
267
|
-
8. **Arkivera** godkända JSON-svar. Paret arkiveras atomärt: båda eller inget
|
|
268
|
-
|
|
269
|
-
|
|
453
|
+
8. **Arkivera** godkända JSON-svar. Paret arkiveras atomärt: båda eller inget
|
|
454
|
+
("båda eller inget" gäller de axlar som kördes i varvet: vid riktad
|
|
455
|
+
omkörning arkiveras den omkörda axelns fil ensam, och dess runs-rad får
|
|
456
|
+
`partial_rerun: true` samt `pairs_with: "<den stående filens namn>"`.
|
|
457
|
+
Pekaren bor på den nya raden, aldrig på den stående axelns äldre rad, som
|
|
458
|
+
aldrig skrivs om, så guard och `detaljer` hittar paret genom att läsa den
|
|
459
|
+
nya raden),
|
|
460
|
+
och `N` i `-pricing-vN.json` är gemensamt för de axlar som kördes i
|
|
461
|
+
varvet (nästa N = högsta befintliga för någon av filtyperna + 1, räknat
|
|
462
|
+
bland de filtyper som kördes; vid riktad omkörning bumpar alltså bara den
|
|
463
|
+
omkörda filtypens N; `-run2`- och `-avvisad`-filer räknas aldrig;
|
|
270
464
|
guard-filer har en EGEN N-sekvens, skild från pricing). Uppdatera
|
|
271
465
|
`state.json.runs` med modell, effort, referens-hash och `underlag_sha256`
|
|
272
466
|
(hash för `spec.md` och `impact.md` som körningen såg) per fil, status →
|
|
273
467
|
`priced` (eller `assessed` vid stopputfall).
|
|
274
468
|
9. **Svara BLUF:** tabell `post · klass · poäng · utfall · konfidens`, en rad
|
|
275
469
|
per avvikelse värd att se (oenighetsflagga, ESKALERA, DELA_UPP,
|
|
276
|
-
INPUT_SAKNAS, AVVIKELSE-rader
|
|
470
|
+
INPUT_SAKNAS, AVVIKELSE-rader, samt varje RÄTTAT-rad från citaträttningen
|
|
471
|
+
enligt steg 5). JSON:en visas inte i chatten —
|
|
277
472
|
`detaljer <post>` gör det. Skriv-tillbaka till brädan sker via request-sync
|
|
278
473
|
i huvudkonversationen, bara på uttrycklig begäran — och det är den flytten
|
|
279
474
|
som tar posten ✅ Spec ok → ⚖️ Poängsättning redo för granskning enligt
|
|
@@ -336,9 +531,95 @@ steg en lista över vad som finns, med steg (`spec`, `granskarblock`, `analys`,
|
|
|
336
531
|
`klass`, `poäng`, `state`) filens innehåll rått. För `klass`/`poäng`: senaste
|
|
337
532
|
`-pricing-vN.json` utan suffix (aldrig `-run2`/`-avvisad`).
|
|
338
533
|
|
|
534
|
+
## Uppgift: `utvärdera`
|
|
535
|
+
|
|
536
|
+
A/B-utvärdering av modellvalet: kör en post genom hela flödet två varv —
|
|
537
|
+
`baseline` med agentdefaults och ett varv med alla agenter tvingade till en
|
|
538
|
+
kandidatmodell (default `opus`) — och arkivera all output sida vid sida under
|
|
539
|
+
experimentytan. Uppgiften samlar underlag; människan analyserar. Ingen dom
|
|
540
|
+
fälls över vilken modell som är bäst.
|
|
541
|
+
|
|
542
|
+
1. **Underlag.** Som `speca` steg 1: rått önskemål eller gid — vid gid hämtas
|
|
543
|
+
posten läsande via request-sync i huvudkonversationen, uppgiftens ENDA
|
|
544
|
+
Asana-kontakt. Skapa `experiments/<slug>/` och `experiment.json` med
|
|
545
|
+
ticketreferens, datum och kandidatmodell.
|
|
546
|
+
2. **Källmanifest EN gång.** Bygg manifestet som i `speca` steg 2 och
|
|
547
|
+
återanvänd det byteidentiskt i båda varven. Anteckna manifest-hash i
|
|
548
|
+
`experiment.json` (`ref-hash.mjs` på manifesttexten). Enda avsedda
|
|
549
|
+
skillnaden mellan varven är modellen.
|
|
550
|
+
3. **Eskaleringsvalet görs en gång** enligt `speca`:s regler och gäller båda
|
|
551
|
+
varven — även eskaleringsvalet är en del av det som hålls konstant.
|
|
552
|
+
4. **Varv `baseline`.** Kör speca-stegen (dispatch, kontraktskontroll,
|
|
553
|
+
arkivering) och därefter bedöm-stegen (analys, klassning ∥ poäng,
|
|
554
|
+
dubbelkörning, citaträttning, riktad omkörning) enligt ordinarie
|
|
555
|
+
uppgifter, men mot variantkatalogen `baseline/` och utan
|
|
556
|
+
godkännandegrind: `approval` förblir `null`, `state.json` får
|
|
557
|
+
`"experiment": true`. Dispatcherna använder ordinarie promptmallar och
|
|
558
|
+
agenternas defaults — ingen override. `REQUEST_ID` och `--expect-id`
|
|
559
|
+
använder alltid ticketens gid/slug, aldrig variantkatalogens namn
|
|
560
|
+
(`baseline`/modellnamnet).
|
|
561
|
+
5. **Varv `<kandidatmodell>`.** Exakt samma steg, prompter och underlag, men
|
|
562
|
+
varje Agent-anrop får modell-override till kandidatmodellen.
|
|
563
|
+
Agentdefinitionerna röres aldrig; effort tas ur definitionen som vanligt.
|
|
564
|
+
Verifiera via MODELL-svansraden att overriden slog igenom och redovisa
|
|
565
|
+
avvikelsen om den finns. Generellt för hela uppgiften: subagentdöd,
|
|
566
|
+
kontraktsbrott (kvarstående BROTT efter citaträttning) eller
|
|
567
|
+
MODELL-avvikelse — i vilket steg det än inträffar — sätter variantens
|
|
568
|
+
`status` till `"ofärdig"` i `experiment.json`.
|
|
569
|
+
6. **DELA_UPP körs autonomt.** Ett DELA_UPP-utfall följer bedöms ordinarie
|
|
570
|
+
delningsregler men utan paus för granskare: delkataloger
|
|
571
|
+
`<variant>/del-1/ … del-N/` med `split` satt, förälderns `spec.md` +
|
|
572
|
+
`granskarblock.md` kopierade in, ny analys + klassning + poäng per del.
|
|
573
|
+
En del utan tak blir `OVER_SKALAN` och delas aldrig vidare. Övriga
|
|
574
|
+
stopputfall (`TILLBAKA_TILL_SPEC`, `KONCEPT`, `DIAGNOS`, `ESKALERA`,
|
|
575
|
+
`INPUT_SAKNAS`) kräver människa eller kund: arkivera utfallet, avbryt det
|
|
576
|
+
varvet och redovisa i jämförelsen — det andra varvet fortsätter oberoende.
|
|
577
|
+
7. **Jämförelse.** När båda varven är klara, skriv
|
|
578
|
+
`experiments/<slug>/jamforelse.md`: delningsbeslutet per variant (delade
|
|
579
|
+
sig / delade sig inte), tabell per post/delpost med klass, poäng,
|
|
580
|
+
inringning, dubbelkörningsutfall, stopputfall och `quote_repairs` per
|
|
581
|
+
variant, samt kort notis om specskillnader (rubrik, omfång, Ingår
|
|
582
|
+
inte-avvikelser). Ingen värdering av vilken variant som är bättre —
|
|
583
|
+
rådatan per katalog är analysunderlaget.
|
|
584
|
+
8. **Rapport.** BLUF i chatten: klart/ofärdigt per varv, stopputfall, och
|
|
585
|
+
sökvägen till `experiments/<slug>/`. Ingen skrivning mot Asana.
|
|
586
|
+
|
|
587
|
+
### Batch: flera poster
|
|
588
|
+
|
|
589
|
+
`utvärdera <post1> <post2> … [kandidatmodell]` kör varje ticket i en EGEN
|
|
590
|
+
headless-process, sekventiellt:
|
|
591
|
+
|
|
592
|
+
```
|
|
593
|
+
claude -p "/request utvärdera <gid> <kandidatmodell>"
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
- **En ticket = ett färskt kontextfönster.** Allt tillstånd bor i arkivet på
|
|
597
|
+
disk; huvudsessionen samlar bara exitstatus och en radssammanfattning per
|
|
598
|
+
ticket. `/clear` mellan tickets behövs inte — processgränsen är
|
|
599
|
+
motsvarigheten. Headless-körningen förutsätter att uppgiften kan köra utan
|
|
600
|
+
interaktiva permission-frågor, vilket håller: den läser repot och skriver
|
|
601
|
+
enbart under `.haus/requests/` — plus EN läsande Asana-hämtning per ticket
|
|
602
|
+
(steg 1), så MCP-behörigheten för läsning måste täcka headless-körning.
|
|
603
|
+
- **Kör vidare vid fel.** En ticket vars gid inte går att hämta eller vars
|
|
604
|
+
process kraschar noteras i sammanfattningen; resten av listan fortsätter.
|
|
605
|
+
- **Återupptagbart:** innan ett varv startas kontrolleras arkivet —
|
|
606
|
+
skippet styrs av variantens `status` i `experiment.json`: `"klar"` eller
|
|
607
|
+
`"stopp"` hoppas över med en notis, `"ofärdig"` körs om från varvets
|
|
608
|
+
början. Varven är billiga relativt risken att lappa ihop halva körningar.
|
|
609
|
+
Ett svep över många tickets kan därmed fortsätta i en senare session utan
|
|
610
|
+
omkörningar.
|
|
611
|
+
|
|
612
|
+
### Sammanställning
|
|
613
|
+
|
|
614
|
+
`utvärdera sammanställ` läser alla `experiments/*/` och skriver
|
|
615
|
+
`experiments/sammanstallning.md`: tabell per ticket med klass/poäng per
|
|
616
|
+
variant, delnings- och stopputfall, och avvikelsemarkering där varianterna
|
|
617
|
+
skiljer sig. Halvfärdiga kataloger märks `ofullständig`, aldrig tyst
|
|
618
|
+
utelämnade. Bara läsning — inga agentdispatcher, inga Asana-anrop.
|
|
619
|
+
|
|
339
620
|
## Mekaniska kontroller: `scripts/`
|
|
340
621
|
|
|
341
|
-
|
|
622
|
+
Sju deterministiska script (fem kontroller + `lib.mjs` med de delade
|
|
342
623
|
byggstenarna + `ref-hash.mjs` för stabil hashning) ersätter modellkörningar
|
|
343
624
|
där kontrollen är ren form —
|
|
344
625
|
billigare, snabbare och utan omdömesglidning. Varje script bär sha256 för de
|
|
@@ -361,9 +642,20 @@ referensfiler vars regler det speglar och VARNAR när en katalogbump ändrat dem
|
|
|
361
642
|
om 0.8-gränsen. Skriver `LIKA` eller `OLIKA` + diffrader, exit 0/1; exit 2 =
|
|
362
643
|
ojämförbara körningar (dispatcherfel, aldrig LIKA).
|
|
363
644
|
- `scripts/compare-scores.mjs <a.json> <b.json>` — samma för poäng: outcome,
|
|
364
|
-
score, confidence, golv- och takankare. Aldrig prosa.
|
|
365
|
-
|
|
366
|
-
|
|
645
|
+
score, confidence, golv- och takankare. Aldrig prosa. Skriver `LIKA`,
|
|
646
|
+
`LIKA_TOLERANS` (poängskillnad ≤ 0,5 med samma outcome och confidence, och
|
|
647
|
+
ankarband som överlappar eller angränsar; exit 0, verdiktraden pekar ut
|
|
648
|
+
körningen med högst poäng) eller `OLIKA` + diffrader; i övrigt samma
|
|
649
|
+
exit-semantik. Båda compare-scripten kräver mode `pricing`; guard-jämförelser
|
|
650
|
+
är en medveten utbyggnad i guard-steget.
|
|
651
|
+
- `scripts/quote-repair.mjs <run.json> <spec.md> [impact.md]`: deterministisk
|
|
652
|
+
citaträttning som körs FÖRE omdispatch när contract-check fällt ett citat: ett
|
|
653
|
+
nästan-ordagrant belägg (eller citattecken-omgärdat driver-segment) ersätts med
|
|
654
|
+
sin entydigt närmaste källmening, allt annat lämnas orört. Rättar aldrig över
|
|
655
|
+
en polaritetsskillnad (negationsord) och skriver allt eller inget, så en
|
|
656
|
+
halvrättad fil aldrig arkiveras. Exit 0 = inga orättbara citat kvar (rättade
|
|
657
|
+
eller inga), 1 = orättbara kvar och filen orörd (omdispatch), 2 =
|
|
658
|
+
användningsfel eller oläsbar input (dispatcherfel).
|
|
367
659
|
|
|
368
660
|
Kontrollerna bedömer aldrig innehåll och returnerar aldrig protokollutfall —
|
|
369
661
|
en post som passerar kan fortfarande stoppas av protokollen själva, och det är
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
* prosa) och inringningens golv- och takankare. Prosa (why, drivers,
|
|
7
7
|
* rationale) jämförs ALDRIG.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Toleransregel: en poängskillnad ≤ 0,5 med samma outcome och confidence är
|
|
10
|
+
* prisbrus, inte oenighet. En ankarskillnad tolereras dessutom bara när banden
|
|
11
|
+
* överlappar eller angränsar; icke-angränsande band är hård diff även vid samma
|
|
12
|
+
* poäng. LIKA_TOLERANS hör till LIKA-familjen (exit 0) med
|
|
13
|
+
* en deterministisk vinnare (högsta poängen, skyddar marginalen). Den
|
|
14
|
+
* skriver aldrig om en fil och räknar aldrig fram ett medelvärde.
|
|
15
|
+
*
|
|
16
|
+
* Skriver LIKA, LIKA_TOLERANS (exit 0) eller OLIKA + diffrader (exit 1) på
|
|
17
|
+
* stdout; varningar går på stderr. Vid OLIKA väljer dispatchern ALDRIG en
|
|
18
|
+
* körning och räknar ALDRIG medelvärde — posten eskaleras till granskaren med
|
|
19
|
+
* båda inringningarna.
|
|
12
20
|
*
|
|
13
21
|
* Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
|
|
14
22
|
* rälsen bor i lib.mjs och delas med compare-classifications.
|
|
@@ -37,14 +45,56 @@ if (a.outcome !== b.outcome) diffs.push(`outcome: ${show(a.outcome)} ↔ ${show(
|
|
|
37
45
|
// säkerhet är information granskaren ska se, precis som klassningens konfidenssida.
|
|
38
46
|
if (a.confidence !== b.confidence)
|
|
39
47
|
diffs.push(`confidence: ${show(a.confidence)} ↔ ${show(b.confidence)}`)
|
|
48
|
+
// Toleransregel (beslut 2026-08-28, ADR-0078): en poängskillnad inom 0,5 med
|
|
49
|
+
// samma outcome och confidence är prisbrus, inte oenighet. Båda pilotfallens
|
|
50
|
+
// eskaleringar gällde Δ = 0,25 och kostade ett helt nytt varv styck. Högsta
|
|
51
|
+
// poängen gäller (skyddar marginalen); ett medelvärde räknas fortfarande aldrig.
|
|
52
|
+
const TOLERANS = 0.5
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ankarnummer på skalan A1..A17. Golv null (under-A1-fallet) läses som 0, alltså
|
|
56
|
+
* steget under A1. Ett ankare som inte går att tolka ger null och behandlas som
|
|
57
|
+
* icke-angränsande, så en oläslig inringning aldrig kan tystas som brus.
|
|
58
|
+
*/
|
|
59
|
+
const ankarNr = (v) => {
|
|
60
|
+
if (v === null || v === undefined) return 0
|
|
61
|
+
const m = /^A(\d+)$/.exec(String(v))
|
|
62
|
+
return m ? Number(m[1]) : null
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Inringningarna får toleransbehandling bara när de faktiskt pekar på samma del
|
|
66
|
+
* av skalan: överlappande eller angränsande band. Två körningar som landar på
|
|
67
|
+
* samma poäng men ringar in A5/A6 respektive A11/A12 är inte prisbrus, det är
|
|
68
|
+
* precis den drift dubbelkörningen finns för att visa, även när summan råkar bli
|
|
69
|
+
* densamma.
|
|
70
|
+
*/
|
|
71
|
+
const nastanSammaBand = (fa, ta, fb, tb) => {
|
|
72
|
+
const [l1, h1, l2, h2] = [ankarNr(fa), ankarNr(ta), ankarNr(fb), ankarNr(tb)]
|
|
73
|
+
if ([l1, h1, l2, h2].some((n) => n === null)) return false
|
|
74
|
+
return l1 <= h2 + 1 && l2 <= h1 + 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tolerans = []
|
|
40
78
|
if (a.outcome === 'POANG' && b.outcome === 'POANG') {
|
|
41
|
-
|
|
79
|
+
const delta = Math.abs(a.score - b.score)
|
|
80
|
+
const inomTolerans = Number.isFinite(delta) && delta <= TOLERANS
|
|
81
|
+
if (a.score !== b.score)
|
|
82
|
+
(inomTolerans ? tolerans : diffs).push(`score: ${show(a.score)} ↔ ${show(b.score)}`)
|
|
42
83
|
const fa = a.bracket?.floor?.anchor ?? null
|
|
43
84
|
const fb = b.bracket?.floor?.anchor ?? null
|
|
44
85
|
const ta = a.bracket?.ceiling?.anchor ?? null
|
|
45
86
|
const tb = b.bracket?.ceiling?.anchor ?? null
|
|
46
|
-
|
|
47
|
-
if (
|
|
87
|
+
const ankarDiffs = []
|
|
88
|
+
if (fa !== fb) ankarDiffs.push(`bracket.floor.anchor: ${show(fa)} ↔ ${show(fb)}`)
|
|
89
|
+
if (ta !== tb) ankarDiffs.push(`bracket.ceiling.anchor: ${show(ta)} ↔ ${show(tb)}`)
|
|
90
|
+
;(inomTolerans && nastanSammaBand(fa, ta, fb, tb) ? tolerans : diffs).push(...ankarDiffs)
|
|
48
91
|
}
|
|
49
92
|
|
|
50
|
-
|
|
93
|
+
if (diffs.length === 0 && tolerans.length > 0) {
|
|
94
|
+
const winner = b.score > a.score ? 2 : 1
|
|
95
|
+
console.log(`LIKA_TOLERANS: högsta gäller ${Math.max(a.score, b.score)} (körning ${winner})`)
|
|
96
|
+
for (const t of tolerans) console.log(`- ${t}`)
|
|
97
|
+
process.exit(0)
|
|
98
|
+
}
|
|
99
|
+
// Hårda diffar visas tillsammans med toleransraderna: granskaren ska se helheten.
|
|
100
|
+
reportDiffs([...diffs, ...tolerans])
|