@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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync } from 'node:fs'
|
|
15
15
|
|
|
16
|
-
import { refDriftWarning } from './lib.mjs'
|
|
16
|
+
import { refDriftWarning, norm, quoteOk } from './lib.mjs'
|
|
17
17
|
|
|
18
18
|
const argv = process.argv.slice(2)
|
|
19
19
|
let expectId = null
|
|
@@ -47,9 +47,6 @@ for (const [rel, expected] of Object.entries(EXPECTED_REFS)) {
|
|
|
47
47
|
const w = refDriftWarning(rel, expected)
|
|
48
48
|
if (w) warnings.push(w)
|
|
49
49
|
}
|
|
50
|
-
// Markdown-betoning (**fet**, *kursiv*, `kod`) strippas före jämförelse — ett
|
|
51
|
-
// citat som återges utan källans markeringar är fortfarande ordagrant.
|
|
52
|
-
const norm = (s) => s.replace(/[*`]/g, '').replace(/\s+/g, ' ').trim()
|
|
53
50
|
let doc
|
|
54
51
|
try {
|
|
55
52
|
doc = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
@@ -69,17 +66,9 @@ try {
|
|
|
69
66
|
process.exit(2)
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* "[...]" ett giltigt belägg för vad som helst). */
|
|
76
|
-
const quoteOk = (q) => {
|
|
77
|
-
const segments = q
|
|
78
|
-
.split(/\s*\[(?:\.\.\.|…)\]\s*/)
|
|
79
|
-
.map((seg) => norm(seg.replace(/^(\.\.\.|…)|(\.\.\.|…)$/g, '')))
|
|
80
|
-
.filter((seg) => seg.length > 0)
|
|
81
|
-
return segments.length > 0 && segments.every((seg) => source.includes(seg))
|
|
82
|
-
}
|
|
69
|
+
// quoteOk kommer från lib.mjs (delad med quote-repair.mjs); bind källan lokalt
|
|
70
|
+
// så befintliga anropsställen (`ok(f.belagg)` osv.) förblir oförändrade.
|
|
71
|
+
const ok = (q) => quoteOk(q, source)
|
|
83
72
|
|
|
84
73
|
/** Alla strängvärden ur ett godtyckligt värde (drivers kan vara objekt). */
|
|
85
74
|
const collectStrings = (v) => {
|
|
@@ -254,7 +243,7 @@ if (doc.classification !== undefined) {
|
|
|
254
243
|
// inte låg kvalitet.
|
|
255
244
|
if (!f.belagg || norm(f.belagg).length === 0)
|
|
256
245
|
problems.push(`fråga ${f.id}: belägg saknas (obligatoriskt, ordagrant citat)`)
|
|
257
|
-
else if (!
|
|
246
|
+
else if (!ok(f.belagg))
|
|
258
247
|
problems.push(
|
|
259
248
|
`fråga ${f.id}: belägg är inte ordagrant citat ur underlaget: "${f.belagg.slice(0, 70)}..."`,
|
|
260
249
|
)
|
|
@@ -296,7 +285,7 @@ if (doc.classification !== undefined) {
|
|
|
296
285
|
for (const [i, d] of (Array.isArray(doc.drivers) ? doc.drivers : []).entries())
|
|
297
286
|
for (const str of collectStrings(d))
|
|
298
287
|
for (const m of str.matchAll(/["“”„«»]([^"“”„«»]{15,})["“”„«»]/g))
|
|
299
|
-
if (!
|
|
288
|
+
if (!ok(m[1]))
|
|
300
289
|
problems.push(
|
|
301
290
|
`drivers[${i}]: citat är inte ordagrant ur underlaget: "${m[1].slice(0, 70)}..."`,
|
|
302
291
|
)
|
|
@@ -111,3 +111,18 @@ export function reportDiffs(diffs) {
|
|
|
111
111
|
for (const d of diffs) console.log(`- ${d}`)
|
|
112
112
|
process.exit(1)
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/** Markdown-betoning strippas före jämförelse: ett citat som återges utan
|
|
116
|
+
* källans fetstil- eller `kod`-markeringar är fortfarande ordagrant. */
|
|
117
|
+
export const norm = (s) => s.replace(/[*`]/g, '').replace(/\s+/g, ' ').trim()
|
|
118
|
+
|
|
119
|
+
/** Ett "ordagrant" citat får hoppa med [...] och kortas med inledande/avslutande …
|
|
120
|
+
* Varje kvarvarande segment måste vara exakt substräng av det normaliserade
|
|
121
|
+
* underlaget. Tomt efter filtrering = INTE ordagrant. */
|
|
122
|
+
export const quoteOk = (q, normalizedSource) => {
|
|
123
|
+
const segments = q
|
|
124
|
+
.split(/\s*\[(?:\.\.\.|…)\]\s*/)
|
|
125
|
+
.map((seg) => norm(seg.replace(/^(\.\.\.|…)|(\.\.\.|…)$/g, '')))
|
|
126
|
+
.filter((seg) => seg.length > 0)
|
|
127
|
+
return segments.length > 0 && segments.every((seg) => normalizedSource.includes(seg))
|
|
128
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deterministisk citaträttning FÖRE omdispatch. Pilotsessionerna 2026-08-28
|
|
4
|
+
* visade att classifiern komponerar belägg i stället för att citera (4 av 4
|
|
5
|
+
* förstapass fälldes av contract-check), och varje fällning kostade en full
|
|
6
|
+
* omkörning på 6-9 min. Det här scriptet räddar det räddningsbara: ett citat
|
|
7
|
+
* som inte är exakt substräng ersätts med sin ENTYDIGT närmaste källmening.
|
|
8
|
+
* Ingen entydig träff = ingen rättning (hellre omdispatch än fel belägg), och
|
|
9
|
+
* skrivningen är allt eller inget så en halvrättad fil aldrig arkiveras.
|
|
10
|
+
* En kandidat med annan polaritet än citatet (se NEGATIONER) är aldrig giltig:
|
|
11
|
+
* rättningen får byta form på pekaren, aldrig vända påståendet.
|
|
12
|
+
*
|
|
13
|
+
* Rättar samma fält som contract-check kontrollerar: classification.fragor[].belagg
|
|
14
|
+
* samt citattecken-omgärdade segment (>= 15 tecken) i score-JSON:ens drivers.
|
|
15
|
+
* Bedömer aldrig innehåll; byter aldrig mening, bara formen på pekaren.
|
|
16
|
+
*
|
|
17
|
+
* Användning: node quote-repair.mjs <run.json> <spec.md> [impact.md]
|
|
18
|
+
* Stdout: "RÄTTAT (N):" + rader, "ORÄTTBART (N):" + rader, eller "INGET ATT RÄTTA".
|
|
19
|
+
* Exit 0 = inga orättbara citat kvar (rättade eller inga), 1 = orättbara kvar
|
|
20
|
+
* (filen orörd), 2 = användningsfel/oläsbar input (dispatcherfel).
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
23
|
+
|
|
24
|
+
import { norm, quoteOk } from './lib.mjs'
|
|
25
|
+
|
|
26
|
+
const [jsonPath, specPath, impactPath] = process.argv.slice(2)
|
|
27
|
+
if (!jsonPath || !specPath) {
|
|
28
|
+
console.error('användning: node quote-repair.mjs <run.json> <spec.md> [impact.md]')
|
|
29
|
+
process.exit(2)
|
|
30
|
+
}
|
|
31
|
+
let doc, sourceRaw
|
|
32
|
+
try {
|
|
33
|
+
doc = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
34
|
+
sourceRaw =
|
|
35
|
+
readFileSync(specPath, 'utf8') + '\n' + (impactPath ? readFileSync(impactPath, 'utf8') : '')
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(`kunde inte läsa input: ${e.message}`)
|
|
38
|
+
process.exit(2)
|
|
39
|
+
}
|
|
40
|
+
const source = norm(sourceRaw)
|
|
41
|
+
|
|
42
|
+
// Kandidater: källans meningar (rad- och meningsgränser) plus par av
|
|
43
|
+
// angränsande meningar, så ett tvåmeningscitat kan få en exakt motsvarighet.
|
|
44
|
+
const sentences = sourceRaw
|
|
45
|
+
.split(/\n+|(?<=[.!?])\s+(?=[A-ZÅÄÖ0-9])/u)
|
|
46
|
+
.map((s) => s.trim())
|
|
47
|
+
.filter((s) => norm(s).length >= 15)
|
|
48
|
+
const candidates = [...sentences]
|
|
49
|
+
for (let i = 0; i + 1 < sentences.length; i++)
|
|
50
|
+
candidates.push(`${sentences[i]} ${sentences[i + 1]}`)
|
|
51
|
+
|
|
52
|
+
const tokens = (s) =>
|
|
53
|
+
new Set(
|
|
54
|
+
norm(s)
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
57
|
+
.filter(Boolean),
|
|
58
|
+
)
|
|
59
|
+
const dice = (A, B) => {
|
|
60
|
+
let inter = 0
|
|
61
|
+
for (const t of A) if (B.has(t)) inter++
|
|
62
|
+
return (2 * inter) / (A.size + B.size || 1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Polaritetsvakt. Dice över tokenmängder är blind för negation: "Ingen" är ett
|
|
67
|
+
* ord av nio, men vänder påståendet. Utan den här listan "rättas" ett belägg som
|
|
68
|
+
* säger att behörighetslogik ÄNDRAS till källmeningen som säger att den INTE
|
|
69
|
+
* ändras, och contract-check släpper igenom det, alltså tvärtemot vad
|
|
70
|
+
* citatgrinden finns för. Ett citat får bara ersättas av en kandidat med exakt
|
|
71
|
+
* samma uppsättning negationsord.
|
|
72
|
+
*/
|
|
73
|
+
const NEGATIONER = new Set([
|
|
74
|
+
'ingen',
|
|
75
|
+
'inga',
|
|
76
|
+
'inget',
|
|
77
|
+
'inte',
|
|
78
|
+
'icke',
|
|
79
|
+
'aldrig',
|
|
80
|
+
'utan',
|
|
81
|
+
'ej',
|
|
82
|
+
'saknas',
|
|
83
|
+
'varken',
|
|
84
|
+
])
|
|
85
|
+
const negationer = (s) => {
|
|
86
|
+
const found = []
|
|
87
|
+
for (const t of tokens(s)) if (NEGATIONER.has(t)) found.push(t)
|
|
88
|
+
return found.sort().join(' ')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Entydighetskravet är själva säkerhetsregeln: bästa träffen måste vara både
|
|
92
|
+
// nära nog (>= 0.6) och klart ensam (marginal >= 0.15), annars rättas inget.
|
|
93
|
+
// Polaritetsvakten går FÖRE poängsättningen: en kandidat med annan uppsättning
|
|
94
|
+
// negationsord är aldrig en giltig ersättare, hur nära den än ligger i Dice.
|
|
95
|
+
// En par-kandidat som bara är bästa träffen med en angränsande mening bilagd
|
|
96
|
+
// (delsträngsöverlapp åt endera hållet) är samma källplats, inte en
|
|
97
|
+
// konkurrerande: annars slår varannan stark enmeningsträff ut sig själv på
|
|
98
|
+
// marginalkravet så snart en granne delar ett enda vanligt ord.
|
|
99
|
+
const findReplacement = (quote) => {
|
|
100
|
+
const q = tokens(quote)
|
|
101
|
+
const pol = negationer(quote)
|
|
102
|
+
const scored = candidates
|
|
103
|
+
.filter((c) => negationer(c) === pol)
|
|
104
|
+
.map((c) => ({ c, s: dice(q, tokens(c)) }))
|
|
105
|
+
.sort((a, b) => b.s - a.s)
|
|
106
|
+
const best = scored[0]
|
|
107
|
+
if (!best || best.s < 0.6) return null
|
|
108
|
+
const second =
|
|
109
|
+
scored.find((x) => x.c !== best.c && !x.c.includes(best.c) && !best.c.includes(x.c))?.s ?? 0
|
|
110
|
+
if (best.s - second >= 0.15 && quoteOk(best.c, source)) return best.c
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const trunc = (s) => (s.length > 60 ? `${s.slice(0, 60)}…` : s)
|
|
115
|
+
// Citattecken i en ersättning som ska splitsas IN mellan citattecken skulle
|
|
116
|
+
// stänga citatet för tidigt: strängen blir obalanserad och nästa läsning ser ett
|
|
117
|
+
// annat citat än det som godkändes. Sådana kandidater refuseras.
|
|
118
|
+
const CITATTECKEN = /["“”„«»]/
|
|
119
|
+
const repaired = []
|
|
120
|
+
const unfixable = []
|
|
121
|
+
const fixQuote = (val, label, forbidQuoteChars = false) => {
|
|
122
|
+
if (quoteOk(val, source)) return val
|
|
123
|
+
const r = findReplacement(val)
|
|
124
|
+
if (r && !(forbidQuoteChars && CITATTECKEN.test(r))) {
|
|
125
|
+
repaired.push(`- ${label}: "${trunc(val)}" → "${trunc(r)}"`)
|
|
126
|
+
return r
|
|
127
|
+
}
|
|
128
|
+
unfixable.push(`- ${label}: "${trunc(val)}"`)
|
|
129
|
+
return val
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (doc.classification && typeof doc.classification === 'object') {
|
|
133
|
+
for (const f of Array.isArray(doc.classification.fragor) ? doc.classification.fragor : [])
|
|
134
|
+
if (f && typeof f.belagg === 'string' && norm(f.belagg).length > 0)
|
|
135
|
+
f.belagg = fixQuote(f.belagg, `fråga ${f.id}`)
|
|
136
|
+
} else if (doc.outcome !== undefined) {
|
|
137
|
+
// Samma citat-regex som contract-check: raka och typografiska citattecken.
|
|
138
|
+
const fixInString = (str, label) =>
|
|
139
|
+
str.replace(
|
|
140
|
+
/(["“”„«»])([^"“”„«»]{15,})(["“”„«»])/g,
|
|
141
|
+
(_m, o, inner, c) => o + fixQuote(inner, label, true) + c,
|
|
142
|
+
)
|
|
143
|
+
const walk = (v, label) =>
|
|
144
|
+
typeof v === 'string'
|
|
145
|
+
? fixInString(v, label)
|
|
146
|
+
: Array.isArray(v)
|
|
147
|
+
? v.map((x) => walk(x, label))
|
|
148
|
+
: v && typeof v === 'object'
|
|
149
|
+
? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, label)]))
|
|
150
|
+
: v
|
|
151
|
+
if (Array.isArray(doc.drivers)) doc.drivers = doc.drivers.map((d, i) => walk(d, `drivers[${i}]`))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (unfixable.length > 0) {
|
|
155
|
+
console.log(`ORÄTTBART (${unfixable.length}):`)
|
|
156
|
+
for (const u of unfixable) console.log(u)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
if (repaired.length > 0) {
|
|
160
|
+
writeFileSync(jsonPath, JSON.stringify(doc, null, 2) + '\n')
|
|
161
|
+
console.log(`RÄTTAT (${repaired.length}):`)
|
|
162
|
+
for (const r of repaired) console.log(r)
|
|
163
|
+
} else {
|
|
164
|
+
console.log('INGET ATT RÄTTA')
|
|
165
|
+
}
|
|
166
|
+
process.exit(0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/haus-workflow",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Haus AI workflow CLI for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"library/global",
|
|
18
18
|
"library/catalog",
|
|
19
19
|
"README.md",
|
|
20
|
-
"CHANGELOG.md"
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"docs/before-you-start.md",
|
|
22
|
+
"docs/glossary.md"
|
|
21
23
|
],
|
|
22
24
|
"scripts": {
|
|
23
25
|
"build": "rm -rf dist && esbuild src/cli.ts --bundle --packages=external --platform=node --format=esm --outdir=dist --external:@inquirer/checkbox",
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"verify:full": "node scripts/verify.mjs --coverage",
|
|
40
42
|
"check:contract-behavior": "node --import tsx scripts/contract-behavior-check.mjs",
|
|
41
43
|
"check:mutation-table": "node scripts/check-mutation-table.mjs",
|
|
44
|
+
"check:docs-site": "node scripts/check-docs-site.mjs",
|
|
42
45
|
"postinstall": "node ./scripts/postinstall.mjs || true",
|
|
43
46
|
"prepare": "lefthook install || true",
|
|
44
47
|
"security:audit": "yarn npm audit -A -R --severity high --environment production"
|