@haus-tech/haus-workflow 4.5.0 → 4.7.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 +28 -0
- package/dist/cli.js +871 -429
- package/library/catalog/manifest.json +6 -15
- package/library/global/agents/request-assessor.md +44 -0
- package/library/global/agents/request-classifier.md +55 -0
- package/library/global/agents/request-scorer.md +54 -0
- package/library/global/agents/request-specifier-eskalerad.md +43 -0
- package/library/global/agents/request-specifier.md +44 -0
- package/library/global/skills/request/SKILL.md +446 -0
- package/library/global/skills/request/scripts/compare-classifications.mjs +94 -0
- package/library/global/skills/request/scripts/compare-scores.mjs +50 -0
- package/library/global/skills/request/scripts/contract-check.mjs +334 -0
- package/library/global/skills/request/scripts/lib.mjs +113 -0
- package/library/global/skills/request/scripts/precheck.mjs +217 -0
- package/library/global/skills/request/scripts/ref-hash.mjs +22 -0
- package/package.json +2 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: request
|
|
3
|
+
description: Enda ingången till Haus requestflöde. Använd när användaren säger "/request", "kör requestflödet", "speca och bedöm", "prisa den här posten", "ta önskemålet hela vägen", "vad ligger i kön", "detaljer <post>", eller när flera request-skills ska kedjas i en följd. Enskilda request-skills (request-write-spec, request-classify m.fl.) fungerar fortfarande fristående; den här skillen äger kedjan, arkivet och dispatchen till subagenter.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /request — requestflödets ingång
|
|
7
|
+
|
|
8
|
+
Tunn orkestrering ovanpå de fem request-skillsen. Den här skillen innehåller
|
|
9
|
+
**noll domänregler**: allt om specformat, klassning, poäng och Asana-fält bor i
|
|
10
|
+
respektive skill. Här bor bara routing, kontextbärande, arkivet och dispatchen.
|
|
11
|
+
|
|
12
|
+
## Uppgifter
|
|
13
|
+
|
|
14
|
+
`/request [uppgift]` — utan argument: visa menyn nedan.
|
|
15
|
+
|
|
16
|
+
| Uppgift | Gör |
|
|
17
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
18
|
+
| `speca <önskemål eller gid>` | Källmanifest → dispatcha `request-specifier` → arkivera spec + granskarblock |
|
|
19
|
+
| `bedöm <gid, slug eller nr>` | Grind → säkra analys → dispatcha `request-classifier` ∥ `request-scorer` → arkivera JSON |
|
|
20
|
+
| `status` | Kö-läge via request-sync, BLUF-tabell med radnummer |
|
|
21
|
+
| `godkänn <nr eller gid>` | Flytta Offer status framåt via request-sync — se grindavsnittet |
|
|
22
|
+
| `underkänn <nr eller gid> <skäl>` | Skäl som Asana-kommentar + iterationsräknare via request-sync |
|
|
23
|
+
| `detaljer <gid, slug eller nr> [steg]` | Skriv ut arkiverad JSON/dokument för posten |
|
|
24
|
+
| `hjälp` | Förklara uppgifterna, rör inga filer |
|
|
25
|
+
|
|
26
|
+
## Hårda regler
|
|
27
|
+
|
|
28
|
+
- **request-sync körs ALDRIG i en subagent.** Uppsättning och brädverifiering är
|
|
29
|
+
interaktiva, fältkartan är sessionsstate, och en enda skrivare mot Asana
|
|
30
|
+
eliminerar races. All Asana-läsning/skrivning sker i huvudkonversationen via
|
|
31
|
+
Skill-verktyget (`request-sync`).
|
|
32
|
+
- **En skrivare i taget mot Asana.** Batchresultat skrivs tillbaka sekventiellt,
|
|
33
|
+
en post i taget, aldrig parallellt.
|
|
34
|
+
- **Dokument klistras bytetroget** in i subagent-prompter. Klassningens och
|
|
35
|
+
poängens belägg är ordagranna citat; en trunkerad eller omformaterad inklistring
|
|
36
|
+
ger tyst fel svar. Klistra aldrig in en sammanfattning.
|
|
37
|
+
- **Prislistan och repot hålls borta från classifier/scorer.** Deras
|
|
38
|
+
agentdefinitioner saknar sök- och shellverktyg (Grep/Glob/Bash), men Read
|
|
39
|
+
finns kvar för skillens referensfil — läs-scopet är alltså en promptregel,
|
|
40
|
+
inte en fysisk spärr. Därför: lägg aldrig prislistans innehåll i prompten,
|
|
41
|
+
och lägg inga egna sökvägar i dispatcherns rader utöver etiketterna i mallen.
|
|
42
|
+
Sökvägar som redan står i det bytetrogna underlaget (analysen namnger filer,
|
|
43
|
+
det är dess jobb) är inte ett brott — regeln gäller det dispatchern
|
|
44
|
+
lägger till. Granska `LÄSTA:`-raden i svaret (se dispatch-kontraktet).
|
|
45
|
+
- **Granskarblocken når aldrig kunden.** Specens granskarblock arkiveras separat
|
|
46
|
+
(`granskarblock.md`). Analysens "## Till granskaren"-block ligger kvar i
|
|
47
|
+
`impact.md` (det är del av underlaget för klassningen) — men inget
|
|
48
|
+
granskarblock får någonsin skickas med i material som går till request-sync
|
|
49
|
+
för kundsynliga fält.
|
|
50
|
+
- **Arkivet är cache, Asana är sanningen.** Vid konflikt vinner brädan.
|
|
51
|
+
- **Godkännande antas aldrig.** Att en spec finns i arkivet betyder inte att den
|
|
52
|
+
är granskad. Se grinden under `bedöm`.
|
|
53
|
+
|
|
54
|
+
## Arkivet: `.haus/requests/`
|
|
55
|
+
|
|
56
|
+
Orkestreringens kuvert. De haus-hanterade request-skillsen känner inte till det;
|
|
57
|
+
den här skillen (och agenterna via sina prompter) underhåller det.
|
|
58
|
+
|
|
59
|
+
**En katalog = en spec = en prissättningsbar post.** En bunt (flera specar ur
|
|
60
|
+
samma önskemål) blir flera kataloger — klassningen räknar omfattning per spec
|
|
61
|
+
och poängen sätts per post, så en katalog med flera specar går inte att bedöma.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
<repo-rot>/.haus/requests/<gid-eller-slug>/
|
|
65
|
+
├── spec.md EN spec, bytetrogen — det granskaren godkänner
|
|
66
|
+
├── granskarblock.md "## Till granskaren" (bunt: samma block
|
|
67
|
+
│ kopieras till varje delkatalog)
|
|
68
|
+
├── impact.md påverkansanalysen, ordagrann inkl. sitt
|
|
69
|
+
│ Till granskaren-block
|
|
70
|
+
├── classification-pricing-vN.json nytt varv = ny fil, skriv aldrig över
|
|
71
|
+
├── score-pricing-vN.json N är gemensamt för paret (se bedöm steg 8)
|
|
72
|
+
├── classification-guard-vN.json guard-utfall synkas aldrig, bara arkiveras
|
|
73
|
+
├── score-guard-vN.json
|
|
74
|
+
└── state.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Katalognamn: Asana-gid när posten finns på brädan, annars en kebab-slug av
|
|
78
|
+
spec-rubriken. Bunt utan gid: `<slug>-1`, `<slug>-2`, … När gid myntas senare:
|
|
79
|
+
döp om katalogen till gid och notera sluggen i `state.json.former_slug`.
|
|
80
|
+
- **Första gången arkivet skapas i ett repo:** säkerställ att `.haus/requests/`
|
|
81
|
+
står i repots `.gitignore` (lägg till raden om den saknas, säg det).
|
|
82
|
+
Prisunderlag ska inte in i kundens historik. `.haus/request.json` (syncens
|
|
83
|
+
config) förblir spårad.
|
|
84
|
+
|
|
85
|
+
### `state.json`
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"request_id": "<gid eller null>",
|
|
90
|
+
"slug": "<kebab-rubrik>",
|
|
91
|
+
"former_slug": null,
|
|
92
|
+
"title": "<spec-rubrik>",
|
|
93
|
+
"status": "spec_draft | spec_synced | assessed | priced | guard_run",
|
|
94
|
+
"bundle": { "parent_slug": null, "part": null, "of": null },
|
|
95
|
+
"split": { "parent": null, "part": null, "of": null },
|
|
96
|
+
"approval": null,
|
|
97
|
+
"note": null,
|
|
98
|
+
"runs": []
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `bundle` = specar ur samma önskemål (sätts av speca vid bunt).
|
|
103
|
+
`split` = delposter ur ett DELA_UPP-utfall (sätts av bedöm). Båda formen
|
|
104
|
+
`{parent…, part: N, of: M}`.
|
|
105
|
+
- `approval` skrivs av grinden i bedöm:
|
|
106
|
+
`{ "source": "asana" | "manual", "by": "<namn, krävs vid manual>",
|
|
107
|
+
"status_read": "<värde>", "date": "YYYY-MM-DD" }`.
|
|
108
|
+
Anteckningen är spårbarhet, aldrig själva godkännandet.
|
|
109
|
+
- `runs[]` får en rad per dispatch, även spec- och analyskörningar:
|
|
110
|
+
`{ "file": "spec.md" | "impact.md" | "<run>.json", "skill": "...",
|
|
111
|
+
"agent": "...", "model": "<ur MODELL-raden>", "effort": "<ur agentdefinitionen>",
|
|
112
|
+
"date": "YYYY-MM-DD",
|
|
113
|
+
"reference_sha256": { "<referensfil>": "<sha256>" },
|
|
114
|
+
"underlag_sha256": { "spec.md": "<sha256>", "impact.md": "<sha256>" },
|
|
115
|
+
"double_run": "bekräftad" | "olika" | null,
|
|
116
|
+
"superseded_by": null }`.
|
|
117
|
+
`underlag_sha256` är underlagsproveniensen: hashen för de dokument körningen
|
|
118
|
+
faktiskt såg. Precheck vägrar bedöma när analysens spec-hash inte matchar
|
|
119
|
+
nuvarande `spec.md`, och guard ska vägra diffa mot en körning vars underlag
|
|
120
|
+
bytts ut — utan detta är ett respec-varv mot gammal analys osynligt.
|
|
121
|
+
Referens-hashen bor i run-raden (inte på toppnivå) och räknas vid körning med
|
|
122
|
+
`node <skillkatalog>/scripts/ref-hash.mjs <referensfil>` — ALDRIG rå
|
|
123
|
+
`shasum` på hela filen: installern stämplar filerna med paketversionen, så en
|
|
124
|
+
hel-fils-hash byter värde vid varje haus-release och skulle ogiltigförklara
|
|
125
|
+
alla arkiverade körningar. Samma verktyg används för `underlag_sha256`.
|
|
126
|
+
Referensfilerna ligger under Claude-konfigkatalogen (`CLAUDE_CONFIG_DIR`
|
|
127
|
+
eller `~/.claude`). En senare guard-körning jämför hash och vägrar diffa vid
|
|
128
|
+
skevhet ("omklassning krävs, skalan har ändrats").
|
|
129
|
+
- `status` blir ALDRIG `priced` när något stopputfall finns i varvets
|
|
130
|
+
körningar, eller när dubbelkörningen gav OLIKA (`double_run: "olika"`).
|
|
131
|
+
- `spec_synced` sätts av speca steg 7 (posten lagd på brädan);
|
|
132
|
+
`guard_run` reserveras för guard-kommandot (senare byggsteg).
|
|
133
|
+
|
|
134
|
+
## Uppgift: `speca`
|
|
135
|
+
|
|
136
|
+
1. **Underlag.** Rått önskemål inklistrat, eller ett gid — hämta då posten via
|
|
137
|
+
request-sync (huvudkonversationen) och använd `## Ursprunglig förfrågan`
|
|
138
|
+
ordagrant.
|
|
139
|
+
2. **Källmanifest.** Isolerade subagenter kan inte "leta innan de antar" utan
|
|
140
|
+
hjälp. Bygg ett manifest innan dispatch: relevanta grep-träffar i repot
|
|
141
|
+
(vyer/termer önskemålet nämner), tidigare specar för samma kund (arkivet +
|
|
142
|
+
brädan), ev. brief. **Alla sökvägar i manifestet absoluta.** Finns inget repo
|
|
143
|
+
i sessionen: skriv `KÄLLOR: inga — allt blir antaganden` så specifiern vet.
|
|
144
|
+
3. **Eskaleringsbedömning.** Misstänkt bunt (flera beslut), behörighet/betalning/
|
|
145
|
+
oåterkallelig data/personuppgifter, eller offertunderlag → utfallet är
|
|
146
|
+
prisbärande och eskaleringen är ett MODELLBYTE, inte en flagga: dispatcha
|
|
147
|
+
`request-specifier-eskalerad` (frontier-modell) i stället för
|
|
148
|
+
`request-specifier`, med `ESKALERAD: ja` plus orsaken i prompten.
|
|
149
|
+
4. **Dispatcha `request-specifier`** (eller den eskalerade varianten enligt
|
|
150
|
+
steg 3) enligt promptmallen i dispatch-kontraktet.
|
|
151
|
+
5. **Arkivera.** Dela svaret vid `## Till granskaren`. En spec → en katalog:
|
|
152
|
+
allt före blocket är specarna; VARJE spec får sin egen katalog med egen
|
|
153
|
+
`spec.md` och eget `state.json` (bunt: `bundle`-fältet ifyllt, blocket
|
|
154
|
+
kopieras till varje katalog som `granskarblock.md`). Bunt ur en gid-post:
|
|
155
|
+
del 1 behåller gid-katalogen, resterande delar får slug-kataloger med
|
|
156
|
+
`bundle.parent_slug` satt till gid:t; syskontasks på brädan mintas först
|
|
157
|
+
vid sync. Lägg en `runs`-rad (`file: "spec.md"`, med `underlag_sha256`
|
|
158
|
+
tomt) i varje berörd state.json.
|
|
159
|
+
**Respec över befintlig katalog invaliderar analysen:** när `spec.md`
|
|
160
|
+
ersätts döps ev. `impact.md` om till `impact-superseded-<datum>.md`,
|
|
161
|
+
status sätts till `spec_draft` och `approval` nollas — en ny spec är
|
|
162
|
+
ogranskad per definition, och en gammal analys mot ny spec är exakt den
|
|
163
|
+
skevhet proveniens-kontrollen finns för att stoppa.
|
|
164
|
+
6. **Formkontroll.** Kör `node <skillkatalog>/scripts/precheck.mjs <katalog>`
|
|
165
|
+
per katalog. BRISTER i spec/granskarblock → dispatcha om specifiern EN gång
|
|
166
|
+
enligt omdispatch-regeln; BRISTER i `state.json` är wrapperns egna filer och
|
|
167
|
+
rättas av wrappern själv, aldrig via omdispatch. Kvarstår brister, visa dem
|
|
168
|
+
och stanna. VARNING:ar visas men stoppar inget.
|
|
169
|
+
7. **Svara BLUF:** en rad per spec (rubrik + ev. flaggor), sedan granskarblocket
|
|
170
|
+
i sin helhet. Erbjud att lägga specarna på brädan via request-sync (🔍) — gör
|
|
171
|
+
det bara på uttrycklig begäran. När en post läggs på brädan: sätt status
|
|
172
|
+
`spec_synced`, döp om slug-katalogen till det myntade gid:t, notera sluggen
|
|
173
|
+
i `former_slug` och uppdatera `request_id` i `state.json`.
|
|
174
|
+
|
|
175
|
+
## Uppgift: `bedöm`
|
|
176
|
+
|
|
177
|
+
Kör prissättningsbenet: påverkansanalys om den saknas, sedan klass ∥ poäng med
|
|
178
|
+
oenighetsgrind.
|
|
179
|
+
|
|
180
|
+
1. **Grind — obligatorisk, aldrig överhoppbar.** Bedömning kräver en granskad
|
|
181
|
+
spec, och godkännandet bor i Asana:
|
|
182
|
+
- **Posten har gid:** läs Offer status via request-sync (huvudkonversationen)
|
|
183
|
+
omedelbart före körningen — aldrig från minnet eller en tidigare listning.
|
|
184
|
+
Endast `✅ Spec ok` (normaliserad på alfabetiska kärnan) släpper igenom.
|
|
185
|
+
Annan status: visa den och stanna. En inklistrad spec för en post som har
|
|
186
|
+
gid räknas inte — det är brädstatusen som gäller, läs den ändå.
|
|
187
|
+
- **Posten saknar gid MEN repot har `.haus/request.json`:** vägra — repot är
|
|
188
|
+
kopplat till en bräda, så en gid-lös post ska in via intaget och granskas
|
|
189
|
+
där, inte prissättas vid sidan av. Grinden är värd något bara om den inte
|
|
190
|
+
går att kliva runt.
|
|
191
|
+
- **Posten saknar gid OCH repot saknar brädkoppling:** fråga uttryckligen
|
|
192
|
+
"Är specen godkänd av en människa — och av vem?" via AskUserQuestion och
|
|
193
|
+
kör bara på ja med namn. Anta aldrig, och låt aldrig "specen ser klar ut"
|
|
194
|
+
eller ett tidigare samtal ersätta frågan.
|
|
195
|
+
- **Posten har gid men saknar lokal arkivkatalog:** stöds inte ännu
|
|
196
|
+
(hydrering från brädan kommer med batch-steget) — säg det och stanna;
|
|
197
|
+
granskarblocket går inte att återskapa ur Asana.
|
|
198
|
+
- Anteckna passerad grind i `state.json.approval` (manual-fallet med `by`).
|
|
199
|
+
Nästa `bedöm` läser om statusen igen.
|
|
200
|
+
2. **Formkontroll.** Kör `node <skillkatalog>/scripts/precheck.mjs <katalog> --impact`
|
|
201
|
+
(utan `--impact` om analysen ännu inte finns). En `spec.md` med mer än ett
|
|
202
|
+
specblock är en BRIST — dela posten enligt arkivregeln innan något dispatchas.
|
|
203
|
+
Övriga BRISTER → åtgärda ledet som brister (om spec: tillbaka till
|
|
204
|
+
speca-steget) innan något dyrt dispatchas.
|
|
205
|
+
3. **Analys.** Saknas `impact.md`, eller fäller precheck den på
|
|
206
|
+
underlagsproveniens (analysen gjordes mot en annan spec): kräver repo i
|
|
207
|
+
sessionen (annars stopp — en gissad analys är förbjuden av skillen själv).
|
|
208
|
+
Dispatcha `request-assessor` enligt promptmallen — specens granskarblock
|
|
209
|
+
skickas med som internt underlag (samma förtroendedomän; flaggorna där är
|
|
210
|
+
ofta analysens viktigaste ingång). Arkivera svaret som `impact.md`
|
|
211
|
+
(inklusive dess "## Till granskaren"-block — visa blocket för användaren,
|
|
212
|
+
aldrig till kundfält), lägg en `runs`-rad (`file: "impact.md"`,
|
|
213
|
+
`underlag_sha256` med hash för `spec.md`). Kör precheck igen med `--impact`.
|
|
214
|
+
4. **Prissättning.** Dispatcha `request-classifier` och `request-scorer`
|
|
215
|
+
**parallellt i samma meddelande** enligt promptmallarna. Ingen prislista,
|
|
216
|
+
inget repo, inga andra posters dokument i prompterna.
|
|
217
|
+
5. **Kontraktskontroll.** Strippa svansraderna (`MODELL:`, `LÄSTA:`, ev.
|
|
218
|
+
`AVVIKELSE:`) ur varje svar, skriv den rena JSON:en till en temporär fil och
|
|
219
|
+
kör `node <skillkatalog>/scripts/contract-check.mjs <svar.json> <spec.md> <impact.md> --expect-id <katalogens id>`.
|
|
220
|
+
BROTT → dispatcha om just den agenten EN gång enligt omdispatch-regeln;
|
|
221
|
+
kvarstår brott, visa dem och stanna utan att arkivera det svaret som giltigt
|
|
222
|
+
varv — men släng det inte: arkivera det som `<typ>-pricing-vN-avvisad.json`
|
|
223
|
+
(utanför N-räkningen) med kontrollens felrader i en runs-rad, så även
|
|
224
|
+
misslyckade körningar har kvitto. En `AVVIKELSE:`-rad visas alltid för
|
|
225
|
+
användaren.
|
|
226
|
+
6. **Dubbelkörning (driftvakt).** Körs BARA när inget av svaren är ett
|
|
227
|
+
stopputfall — stopproutningen i steg 7 går alltid först, en post som ändå
|
|
228
|
+
går till människa ska inte kosta fyra extra körningar. Trigger: klassningens
|
|
229
|
+
**utfall** är MEDIUM, LARGE eller OFFERT, ELLER poängen är ≥ 4 (poängen
|
|
230
|
+
multipliceras direkt till pris — en hög poäng på låg klass förtjänar samma
|
|
231
|
+
vakt). Dispatcha då `request-classifier` OCH `request-scorer` EN gång till,
|
|
232
|
+
samma prompter, nya isolerade körningar, parallellt i samma meddelande.
|
|
233
|
+
Kostnaden (ett extra par körningar) läggs medvetet exakt där pengarna står
|
|
234
|
+
på spel — poängen multipliceras direkt till pris och båda skillsens eget
|
|
235
|
+
acceptanstest är att två oberoende körningar ger samma resultat.
|
|
236
|
+
- Kör kontraktskontrollen (steg 5) på båda nya svaren först, med egen
|
|
237
|
+
omdispatch-budget (EN per agent).
|
|
238
|
+
- Jämför sedan deterministiskt:
|
|
239
|
+
`node <skillkatalog>/scripts/compare-classifications.mjs <klass1.json> <klass2.json>`
|
|
240
|
+
`node <skillkatalog>/scripts/compare-scores.mjs <poäng1.json> <poäng2.json>`
|
|
241
|
+
- **Båda LIKA** → körning 1-paret gäller: `double_run: "bekräftad"` på dess
|
|
242
|
+
runs-rader. Körning 2-filerna arkiveras som `...-pricing-vN-run2.json`
|
|
243
|
+
med SAMMA N som paret och `superseded_by` satt till motsvarande körning
|
|
244
|
+
1-fil. `-run2`-filer räknas ALDRIG när nästa N bestäms, och guard och
|
|
245
|
+
`detaljer` läser alltid filen utan suffix.
|
|
246
|
+
- **Någon OLIKA** → behandla posten som ESKALERA. Det beskriver routingen,
|
|
247
|
+
aldrig artefakten: ingen fil ändras, ingen JSON skrivs om, inget
|
|
248
|
+
ESKALERA-värde patchas in. Arkivera båda paren (körning 2 som `-run2`),
|
|
249
|
+
sätt `double_run: "olika"` och `superseded_by: null` på alla berörda
|
|
250
|
+
runs-rader, status `assessed`. Visa diffen (rad · körning 1 · körning 2)
|
|
251
|
+
för granskaren. Inget `customer`-block ur någon av körningarna får gå
|
|
252
|
+
till request-sync så länge `double_run` står på `"olika"`. Välj ALDRIG
|
|
253
|
+
en av körningarna själv och räkna ALDRIG fram ett medelvärde.
|
|
254
|
+
**Återställningen är ett nytt varv:** efter granskarens beslut (justera
|
|
255
|
+
underlaget, eller konstatera att en körning hade rätt) körs `bedöm` om —
|
|
256
|
+
spärren sitter på varvets rader, ett nytt varv med LIKA låser upp.
|
|
257
|
+
- Exit 2 från ett jämförelsescript är ett dispatcherfel (ojämförbara
|
|
258
|
+
körningar) — visa felet och stanna, rapportera aldrig LIKA.
|
|
259
|
+
7. **Oenighetsgrind klass ↔ poäng.** Deterministiska regler, i ordning:
|
|
260
|
+
- Endera svaret är ett stopputfall → hela posten routas enligt stopplistan
|
|
261
|
+
nedan. Det andra svaret arkiveras men markeras `superseded_by` mot
|
|
262
|
+
stopputfallets fil, och statusen blir `assessed`, aldrig `priced`.
|
|
263
|
+
- Båda levererar (klass + poäng): flagga oenighet i BLUF-tabellen mot
|
|
264
|
+
ankarbanden — klass TINY/SMALL med poäng ≥ 4, klass MEDIUM med poäng
|
|
265
|
+
≤ 0.75 eller ≥ 13, klass LARGE/OFFERT med poäng ≤ 3. Flaggan är
|
|
266
|
+
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
|
+
och `N` i `-pricing-vN.json` är gemensamt (nästa N = högsta befintliga för
|
|
269
|
+
någon av filtyperna + 1; `-run2`- och `-avvisad`-filer räknas aldrig;
|
|
270
|
+
guard-filer har en EGEN N-sekvens, skild från pricing). Uppdatera
|
|
271
|
+
`state.json.runs` med modell, effort, referens-hash och `underlag_sha256`
|
|
272
|
+
(hash för `spec.md` och `impact.md` som körningen såg) per fil, status →
|
|
273
|
+
`priced` (eller `assessed` vid stopputfall).
|
|
274
|
+
9. **Svara BLUF:** tabell `post · klass · poäng · utfall · konfidens`, en rad
|
|
275
|
+
per avvikelse värd att se (oenighetsflagga, ESKALERA, DELA_UPP,
|
|
276
|
+
INPUT_SAKNAS, AVVIKELSE-rader). JSON:en visas inte i chatten —
|
|
277
|
+
`detaljer <post>` gör det. Skriv-tillbaka till brädan sker via request-sync
|
|
278
|
+
i huvudkonversationen, bara på uttrycklig begäran — och det är den flytten
|
|
279
|
+
som tar posten ✅ Spec ok → ⚖️ Poängsättning redo för granskning enligt
|
|
280
|
+
syncens regler.
|
|
281
|
+
|
|
282
|
+
**Stopputfall routas, aldrig tolkas:**
|
|
283
|
+
|
|
284
|
+
- `DELA_UPP` → föreslå ny analysrunda per del; delarna blir egna kataloger med
|
|
285
|
+
`split` satt (`part`/`of`), och en del utan tak blir `OVER_SKALAN`, aldrig
|
|
286
|
+
DELA_UPP igen. Delposter ärver enligt skillen postens spec och skriver aldrig
|
|
287
|
+
en egen: förälderns `spec.md` och `granskarblock.md` KOPIERAS in i varje
|
|
288
|
+
delkatalog (som buntens granskarblock), medan `impact.md` skrivs per del av
|
|
289
|
+
den nya analysrundan.
|
|
290
|
+
- `OVER_SKALAN` → posten offereras separat; delas aldrig vidare (delningen görs
|
|
291
|
+
ett steg, skillens egen regel).
|
|
292
|
+
- `TILLBAKA_TILL_SPEC` / `KONCEPT` / `DIAGNOS` → tillbaka till spec-steget
|
|
293
|
+
respektive kunddialog; visa skälet.
|
|
294
|
+
- `ESKALERA` / `INPUT_SAKNAS` → visa skälet och stanna.
|
|
295
|
+
|
|
296
|
+
## Uppgift: `status`
|
|
297
|
+
|
|
298
|
+
Kör request-sync:s kö-läsning i huvudkonversationen för lägena
|
|
299
|
+
📥 Inkommet, 🔍 Spec redo för granskning, ✅ Spec ok och ⚖️ Poängsättning redo
|
|
300
|
+
för granskning. Svara som BLUF-tabell med **radnummer**: `nr · post · Offer
|
|
301
|
+
status`. Spara nr → gid-mappningen i sessionen (gid behöver inte visas i
|
|
302
|
+
tabellen) — numren är vad `godkänn`/`underkänn`/`bedöm`/`detaljer` refererar
|
|
303
|
+
till, och varje verb slår upp och agerar på **gid**, aldrig på numret självt.
|
|
304
|
+
Ingen JSON i chatten.
|
|
305
|
+
|
|
306
|
+
## Uppgifter: `godkänn` / `underkänn`
|
|
307
|
+
|
|
308
|
+
Genvägar till samma spak som i Asana — **Offer status på brädan förblir enda
|
|
309
|
+
beslutet**. Verben gör beslutet snabbare att verkställa, aldrig lättare att
|
|
310
|
+
missta:
|
|
311
|
+
|
|
312
|
+
- **Adressering:** radnummer ur senaste `status`-listningen i samma session
|
|
313
|
+
(slås upp till gid via den sparade mappningen), eller ett gid direkt. ALDRIG
|
|
314
|
+
fuzzy rubrikmatchning — fel post godkänd är värre än ett stopp. Finns ingen
|
|
315
|
+
färsk listning och inget gid: kör `status` först.
|
|
316
|
+
- **En post per verb-anrop.** "Godkänn 2 och 4" är två anrop i följd, vart och
|
|
317
|
+
ett läser och flyttar sin post. Aldrig batch-godkännande på "allt ser bra ut".
|
|
318
|
+
- **Lägesgrind för båda verben:** läs postens nuvarande Offer status via
|
|
319
|
+
request-sync först. Verben verkar bara från gransknings-lägena
|
|
320
|
+
(🔍 Spec redo för granskning, ⚖️ Poängsättning redo för granskning). Står
|
|
321
|
+
posten någon annanstans: visa status och stanna i stället för att gissa.
|
|
322
|
+
- **`godkänn <nr|gid>`:** 🔍 → ✅ Spec ok, respektive ⚖️ → 📤 Klar – till kund.
|
|
323
|
+
- **`underkänn <nr|gid> <skäl>`:** skäl är obligatoriskt — utan skäl, fråga
|
|
324
|
+
efter det. request-sync lägger skälet som kommentar på posten, bumpar
|
|
325
|
+
iterationsräknaren och flyttar status bakåt enligt sina bakåtflyttsregler.
|
|
326
|
+
- **Post utan bräda** (bara arkiv): `godkänn` antecknar
|
|
327
|
+
`approval: {source: "manual", date}` i `state.json` efter en uttrycklig
|
|
328
|
+
bekräftelsefråga. Säg tydligt att detta inte syns för någon annan än arkivet.
|
|
329
|
+
- Verben rör Offer status och kommentarer — aldrig Score, Class eller
|
|
330
|
+
beskrivningsfält.
|
|
331
|
+
|
|
332
|
+
## Uppgift: `detaljer`
|
|
333
|
+
|
|
334
|
+
`detaljer <gid|slug|nr> [steg]` — skriv ut arkiverade filer för posten: utan
|
|
335
|
+
steg en lista över vad som finns, med steg (`spec`, `granskarblock`, `analys`,
|
|
336
|
+
`klass`, `poäng`, `state`) filens innehåll rått. För `klass`/`poäng`: senaste
|
|
337
|
+
`-pricing-vN.json` utan suffix (aldrig `-run2`/`-avvisad`).
|
|
338
|
+
|
|
339
|
+
## Mekaniska kontroller: `scripts/`
|
|
340
|
+
|
|
341
|
+
Sex deterministiska script (fyra kontroller + `lib.mjs` med de delade
|
|
342
|
+
byggstenarna + `ref-hash.mjs` för stabil hashning) ersätter modellkörningar
|
|
343
|
+
där kontrollen är ren form —
|
|
344
|
+
billigare, snabbare och utan omdömesglidning. Varje script bär sha256 för de
|
|
345
|
+
referensfiler vars regler det speglar och VARNAR när en katalogbump ändrat dem
|
|
346
|
+
(kontraktsdrift ska upptäckas av en check, inte av en kund):
|
|
347
|
+
|
|
348
|
+
- `scripts/precheck.mjs <arkivkatalog> [--impact]` — formkontroll FÖRE dyra
|
|
349
|
+
dispatcher: rubriker, exakt ett specblock, 2–5 punkter, förbjuden vokabulär,
|
|
350
|
+
granskarblocksrader, state.json-fält. Skiljer på BRISTER (exit 1, stoppar)
|
|
351
|
+
och VARNING (heuristik — visas, stoppar inte).
|
|
352
|
+
- `scripts/contract-check.mjs <run.json> <spec.md> [impact.md] [--expect-id <id>]`
|
|
353
|
+
— kontraktskontroll av classify/score-JSON EFTER körning: enums,
|
|
354
|
+
konfidensintervall, poängupplösning, inringningens närvaro (även per
|
|
355
|
+
alternativ; under-A1-fallet med floor null och tak A1 är giltigt), belägg/
|
|
356
|
+
driver-citat som ordagranna substrängar av underlaget, protokollterm-
|
|
357
|
+
svartlista i kundtextfält, sluten kuvertnyckellista, request_id mot katalogen.
|
|
358
|
+
INPUT_SAKNAS kräver saknas-lista i stället för frågerader.
|
|
359
|
+
- `scripts/compare-classifications.mjs <a.json> <b.json>` — deterministisk
|
|
360
|
+
dubbelkörningsdiff: utfall, basklass, varje frågas svar och konfidensens sida
|
|
361
|
+
om 0.8-gränsen. Skriver `LIKA` eller `OLIKA` + diffrader, exit 0/1; exit 2 =
|
|
362
|
+
ojämförbara körningar (dispatcherfel, aldrig LIKA).
|
|
363
|
+
- `scripts/compare-scores.mjs <a.json> <b.json>` — samma för poäng: outcome,
|
|
364
|
+
score, confidence, golv- och takankare. Aldrig prosa. Samma exit-semantik.
|
|
365
|
+
Båda compare-scripten kräver mode `pricing`; guard-jämförelser är en medveten
|
|
366
|
+
utbyggnad i guard-steget.
|
|
367
|
+
|
|
368
|
+
Kontrollerna bedömer aldrig innehåll och returnerar aldrig protokollutfall —
|
|
369
|
+
en post som passerar kan fortfarande stoppas av protokollen själva, och det är
|
|
370
|
+
deras jobb. Vid script-fel (exit 2 eller krasch): visa felet och stanna, kör
|
|
371
|
+
aldrig vidare som om kontrollen passerat.
|
|
372
|
+
|
|
373
|
+
## Dispatch-kontrakt (gäller alla fyra agenterna)
|
|
374
|
+
|
|
375
|
+
- Prompten är självbärande: agenten ser inte den här konversationen.
|
|
376
|
+
- **Promptmallar, etiketterna ordagrant:**
|
|
377
|
+
|
|
378
|
+
`request-specifier`:
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
ÖNSKEMÅL:
|
|
382
|
+
<rått önskemål, ordagrant>
|
|
383
|
+
|
|
384
|
+
KÄLLOR:
|
|
385
|
+
<manifest med absoluta sökvägar, eller "inga — allt blir antaganden">
|
|
386
|
+
|
|
387
|
+
REPO-ROT: <absolut sökväg eller "saknas">
|
|
388
|
+
ESKALERAD: <ja|nej>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`request-assessor`:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
REPO-ROT: <absolut sökväg>
|
|
395
|
+
|
|
396
|
+
SPEC (godkänd):
|
|
397
|
+
<spec.md bytetroget>
|
|
398
|
+
|
|
399
|
+
GRANSKARBLOCK (internt underlag, följer aldrig med i analysens kundtext):
|
|
400
|
+
<granskarblock.md bytetroget>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`request-classifier` / `request-scorer`:
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
REQUEST_ID: <gid eller slug>
|
|
407
|
+
MODE: <pricing|guard>
|
|
408
|
+
DELPOST: <nej | ja, del N av M>
|
|
409
|
+
|
|
410
|
+
SPEC:
|
|
411
|
+
<spec.md bytetroget>
|
|
412
|
+
|
|
413
|
+
PÅVERKANSANALYS:
|
|
414
|
+
<impact.md bytetroget>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
(guard-läge lägger till `FULLPLAN:` och `TIDIGARE_KLASSNING:` /
|
|
418
|
+
`TIDIGARE_POANG:` — guard-kommandot kommer i ett senare steg.)
|
|
419
|
+
|
|
420
|
+
- **Svansrader efter huvudinnehållet**, i denna ordning: `MODELL: <modell-id>`
|
|
421
|
+
(alla agenter), `LÄSTA: <kommaseparerade absoluta sökvägar eller "inga">`
|
|
422
|
+
(endast classifier/scorer — deras Read är promptbegränsad, raden gör regeln
|
|
423
|
+
granskbar), `AVVIKELSE: <text>` (endast om något i prompten bröt mot agentens
|
|
424
|
+
regler, t.ex. en prislista i underlaget). Svansraderna strippas före varje
|
|
425
|
+
maskinell kontroll och före arkivering; MODELL antecknas i runs-raden,
|
|
426
|
+
LÄSTA verifieras ligga under agentens tillåtna katalog, AVVIKELSE visas
|
|
427
|
+
alltid för användaren.
|
|
428
|
+
- Agentens svar i övrigt är rådata enligt skillens kontrakt (prosa-dokument
|
|
429
|
+
eller JSON), ingen kommentar runt om.
|
|
430
|
+
- **Omdispatch (max EN per agent och varv):** skicka HELA ursprungsprompten
|
|
431
|
+
oförändrad, följd av ett block:
|
|
432
|
+
```
|
|
433
|
+
RÄTTNINGAR:
|
|
434
|
+
<felraderna från kontrollen, citerade ordagrant>
|
|
435
|
+
```
|
|
436
|
+
En ny subagent minns inget — enbart felrader utan underlag är obrukbart.
|
|
437
|
+
|
|
438
|
+
## Använd inte när
|
|
439
|
+
|
|
440
|
+
- Användaren vill köra en enskild skill fristående ("skriv bara en spec") —
|
|
441
|
+
invokera den skillen direkt.
|
|
442
|
+
- Frågan gäller Asana-uppsättning ("koppla repot") — det är request-sync:s
|
|
443
|
+
uppsättningsflöde, kör den direkt.
|
|
444
|
+
- Guard-läget ("kör guarden", före första kodrad) — körs tills vidare via
|
|
445
|
+
request-classify/request-score direkt; ett `guard`-kommando kommer i ett
|
|
446
|
+
senare steg.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministisk dubbelkörningsdiff för klassningar (driftvakten på MEDIUM+).
|
|
3
|
+
* Jämför utfall, basklass, varje frågas svar och vilken sida om KONFIDENSGRÄNS
|
|
4
|
+
* (0.8) konfidensen ligger, mellan två oberoende körningar på samma underlag.
|
|
5
|
+
* Prosafält (belägg, motiveringar) jämförs INTE — två korrekta körningar
|
|
6
|
+
* formulerar sig olika, det är svaren som ska vara stabila. Konfidensens SIDA
|
|
7
|
+
* om gränsen är däremot inte prosa: den avgör ESKALERA enligt protokollet.
|
|
8
|
+
*
|
|
9
|
+
* Skriver LIKA (exit 0) eller OLIKA + diffrader (exit 1) på stdout; varningar
|
|
10
|
+
* går på stderr så en verdiktparser aldrig sväljer dem. Vid OLIKA väljer
|
|
11
|
+
* dispatchern ALDRIG en körning och räknar ALDRIG medelvärde — posten
|
|
12
|
+
* eskaleras till granskaren med båda körningarna.
|
|
13
|
+
*
|
|
14
|
+
* Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
|
|
15
|
+
* rälsen bor i lib.mjs och delas med compare-scores.
|
|
16
|
+
*
|
|
17
|
+
* Användning: node compare-classifications.mjs <körning1.json> <körning2.json>
|
|
18
|
+
*/
|
|
19
|
+
import { loadRunPair, refDriftWarning, reportDiffs } from './lib.mjs'
|
|
20
|
+
|
|
21
|
+
// KONFIDENSGRANS speglar klassningsinstruktionen — varna (stderr) vid katalogdrift.
|
|
22
|
+
const REF_SHA = '811f0bff62c749255e86da7ea8db0ea868ebc9df14262f9889101c110d9d14ff'
|
|
23
|
+
{
|
|
24
|
+
const w = refDriftWarning('request-classify/references/klassningsinstruktion.md', REF_SHA)
|
|
25
|
+
if (w) console.error(`VARNING: ${w} — kontrollera KONFIDENSGRANS`)
|
|
26
|
+
}
|
|
27
|
+
const KONFIDENSGRANS = 0.8
|
|
28
|
+
|
|
29
|
+
const { a: docA, b: docB } = loadRunPair(
|
|
30
|
+
'användning: node compare-classifications.mjs <körning1.json> <körning2.json>',
|
|
31
|
+
)
|
|
32
|
+
const a = docA.classification
|
|
33
|
+
const b = docB.classification
|
|
34
|
+
if (!a || !b) {
|
|
35
|
+
console.error('minst en fil saknar classification-nyckeln — är båda classify-körningar?')
|
|
36
|
+
process.exit(2)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Frågelagret är jämförelsens kärna — utan kompletta, unikt id-satta rader kan
|
|
40
|
+
// en äkta oenighet tyst försvinna. Då vägrar vi hellre än gissar.
|
|
41
|
+
const validateFragor = (rows, label) => {
|
|
42
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
43
|
+
console.error(`${label}: fragor saknas eller är tom — kan inte jämföra`)
|
|
44
|
+
process.exit(2)
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set()
|
|
47
|
+
for (const f of rows) {
|
|
48
|
+
if (typeof f?.id !== 'string' || f.id.trim() === '') {
|
|
49
|
+
console.error(`${label}: en frågerad saknar id — kan inte jämföra`)
|
|
50
|
+
process.exit(2)
|
|
51
|
+
}
|
|
52
|
+
if (seen.has(f.id)) {
|
|
53
|
+
console.error(`${label}: dubblett-id "${f.id}" — kan inte jämföra`)
|
|
54
|
+
process.exit(2)
|
|
55
|
+
}
|
|
56
|
+
seen.add(f.id)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
validateFragor(a.fragor, 'körning 1')
|
|
60
|
+
validateFragor(b.fragor, 'körning 2')
|
|
61
|
+
|
|
62
|
+
const show = (v) => JSON.stringify(v ?? null)
|
|
63
|
+
const normSvar = (v) =>
|
|
64
|
+
String(v ?? '')
|
|
65
|
+
.trim()
|
|
66
|
+
.toUpperCase()
|
|
67
|
+
|
|
68
|
+
const diffs = []
|
|
69
|
+
if (a.utfall !== b.utfall) diffs.push(`utfall: ${show(a.utfall)} ↔ ${show(b.utfall)}`)
|
|
70
|
+
if ((a.basklass ?? null) !== (b.basklass ?? null))
|
|
71
|
+
diffs.push(`basklass: ${show(a.basklass)} ↔ ${show(b.basklass)}`)
|
|
72
|
+
|
|
73
|
+
const byId = (rows) => new Map(rows.map((f) => [f.id, f]))
|
|
74
|
+
const mapA = byId(a.fragor)
|
|
75
|
+
const mapB = byId(b.fragor)
|
|
76
|
+
for (const id of new Set([...mapA.keys(), ...mapB.keys()])) {
|
|
77
|
+
const fa = mapA.get(id)
|
|
78
|
+
const fb = mapB.get(id)
|
|
79
|
+
if (!fa || !fb) {
|
|
80
|
+
// Körningarna stannade på olika steg — samma fakta gav olika väg.
|
|
81
|
+
diffs.push(`fråga ${id}: besvarad i ${fa ? 'körning 1' : 'körning 2'} men inte i den andra`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (normSvar(fa.svar) !== normSvar(fb.svar))
|
|
85
|
+
diffs.push(`fråga ${id}: ${show(fa.svar)} ↔ ${show(fb.svar)}`)
|
|
86
|
+
const sideA = (fa.konfidens ?? 0) >= KONFIDENSGRANS
|
|
87
|
+
const sideB = (fb.konfidens ?? 0) >= KONFIDENSGRANS
|
|
88
|
+
if (sideA !== sideB)
|
|
89
|
+
diffs.push(
|
|
90
|
+
`fråga ${id}: konfidens på olika sida om ${KONFIDENSGRANS} (${show(fa.konfidens)} ↔ ${show(fb.konfidens)})`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
reportDiffs(diffs)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministisk dubbelkörningsdiff för poängsättningar (driftvakten på
|
|
3
|
+
* MEDIUM+). Poängen har större driftyta än klassens enum och multipliceras
|
|
4
|
+
* direkt till pris — request-score:s eget acceptanstest är att två oberoende
|
|
5
|
+
* körningar ger samma poäng. Jämförs: outcome, score, confidence (enum, inte
|
|
6
|
+
* prosa) och inringningens golv- och takankare. Prosa (why, drivers,
|
|
7
|
+
* rationale) jämförs ALDRIG.
|
|
8
|
+
*
|
|
9
|
+
* Skriver LIKA (exit 0) eller OLIKA + diffrader (exit 1) på stdout; varningar
|
|
10
|
+
* går på stderr. Vid OLIKA väljer dispatchern ALDRIG en körning och räknar
|
|
11
|
+
* ALDRIG medelvärde — posten eskaleras till granskaren med båda inringningarna.
|
|
12
|
+
*
|
|
13
|
+
* Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
|
|
14
|
+
* rälsen bor i lib.mjs och delas med compare-classifications.
|
|
15
|
+
*
|
|
16
|
+
* Användning: node compare-scores.mjs <körning1.json> <körning2.json>
|
|
17
|
+
*/
|
|
18
|
+
import { loadRunPair, refDriftWarning, reportDiffs } from './lib.mjs'
|
|
19
|
+
|
|
20
|
+
// Jämförelsefälten speglar poängskalans kontrakt — varna (stderr) vid katalogdrift.
|
|
21
|
+
const REF_SHA = 'cb5a6c221b5d39c445ca31a427fd3f96c648388ac5441c22ea2ea5f72e1f8e31'
|
|
22
|
+
{
|
|
23
|
+
const w = refDriftWarning('request-score/references/poangskala.md', REF_SHA)
|
|
24
|
+
if (w) console.error(`VARNING: ${w}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { a, b } = loadRunPair('användning: node compare-scores.mjs <körning1.json> <körning2.json>')
|
|
28
|
+
if (a.outcome === undefined || b.outcome === undefined) {
|
|
29
|
+
console.error('minst en fil saknar outcome-nyckeln — är båda score-körningar?')
|
|
30
|
+
process.exit(2)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const show = (v) => JSON.stringify(v ?? null)
|
|
34
|
+
const diffs = []
|
|
35
|
+
if (a.outcome !== b.outcome) diffs.push(`outcome: ${show(a.outcome)} ↔ ${show(b.outcome)}`)
|
|
36
|
+
// confidence är ett enum (hog/medel/lag), inte prosa — samma poäng med olika
|
|
37
|
+
// säkerhet är information granskaren ska se, precis som klassningens konfidenssida.
|
|
38
|
+
if (a.confidence !== b.confidence)
|
|
39
|
+
diffs.push(`confidence: ${show(a.confidence)} ↔ ${show(b.confidence)}`)
|
|
40
|
+
if (a.outcome === 'POANG' && b.outcome === 'POANG') {
|
|
41
|
+
if (a.score !== b.score) diffs.push(`score: ${show(a.score)} ↔ ${show(b.score)}`)
|
|
42
|
+
const fa = a.bracket?.floor?.anchor ?? null
|
|
43
|
+
const fb = b.bracket?.floor?.anchor ?? null
|
|
44
|
+
const ta = a.bracket?.ceiling?.anchor ?? null
|
|
45
|
+
const tb = b.bracket?.ceiling?.anchor ?? null
|
|
46
|
+
if (fa !== fb) diffs.push(`bracket.floor.anchor: ${show(fa)} ↔ ${show(fb)}`)
|
|
47
|
+
if (ta !== tb) diffs.push(`bracket.ceiling.anchor: ${show(ta)} ↔ ${show(tb)}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
reportDiffs(diffs)
|