@haus-tech/haus-workflow 4.9.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/cli.js +847 -499
- package/library/catalog/manifest.json +97 -34
- package/library/global/agents/request-assessor.md +2 -2
- package/library/global/agents/request-scorer.md +13 -8
- package/library/global/agents/request-specifier-eskalerad.md +1 -1
- package/library/global/agents/request-specifier.md +1 -1
- package/library/global/skills/request/SKILL.md +675 -363
- package/library/global/skills/request/scripts/archive.mjs +1376 -0
- package/library/global/skills/request/scripts/check-lasta.mjs +64 -0
- package/library/global/skills/request/scripts/compare-scores.mjs +23 -15
- package/library/global/skills/request/scripts/contract-check.mjs +108 -36
- package/library/global/skills/request/scripts/lib.mjs +66 -0
- package/library/global/skills/request/scripts/precheck.mjs +30 -27
- package/library/global/skills/request/scripts/report.mjs +571 -0
- package/library/global/skills/request/scripts/resolve-anchors.mjs +243 -0
- package/library/global/skills/request-assess-impact/SKILL.md +10 -10
- package/library/global/skills/request-score/SKILL.md +19 -10
- package/library/global/skills/request-sync/SKILL.md +4 -4
- package/library/global/skills/request-write-spec/SKILL.md +6 -6
- package/library/global/skills/request-write-spec/references/specformat.md +2 -2
- package/package.json +1 -1
- package/library/global/agents/request-classifier.md +0 -55
- package/library/global/skills/request/scripts/compare-classifications.mjs +0 -94
- package/library/global/skills/request/scripts/quote-repair.mjs +0 -166
- package/library/global/skills/request-classify/SKILL.md +0 -79
- package/library/global/skills/request-classify/references/klassningsinstruktion.md +0 -152
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deterministisk kontroll av scorerns LÄSTA-svansrad (ADR-0099). Läs-scopet
|
|
4
|
+
* är en promptregel, inte en fysisk spärr — raden gör den granskbar, och det
|
|
5
|
+
* här scriptet gör granskningen mekanisk i stället för en instruktion som är
|
|
6
|
+
* lätt att missa (granskningens fynd på PR #470).
|
|
7
|
+
*
|
|
8
|
+
* Tillåten mängd: EXAKT underlagens två sökvägar (spec.md, impact.md — båda
|
|
9
|
+
* OBLIGATORISKA: scorern måste ha läst sitt underlag), plus valfria filer
|
|
10
|
+
* under skillens egen katalog (`skills/request-score/` under Claude-
|
|
11
|
+
* konfigkatalogen, referensfilen). Allt annat — körningskatalogens övriga
|
|
12
|
+
* filer, repot, vad som helst — är ett kontraktsbrott: behandla exit 1 som
|
|
13
|
+
* BROTT i bedöm steg 5 (omdispatch EN gång, sedan stopp).
|
|
14
|
+
*
|
|
15
|
+
* Användning: node check-lasta.mjs "<LÄSTA-radens innehåll>" <spec.md> <impact.md>
|
|
16
|
+
* Innehållet är det som står EFTER "LÄSTA: ": kommaseparerade absoluta
|
|
17
|
+
* sökvägar, eller literalen "inga".
|
|
18
|
+
* Exit 0 = ok, 1 = kontraktsbrott (BROTT-rader på stdout), 2 = användningsfel.
|
|
19
|
+
*/
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
|
|
22
|
+
import { claudeDir } from './lib.mjs'
|
|
23
|
+
|
|
24
|
+
const [lasta, specPath, impactPath] = process.argv.slice(2)
|
|
25
|
+
if (lasta == null || !specPath || !impactPath) {
|
|
26
|
+
console.error('användning: node check-lasta.mjs "<LÄSTA-innehåll>" <spec.md> <impact.md>')
|
|
27
|
+
process.exit(2)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const problems = []
|
|
31
|
+
const spec = path.resolve(specPath)
|
|
32
|
+
const impact = path.resolve(impactPath)
|
|
33
|
+
// Skillkatalogen med avslutande separator, så prefixtestet aldrig släpper
|
|
34
|
+
// igenom en granne som `request-score-annat/`.
|
|
35
|
+
const skillDir = path.resolve(claudeDir(), 'skills', 'request-score') + path.sep
|
|
36
|
+
|
|
37
|
+
const lista =
|
|
38
|
+
lasta.trim() === 'inga'
|
|
39
|
+
? []
|
|
40
|
+
: lasta
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((s) => s.trim())
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
|
|
45
|
+
const lastaResolved = lista.map((p) => path.resolve(p))
|
|
46
|
+
|
|
47
|
+
for (const kravd of [spec, impact])
|
|
48
|
+
if (!lastaResolved.includes(kravd))
|
|
49
|
+
problems.push(`obligatoriskt underlag olästt: ${kravd} saknas på LÄSTA-raden`)
|
|
50
|
+
|
|
51
|
+
for (const p of lastaResolved)
|
|
52
|
+
if (p !== spec && p !== impact && !p.startsWith(skillDir))
|
|
53
|
+
problems.push(
|
|
54
|
+
`otillåten sökväg på LÄSTA-raden: ${p} — tillåtet är exakt underlagens två ` +
|
|
55
|
+
'filer plus skillens egen katalog',
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (problems.length === 0) {
|
|
59
|
+
console.log('OK: LÄSTA-raden håller sig inom läs-scopet')
|
|
60
|
+
process.exit(0)
|
|
61
|
+
}
|
|
62
|
+
console.log(`BROTT (${problems.length}):`)
|
|
63
|
+
for (const p of problems) console.log(`- ${p}`)
|
|
64
|
+
process.exit(1)
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Deterministisk dubbelkörningsdiff för poängsättningar (driftvakten på
|
|
3
3
|
* MEDIUM+). Poängen har större driftyta än klassens enum och multipliceras
|
|
4
4
|
* direkt till pris — request-score:s eget acceptanstest är att två oberoende
|
|
5
|
-
* körningar ger samma poäng.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* körningar ger samma poäng. Jämförelsefälten är outcome, score och
|
|
6
|
+
* inringningens golv- och takankare. Prosa (why, drivers, rationale) jämförs
|
|
7
|
+
* ALDRIG. Confidence (enum, inte prosa) visas alltid i utskriften, men fäller
|
|
8
|
+
* bara ett par utanför toleransen, se nedan.
|
|
8
9
|
*
|
|
9
|
-
* Toleransregel: en poängskillnad ≤ 0,5 med samma outcome
|
|
10
|
-
*
|
|
11
|
-
* överlappar eller angränsar; icke-angränsande band är hård diff även
|
|
12
|
-
* poäng. LIKA_TOLERANS hör till LIKA-familjen (exit 0) med
|
|
10
|
+
* Toleransregel: en poängskillnad ≤ 0,5 med samma outcome är prisbrus, inte
|
|
11
|
+
* oenighet, oavsett confidence. En ankarskillnad tolereras dessutom bara när
|
|
12
|
+
* banden överlappar eller angränsar; icke-angränsande band är hård diff även
|
|
13
|
+
* vid samma poäng. LIKA_TOLERANS hör till LIKA-familjen (exit 0) med
|
|
13
14
|
* en deterministisk vinnare (högsta poängen, skyddar marginalen). Den
|
|
14
15
|
* skriver aldrig om en fil och räknar aldrig fram ett medelvärde.
|
|
15
16
|
*
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
* båda inringningarna.
|
|
20
21
|
*
|
|
21
22
|
* Exit 2 = jämförelsen kan inte göras alls (dispatcherns fel, aldrig LIKA) —
|
|
22
|
-
* rälsen bor i lib.mjs
|
|
23
|
+
* rälsen bor i lib.mjs (delades tidigare med compare-classifications,
|
|
24
|
+
* borttagen med klassaxeln i ADR-0098).
|
|
23
25
|
*
|
|
24
26
|
* Användning: node compare-scores.mjs <körning1.json> <körning2.json>
|
|
25
27
|
*/
|
|
@@ -41,14 +43,11 @@ if (a.outcome === undefined || b.outcome === undefined) {
|
|
|
41
43
|
const show = (v) => JSON.stringify(v ?? null)
|
|
42
44
|
const diffs = []
|
|
43
45
|
if (a.outcome !== b.outcome) diffs.push(`outcome: ${show(a.outcome)} ↔ ${show(b.outcome)}`)
|
|
44
|
-
// confidence är ett enum (hog/medel/lag), inte prosa — samma poäng med olika
|
|
45
|
-
// säkerhet är information granskaren ska se, precis som klassningens konfidenssida.
|
|
46
|
-
if (a.confidence !== b.confidence)
|
|
47
|
-
diffs.push(`confidence: ${show(a.confidence)} ↔ ${show(b.confidence)}`)
|
|
48
46
|
// Toleransregel (beslut 2026-08-28, ADR-0078): en poängskillnad inom 0,5 med
|
|
49
|
-
// samma outcome
|
|
50
|
-
// eskaleringar gällde Δ = 0,25 och kostade ett helt nytt varv
|
|
51
|
-
// poängen gäller (skyddar marginalen); ett medelvärde räknas
|
|
47
|
+
// samma outcome är prisbrus, inte oenighet, oavsett confidence. Båda
|
|
48
|
+
// pilotfallens eskaleringar gällde Δ = 0,25 och kostade ett helt nytt varv
|
|
49
|
+
// styck. Högsta poängen gäller (skyddar marginalen); ett medelvärde räknas
|
|
50
|
+
// fortfarande aldrig.
|
|
52
51
|
const TOLERANS = 0.5
|
|
53
52
|
|
|
54
53
|
/**
|
|
@@ -75,9 +74,16 @@ const nastanSammaBand = (fa, ta, fb, tb) => {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
const tolerans = []
|
|
77
|
+
// Konfidensordet (hog/medel/lag) är en självrapporterad etikett. Den visas
|
|
78
|
+
// alltid i utskriften, men fäller aldrig ensam ett par vars poäng ligger inom
|
|
79
|
+
// toleransen: identisk poäng med medel↔hog routade en skarp post till ESKALERA
|
|
80
|
+
// på enbart ordet (ADR-0092). Utanför toleransen är den hård diff som förut.
|
|
81
|
+
const konfDiff =
|
|
82
|
+
a.confidence !== b.confidence ? `confidence: ${show(a.confidence)} ↔ ${show(b.confidence)}` : null
|
|
78
83
|
if (a.outcome === 'POANG' && b.outcome === 'POANG') {
|
|
79
84
|
const delta = Math.abs(a.score - b.score)
|
|
80
85
|
const inomTolerans = Number.isFinite(delta) && delta <= TOLERANS
|
|
86
|
+
if (konfDiff) (inomTolerans ? tolerans : diffs).push(konfDiff)
|
|
81
87
|
if (a.score !== b.score)
|
|
82
88
|
(inomTolerans ? tolerans : diffs).push(`score: ${show(a.score)} ↔ ${show(b.score)}`)
|
|
83
89
|
const fa = a.bracket?.floor?.anchor ?? null
|
|
@@ -88,6 +94,8 @@ if (a.outcome === 'POANG' && b.outcome === 'POANG') {
|
|
|
88
94
|
if (fa !== fb) ankarDiffs.push(`bracket.floor.anchor: ${show(fa)} ↔ ${show(fb)}`)
|
|
89
95
|
if (ta !== tb) ankarDiffs.push(`bracket.ceiling.anchor: ${show(ta)} ↔ ${show(tb)}`)
|
|
90
96
|
;(inomTolerans && nastanSammaBand(fa, ta, fb, tb) ? tolerans : diffs).push(...ankarDiffs)
|
|
97
|
+
} else if (konfDiff) {
|
|
98
|
+
diffs.push(konfDiff)
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
if (diffs.length === 0 && tolerans.length > 0) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Mekanisk efterhandskontroll av en
|
|
4
|
-
*
|
|
3
|
+
* Mekanisk efterhandskontroll av en score-JSON mot sitt underlag.
|
|
4
|
+
* Classify-grenen finns kvar ENBART för äldre arkiv (före ADR-0098, då
|
|
5
|
+
* klassaxeln togs bort): inga nya classify-svar produceras, men arkiverade
|
|
6
|
+
* ska fortsätta gå att kontrollera. Fångar det starka modeller ändå slarvar
|
|
7
|
+
* med i lång kontext:
|
|
5
8
|
* parafraserade "ordagranna" citat, läckta protokolltermer i kundtext, brutna
|
|
6
9
|
* enum- och upplösningsregler. Bedömer aldrig innehållets rimlighet — bara
|
|
7
10
|
* kontraktet. Vid BROTT: dispatcha agenten om EN gång med felraderna, sedan
|
|
@@ -13,15 +16,41 @@
|
|
|
13
16
|
*/
|
|
14
17
|
import { readFileSync } from 'node:fs'
|
|
15
18
|
|
|
16
|
-
import { refDriftWarning, norm, quoteOk } from './lib.mjs'
|
|
19
|
+
import { refDriftWarning, norm, quoteOk, CLASS_UTFALL, SCORE_OUTCOMES } from './lib.mjs'
|
|
17
20
|
|
|
18
21
|
const argv = process.argv.slice(2)
|
|
19
22
|
let expectId = null
|
|
20
23
|
const idFlag = argv.indexOf('--expect-id')
|
|
21
24
|
if (idFlag !== -1) {
|
|
22
|
-
|
|
25
|
+
// Ett `--expect-id` utan ett efterföljande värde (sista argumentet, eller
|
|
26
|
+
// direkt följt av en ny flagga) fick tidigare `expectId = undefined`, och
|
|
27
|
+
// `undefined != null` är `false` i JS, så kontrollen mot `request_id` längre
|
|
28
|
+
// ner hoppades då tyst över helt, exakt som om flaggan aldrig angetts.
|
|
29
|
+
// `--expect-id` är den ENDA spärren mot att arkivera en körning mot fel
|
|
30
|
+
// post (avgörande i `utvärdera`, se SKILL.md), så ett värdelöst anrop får
|
|
31
|
+
// aldrig tolkas som "ingen kontroll begärd". Det är nästan säkert ett
|
|
32
|
+
// dispatcherfel som ska stanna hårt (exit 2), inte ett tyst OK.
|
|
33
|
+
const v = argv[idFlag + 1]
|
|
34
|
+
if (v === undefined || v.startsWith('--')) {
|
|
35
|
+
console.error('--expect-id kräver ett värde')
|
|
36
|
+
process.exit(2)
|
|
37
|
+
}
|
|
38
|
+
expectId = v
|
|
23
39
|
argv.splice(idFlag, 2)
|
|
24
40
|
}
|
|
41
|
+
// En felstavad flaggform (t.ex. `--expectid`, bindestrecket i "expect-id"
|
|
42
|
+
// tappat) matchar aldrig `--expect-id` ovan och blir kvar i `argv` som ett
|
|
43
|
+
// obehandlat argument. Utan den här kontrollen landade den tyst som
|
|
44
|
+
// `impactPath` (eller ströks av destructuringen om positionen inte räckte
|
|
45
|
+
// till) och kontrollen mot `request_id` kördes aldrig, samma
|
|
46
|
+
// fail-öppet-brist som det värdelösa fallet ovan, bara via ett annat
|
|
47
|
+
// missförstått flaggnamn. Varje kvarvarande `--`-token är alltså ett
|
|
48
|
+
// användningsfel, aldrig en gissning om vad anroparen menade.
|
|
49
|
+
const okandFlagga = argv.find((a) => a.startsWith('--'))
|
|
50
|
+
if (okandFlagga !== undefined) {
|
|
51
|
+
console.error(`okänd flagga: ${okandFlagga} (giltig: --expect-id <id>)`)
|
|
52
|
+
process.exit(2)
|
|
53
|
+
}
|
|
25
54
|
const [jsonPath, specPath, impactPath] = argv
|
|
26
55
|
if (!jsonPath || !specPath) {
|
|
27
56
|
console.error(
|
|
@@ -37,9 +66,10 @@ const warnings = []
|
|
|
37
66
|
// i referensfilerna. Pinsen är markör-strippade (stabila över haus-releaser,
|
|
38
67
|
// som stämplar om filer med paketversionen) och sökvägarna hedrar
|
|
39
68
|
// CLAUDE_CONFIG_DIR — se lib.mjs.
|
|
69
|
+
// klassningsinstruktion.md togs bort med klassaxeln (ADR-0098): classify-
|
|
70
|
+
// grenen kontrollerar bara arkiverade filer och behöver ingen levande
|
|
71
|
+
// referensfil att drifta mot.
|
|
40
72
|
const EXPECTED_REFS = {
|
|
41
|
-
'request-classify/references/klassningsinstruktion.md':
|
|
42
|
-
'811f0bff62c749255e86da7ea8db0ea868ebc9df14262f9889101c110d9d14ff',
|
|
43
73
|
'request-score/references/poangskala.md':
|
|
44
74
|
'cb5a6c221b5d39c445ca31a427fd3f96c648388ac5441c22ea2ea5f72e1f8e31',
|
|
45
75
|
}
|
|
@@ -66,8 +96,8 @@ try {
|
|
|
66
96
|
process.exit(2)
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
// quoteOk kommer från lib.mjs
|
|
70
|
-
//
|
|
99
|
+
// quoteOk kommer från lib.mjs; bind källan lokalt så befintliga anropsställen
|
|
100
|
+
// (`ok(f.belagg)` osv.) förblir oförändrade.
|
|
71
101
|
const ok = (q) => quoteOk(q, source)
|
|
72
102
|
|
|
73
103
|
/** Alla strängvärden ur ett godtyckligt värde (drivers kan vara objekt). */
|
|
@@ -106,12 +136,48 @@ const CUSTOMER_BLACKLIST = [
|
|
|
106
136
|
// Ankar-id (A1–A17) kolliderar strukturellt med pappersformat (A3/A4/A5) och
|
|
107
137
|
// går inte att regexa isär — heuristik, alltså varning i stället för brott.
|
|
108
138
|
const ANCHOR_RE = /(?<![\p{L}\d_])A(?:1[0-7]|[1-9])(?![\p{L}\d_])/u
|
|
139
|
+
// Vardagsidiom där "poängen" är vanlig svenska, inte skalans mekanik.
|
|
140
|
+
// Maskas bort FÖRE svartlistan så att "hela poängen med posten" går fri
|
|
141
|
+
// medan "poängen är 5" och "ger 5 poäng" fortfarande fälls.
|
|
142
|
+
//
|
|
143
|
+
// Undantaget är snävat till den STARKA idiom-markören, inte till talformen.
|
|
144
|
+
// Det observerade fallet som motiverade hela undantaget är "två fönster är
|
|
145
|
+
// hela poängen med posten", och "hela poängen med/i" är en konstruktion som
|
|
146
|
+
// bara förekommer idiomatiskt (syftet med något). Mekaniken uttrycks
|
|
147
|
+
// aldrig så: den säger "poängen är 5" eller "poängen med posten är 5",
|
|
148
|
+
// aldrig "hela poängen med posten är 5". Att kräva ordet "hela" gör
|
|
149
|
+
// undantaget smalast möjligt och stänger båda hålen i mekaniskt bruk på en
|
|
150
|
+
// gång, oavsett om talet skrivs som siffra ("är 5") eller ord ("handlar om
|
|
151
|
+
// fem enheter"). Ett tidigare försök jagade i stället en siffra i resten
|
|
152
|
+
// av meningen som ENDA spärr, utan att kräva "hela", och missade just
|
|
153
|
+
// ordformen (fälldes i en granskning). Siffer-lookaheaden ligger kvar som
|
|
154
|
+
// extra spärr på med/i-varianten, ren belt-and-braces mot specialfall som
|
|
155
|
+
// "hela poängen med de 5 posterna är 5" där "hela" ändå finns men en
|
|
156
|
+
// siffra i samma mening ändå bör tippa mot BROTT.
|
|
157
|
+
//
|
|
158
|
+
// Konsekvens, medveten falsk positiv: "poängen med det här är att slippa
|
|
159
|
+
// dubbelarbete" (idiom UTAN "hela") fälls nu som BROTT. Det kostar en
|
|
160
|
+
// omdispatch där agenten kan skriva "syftet med" i stället. Det är den
|
|
161
|
+
// säkra riktningen, eftersom BROTT ger en ofarlig omdispatch medan motsatt
|
|
162
|
+
// fel (falsk negativ) släpper protokollmekanik hela vägen ut i kundtext.
|
|
163
|
+
// Rör INTE kravet på "hela" utan att förstå detta, det ser ut som det kan
|
|
164
|
+
// "lättas upp" men gör det inte utan att öppna hålet igen.
|
|
165
|
+
//
|
|
166
|
+
// "poängen är att" behöver ingen lookahead: "är att" följs alltid av en
|
|
167
|
+
// sats, aldrig av en poängangivelse.
|
|
168
|
+
const IDIOM_ALLOW = [/hela\s+poängen\s+(?:med|i)\b(?![^.!?]*\d)/giu, /poängen\s+är\s+att\b/giu]
|
|
169
|
+
const maskIdioms = (text) =>
|
|
170
|
+
IDIOM_ALLOW.reduce((t, re) => t.replace(re, (m) => '�'.repeat(m.length)), text)
|
|
109
171
|
const checkCustomerText = (text, field) => {
|
|
110
172
|
if (typeof text !== 'string') return
|
|
173
|
+
const masked = maskIdioms(text)
|
|
111
174
|
for (const re of CUSTOMER_BLACKLIST) {
|
|
112
|
-
const m =
|
|
175
|
+
const m = masked.match(re)
|
|
113
176
|
if (m) problems.push(`${field}: protokollterm "${m[0]}" i kundtext`)
|
|
114
177
|
}
|
|
178
|
+
// Ankarvarningen körs mot ORIGINALET, aldrig mot den maskade strängen.
|
|
179
|
+
// Maskningen bevarar längden men inte innehållet, och varningscitatet ska
|
|
180
|
+
// visa vad som faktiskt stod i texten.
|
|
115
181
|
const a = text.match(ANCHOR_RE)
|
|
116
182
|
if (a) warnings.push(`${field}: "${a[0]}" — ankar-id eller pappersformat? Kontrollera`)
|
|
117
183
|
}
|
|
@@ -182,19 +248,10 @@ const checkEnvelopeKeys = (kind) => {
|
|
|
182
248
|
}
|
|
183
249
|
|
|
184
250
|
if (doc.classification !== undefined) {
|
|
185
|
-
// ---- classify
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
'MEDIUM',
|
|
190
|
-
'LARGE',
|
|
191
|
-
'OFFERT',
|
|
192
|
-
'KONCEPT',
|
|
193
|
-
'DIAGNOS',
|
|
194
|
-
'TILLBAKA_TILL_SPEC',
|
|
195
|
-
'INPUT_SAKNAS',
|
|
196
|
-
'ESKALERA',
|
|
197
|
-
]
|
|
251
|
+
// ---- classify (endast äldre arkiv, före ADR-0098) -------------------------
|
|
252
|
+
// Enumen bor i lib.mjs (CLASS_UTFALL) — delad med archive.mjs, som validerar
|
|
253
|
+
// samma fält på en ARKIVERAD körning innan set-status priced litar på den.
|
|
254
|
+
const UTFALL = CLASS_UTFALL
|
|
198
255
|
// Trasig form är brottet, aldrig en krasch — utan '- '-rader har omdispatchen
|
|
199
256
|
// inget att citera och stacktracen ser ut som ett scriptfel.
|
|
200
257
|
const c = doc.classification && typeof doc.classification === 'object' ? doc.classification : null
|
|
@@ -222,6 +279,9 @@ if (doc.classification !== undefined) {
|
|
|
222
279
|
'classification.fragor: saknas eller är tom — varje körning ska redovisa sina frågerader',
|
|
223
280
|
)
|
|
224
281
|
}
|
|
282
|
+
// Fråge-id är protokollets, aldrig modellens. Ett påhittat id ("S1-stod")
|
|
283
|
+
// passerade tidigare som "övrig rad" och kunde inte jämföras i dubbelkörningen.
|
|
284
|
+
const PROTOKOLL_IDS = ['S1', 'S2', 'S3', 'BASKLASS', 'B1', 'B2', 'B3']
|
|
225
285
|
const fragorRows = Array.isArray(c.fragor) ? c.fragor : []
|
|
226
286
|
for (const f0 of fragorRows) {
|
|
227
287
|
if (f0 == null || typeof f0 !== 'object') {
|
|
@@ -229,6 +289,10 @@ if (doc.classification !== undefined) {
|
|
|
229
289
|
continue
|
|
230
290
|
}
|
|
231
291
|
const f = f0
|
|
292
|
+
if (!PROTOKOLL_IDS.includes(f.id))
|
|
293
|
+
problems.push(
|
|
294
|
+
`fråga ${f.id}: okänt fråge-id (protokollet har exakt: ${PROTOKOLL_IDS.join(', ')})`,
|
|
295
|
+
)
|
|
232
296
|
// S1–S3/B1–B3 svarar JA/NEJ; basklassraden svarar med en klass
|
|
233
297
|
// (instruktionens JSON, "ordagrant och orört" — tvinga aldrig om den).
|
|
234
298
|
if (/^[SB][1-3]$/i.test(f.id ?? '')) {
|
|
@@ -239,11 +303,18 @@ if (doc.classification !== undefined) {
|
|
|
239
303
|
}
|
|
240
304
|
if (typeof f.konfidens !== 'number' || f.konfidens < 0 || f.konfidens > 1)
|
|
241
305
|
problems.push(`fråga ${f.id}: konfidens ${f.konfidens} utanför 0–1`)
|
|
242
|
-
// Belägg är obligatoriskt
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
306
|
+
// Belägg är obligatoriskt. Saknas belagg men ankaret finns har dispatchern
|
|
307
|
+
// hoppat över resolve-steget: det är dispatcherns fel (exit 2), aldrig
|
|
308
|
+
// agentens brott, en omdispatch vore meningslös.
|
|
309
|
+
if (!f.belagg || norm(f.belagg).length === 0) {
|
|
310
|
+
if (f.belagg_ankare) {
|
|
311
|
+
console.error(`fråga ${f.id}: belagg_ankare olöst — kör resolve-anchors.mjs först`)
|
|
312
|
+
process.exit(2)
|
|
313
|
+
}
|
|
314
|
+
problems.push(
|
|
315
|
+
`fråga ${f.id}: belägg saknas (obligatoriskt: belagg_ankare i svaret, belagg efter resolve)`,
|
|
316
|
+
)
|
|
317
|
+
} else if (!ok(f.belagg))
|
|
247
318
|
problems.push(
|
|
248
319
|
`fråga ${f.id}: belägg är inte ordagrant citat ur underlaget: "${f.belagg.slice(0, 70)}..."`,
|
|
249
320
|
)
|
|
@@ -255,15 +326,8 @@ if (doc.classification !== undefined) {
|
|
|
255
326
|
checkCustomerText(dep, `customer.dependencies[${i}]`)
|
|
256
327
|
} else if (doc.outcome !== undefined) {
|
|
257
328
|
// ---- score ---------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
'KONCEPT',
|
|
261
|
-
'DIAGNOS',
|
|
262
|
-
'TILLBAKA_TILL_SPEC',
|
|
263
|
-
'DELA_UPP',
|
|
264
|
-
'OVER_SKALAN',
|
|
265
|
-
'INPUT_SAKNAS',
|
|
266
|
-
]
|
|
329
|
+
// Enumen bor i lib.mjs (SCORE_OUTCOMES) — samma delning som UTFALL ovan.
|
|
330
|
+
const OUTCOMES = SCORE_OUTCOMES
|
|
267
331
|
checkEnvelopeKeys('score')
|
|
268
332
|
if (!MODES.includes(doc.mode)) problems.push(`mode: ogiltigt värde "${doc.mode}"`)
|
|
269
333
|
if (!OUTCOMES.includes(doc.outcome)) problems.push(`outcome: ogiltigt värde "${doc.outcome}"`)
|
|
@@ -279,6 +343,14 @@ if (doc.classification !== undefined) {
|
|
|
279
343
|
} else if (doc.score != null) {
|
|
280
344
|
problems.push(`score: ska vara null när outcome är ${doc.outcome}`)
|
|
281
345
|
}
|
|
346
|
+
// Ett ankare utan sitt lösta citat betyder att dispatchern hoppat över
|
|
347
|
+
// resolve-anchors.mjs: dispatcherns fel (exit 2), aldrig agentens brott.
|
|
348
|
+
for (const [i, d] of (Array.isArray(doc.drivers) ? doc.drivers : []).entries())
|
|
349
|
+
if (d && typeof d === 'object' && d.ankare && !d.citat) {
|
|
350
|
+
console.error(`drivers[${i}]: ankare olöst — kör resolve-anchors.mjs först`)
|
|
351
|
+
process.exit(2)
|
|
352
|
+
}
|
|
353
|
+
|
|
282
354
|
// Citat i drivers (även i objektform) ska vara ordagranna ur underlaget.
|
|
283
355
|
// Både raka och typografiska citattecken räknas. Ett icke-array-värde är
|
|
284
356
|
// redan fällt av POANG-kontrollen ovan — här skyddar vi bara iterationen.
|
|
@@ -116,6 +116,10 @@ export function reportDiffs(diffs) {
|
|
|
116
116
|
* källans fetstil- eller `kod`-markeringar är fortfarande ordagrant. */
|
|
117
117
|
export const norm = (s) => s.replace(/[*`]/g, '').replace(/\s+/g, ' ').trim()
|
|
118
118
|
|
|
119
|
+
/** Kanoniserar ett fakturerat modell-id: kontextsuffix som `[1m]` är samma
|
|
120
|
+
* modell på en annan kontextnivå, aldrig en egen modell (ADR-0099). */
|
|
121
|
+
export const canonicalModel = (id) => id.replace(/\[[^\]]*\]$/, '')
|
|
122
|
+
|
|
119
123
|
/** Ett "ordagrant" citat får hoppa med [...] och kortas med inledande/avslutande …
|
|
120
124
|
* Varje kvarvarande segment måste vara exakt substräng av det normaliserade
|
|
121
125
|
* underlaget. Tomt efter filtrering = INTE ordagrant. */
|
|
@@ -126,3 +130,65 @@ export const quoteOk = (q, normalizedSource) => {
|
|
|
126
130
|
.filter((seg) => seg.length > 0)
|
|
127
131
|
return segments.length > 0 && segments.every((seg) => normalizedSource.includes(seg))
|
|
128
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Klassaxelns fullständiga utfallsenum (klassningsinstruktion.md, Steg 4) och
|
|
136
|
+
* poängaxelns fullständiga outcome-enum (request-score/SKILL.md). Delade
|
|
137
|
+
* mellan contract-check.mjs (validerar en FÄRSK körning mot protokollet) och
|
|
138
|
+
* archive.mjs (validerar en ARKIVERAD körning innan set-status priced litar
|
|
139
|
+
* på dess utfall) — en enda lista, aldrig två som kan glida isär.
|
|
140
|
+
*
|
|
141
|
+
* Detta är en ALLOW-lista (känt värde), inte bara en icke-tom-sträng-kontroll.
|
|
142
|
+
* En DENY-lista över stopputfall (se CLASS_STOP/SCORE_STOP i archive.mjs)
|
|
143
|
+
* släpper igenom en felstavning ("OFERT"), en tom sträng, eller fel skiftläge
|
|
144
|
+
* ("eskalera") eftersom inget av dem finns i deny-listan — grinden failar
|
|
145
|
+
* ÖPPET på precis den typ av trasig data en modell råkar producera. En
|
|
146
|
+
* allow-lista failar STÄNGT i stället: allt som inte är ett av de kända
|
|
147
|
+
* värdena är ett integritetsfel, aldrig ett grönt kort.
|
|
148
|
+
*/
|
|
149
|
+
// CLASS_UTFALL finns kvar ENBART för äldre arkiv (före ADR-0098, då
|
|
150
|
+
// klassaxeln togs bort): archive.mjs och contract-check.mjs validerar
|
|
151
|
+
// fortfarande arkiverade classification-filer mot den.
|
|
152
|
+
export const CLASS_UTFALL = [
|
|
153
|
+
'TINY',
|
|
154
|
+
'SMALL',
|
|
155
|
+
'MEDIUM',
|
|
156
|
+
'LARGE',
|
|
157
|
+
'OFFERT',
|
|
158
|
+
'KONCEPT',
|
|
159
|
+
'DIAGNOS',
|
|
160
|
+
'TILLBAKA_TILL_SPEC',
|
|
161
|
+
'INPUT_SAKNAS',
|
|
162
|
+
'ESKALERA',
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
export const SCORE_OUTCOMES = [
|
|
166
|
+
'POANG',
|
|
167
|
+
'KONCEPT',
|
|
168
|
+
'DIAGNOS',
|
|
169
|
+
'TILLBAKA_TILL_SPEC',
|
|
170
|
+
'DELA_UPP',
|
|
171
|
+
'OVER_SKALAN',
|
|
172
|
+
'INPUT_SAKNAS',
|
|
173
|
+
// ADR-0098: flyttad hit från klassningens utfall när klassningen togs bort —
|
|
174
|
+
// utgången "en människa ska titta på det här" får aldrig saknas.
|
|
175
|
+
'ESKALERA',
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
/** Rubrikrad: fetstilsrad eller markdown-rubrik som utgör hela raden. Delad
|
|
179
|
+
* mellan precheck (formkontroll) och report (fältvis jämförelse) så rapporten
|
|
180
|
+
* läser specen med exakt samma regler som grinden. */
|
|
181
|
+
export const isHeadingLine = (line) =>
|
|
182
|
+
/^\*\*[^*]+\*\*\s*$/.test(line.trim()) || /^#{1,6}\s+\S/.test(line.trim())
|
|
183
|
+
|
|
184
|
+
export const headingRe = (h) => new RegExp(`^(\\*\\*${h}\\*\\*|#{1,6}\\s*${h})\\s*$`, 'm')
|
|
185
|
+
|
|
186
|
+
/** Raderna i sektionen under rubriken `heading`, fram till nästa rubrikrad. */
|
|
187
|
+
export const sectionLines = (block, heading) => {
|
|
188
|
+
const lines = block.split('\n')
|
|
189
|
+
const start = lines.findIndex((l) => headingRe(heading).test(l.trim()))
|
|
190
|
+
if (start === -1) return null
|
|
191
|
+
const rest = lines.slice(start + 1)
|
|
192
|
+
const end = rest.findIndex((l) => isHeadingLine(l))
|
|
193
|
+
return rest.slice(0, end === -1 ? rest.length : end)
|
|
194
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* status (ok/fel-lista), aldrig ett protokollutfall.
|
|
8
8
|
*
|
|
9
9
|
* BRISTER (exit 1) är hårda formfel. VARNING är heuristik som kräver mänsklig
|
|
10
|
-
* blick (
|
|
10
|
+
* blick (poängord i analys, saknad Ingår inte) — de skrivs ut men stoppar inget.
|
|
11
11
|
*
|
|
12
12
|
* Användning: node precheck.mjs <arkivkatalog> [--impact]
|
|
13
13
|
* --impact kräv och kontrollera även impact.md (bedöm-steget)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readFileSync, existsSync } from 'node:fs'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
18
|
|
|
19
|
-
import { markerStrippedSha256, refDriftWarning } from './lib.mjs'
|
|
19
|
+
import { markerStrippedSha256, refDriftWarning, headingRe, sectionLines } from './lib.mjs'
|
|
20
20
|
|
|
21
21
|
const dir = process.argv[2]
|
|
22
22
|
const requireImpact = process.argv.includes('--impact')
|
|
@@ -31,7 +31,7 @@ const warnings = []
|
|
|
31
31
|
// Kontraktsdriftvakt: rubrik- och vokabulärreglerna nedan speglar specformatet.
|
|
32
32
|
// Pinnen är markör-strippad (stabil över haus-releaser, som stämplar om filer
|
|
33
33
|
// med paketversionen) och sökvägen hedrar CLAUDE_CONFIG_DIR — se lib.mjs.
|
|
34
|
-
const SPECFORMAT_SHA = '
|
|
34
|
+
const SPECFORMAT_SHA = 'e6134fd8a3c1cc9e3056c849d8834310e39d269846f2b6bec2871a83d488fc01'
|
|
35
35
|
{
|
|
36
36
|
const w = refDriftWarning('request-write-spec/references/specformat.md', SPECFORMAT_SHA)
|
|
37
37
|
if (w) warnings.push(w)
|
|
@@ -43,19 +43,6 @@ const TIDSORD_RE = /[\p{L}]*tim(?:me|mar|marna|men)[\p{L}]*|[\p{L}]*estim(?:at|e
|
|
|
43
43
|
// JS \b bryter vid å/ä/ö — svenska ord kräver Unicode-medvetna gränser
|
|
44
44
|
// ("kräver" får aldrig matcha förbudet "kr").
|
|
45
45
|
const W = (pat) => new RegExp(`(?<![\\p{L}\\d_])(?:${pat})(?![\\p{L}\\d_])`, 'iu')
|
|
46
|
-
// Rubrikrad: fetstilsrad eller markdown-rubrik som utgör hela raden.
|
|
47
|
-
const isHeadingLine = (line) =>
|
|
48
|
-
/^\*\*[^*]+\*\*\s*$/.test(line.trim()) || /^#{1,6}\s+\S/.test(line.trim())
|
|
49
|
-
const headingRe = (h) => new RegExp(`^(\\*\\*${h}\\*\\*|#{1,6}\\s*${h})\\s*$`, 'm')
|
|
50
|
-
/** Raderna i sektionen under rubriken `heading`, fram till nästa rubrikrad. */
|
|
51
|
-
const sectionLines = (block, heading) => {
|
|
52
|
-
const lines = block.split('\n')
|
|
53
|
-
const start = lines.findIndex((l) => headingRe(heading).test(l.trim()))
|
|
54
|
-
if (start === -1) return null
|
|
55
|
-
const rest = lines.slice(start + 1)
|
|
56
|
-
const end = rest.findIndex((l) => isHeadingLine(l))
|
|
57
|
-
return rest.slice(0, end === -1 ? rest.length : end)
|
|
58
|
-
}
|
|
59
46
|
|
|
60
47
|
const read = (name, required) => {
|
|
61
48
|
const p = join(dir, name)
|
|
@@ -156,19 +143,16 @@ if (requireImpact) {
|
|
|
156
143
|
problems.push('impact.md: "## Till granskaren" saknas')
|
|
157
144
|
for (const row of ['Ej verifierat', 'Avvikelse mot specen', 'Obesvarade utfallsval'])
|
|
158
145
|
if (!impact.includes(`**${row}:**`)) problems.push(`impact.md: raden "**${row}:**" saknas`)
|
|
159
|
-
// Analysen får inte läcka
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
146
|
+
// Analysen får inte läcka poängsättning eller tid (ADR-0098: kontrollen
|
|
147
|
+
// gällde tidigare klassord; klassaxeln är borttagen och regeln riktas nu
|
|
148
|
+
// mot poängen, som är det analysens läsare inte får primas mot).
|
|
149
|
+
// Poängord är dock en heuristik — "poängen med ändringen" och ett
|
|
150
|
+
// ordagrant citerat kod-enum är legitima — så träffar blir varningar för
|
|
151
|
+
// mänsklig blick, aldrig hårda stopp. Tidsord förblir hårda.
|
|
152
|
+
const m = impact.match(W('poäng(?:en|er|sätt\\p{L}*|sättning\\p{L}*)?'))
|
|
164
153
|
if (m)
|
|
165
154
|
warnings.push(
|
|
166
|
-
`impact.md:
|
|
167
|
-
)
|
|
168
|
-
const cls = impact.match(/(?<![\p{L}\d_])(TINY|SMALL|MEDIUM|LARGE|OFFERT)(?![\p{L}\d_])/u)
|
|
169
|
-
if (cls)
|
|
170
|
-
warnings.push(
|
|
171
|
-
`impact.md: klassnamnet "${cls[0]}" — läcka, eller citat av ett kod-enum? Kontrollera`,
|
|
155
|
+
`impact.md: poängord "${m[0]}" — kontrollera att analysen inte läcker poängsättning`,
|
|
172
156
|
)
|
|
173
157
|
const t = impact.match(TIDSORD_RE)
|
|
174
158
|
if (t) problems.push(`impact.md: förbjudet tidsord "${t[0]}"`)
|
|
@@ -182,6 +166,25 @@ if (state != null) {
|
|
|
182
166
|
const s = JSON.parse(state)
|
|
183
167
|
for (const key of ['request_id', 'slug', 'title', 'status', 'runs'])
|
|
184
168
|
if (!(key in s)) problems.push(`state.json: fältet "${key}" saknas`)
|
|
169
|
+
// ADR-0099 (#461): två byteidentiska runs-rader för samma fil är
|
|
170
|
+
// signaturen ett OSTÄMPLAT omtag lämnar — samma dispatch bokförd två
|
|
171
|
+
// gånger, den förkastade utan avvisad-fil och utan felrader. Heuristik
|
|
172
|
+
// (en legitim dubblett är tänkbar), därför varning, aldrig brist.
|
|
173
|
+
if (Array.isArray(s.runs)) {
|
|
174
|
+
const sedda = new Set()
|
|
175
|
+
const flaggade = new Set()
|
|
176
|
+
for (const rad of s.runs) {
|
|
177
|
+
const nyckel = JSON.stringify(rad)
|
|
178
|
+
if (sedda.has(nyckel) && !flaggade.has(nyckel)) {
|
|
179
|
+
flaggade.add(nyckel)
|
|
180
|
+
warnings.push(
|
|
181
|
+
`state.json: identiska runs-rader för ${rad?.file ?? '?'} — ett omtag ska ` +
|
|
182
|
+
'arkiveras som -avvisad med kontrollens felrader, aldrig bokföras som en dubblettrad',
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
sedda.add(nyckel)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
185
188
|
// Underlagsproveniens: analysen måste höra till DEN spec som ligger i
|
|
186
189
|
// katalogen nu. En respec efter underkännande lämnar annars en gammal
|
|
187
190
|
// analys som prissätts mot ny spec — helt osynligt utan den här kontrollen.
|