@haus-tech/haus-workflow 5.6.1 → 5.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 +2 -0
- package/library/global/agents/request-assessor.md +52 -51
- package/library/global/agents/request-scorer.md +52 -52
- package/library/global/agents/request-specifier-eskalerad.md +46 -44
- package/library/global/agents/request-specifier.md +43 -40
- package/library/global/skills/request/SKILL.md +1415 -1360
- package/library/global/skills/request/scripts/contract-check.mjs +17 -1
- package/library/global/skills/request/scripts/kontroll.mjs +7 -5
- package/library/global/skills/request/scripts/korning.mjs +16 -3
- package/library/global/skills/request/scripts/lib.mjs +289 -17
- package/library/global/skills/request/scripts/precheck.mjs +105 -40
- package/library/global/skills/request-assess-impact/SKILL.md +144 -140
- package/library/global/skills/request-score/SKILL.md +290 -266
- package/library/global/skills/request-sync/SKILL.md +246 -245
- package/library/global/skills/request-write-spec/SKILL.md +63 -61
- package/library/global/skills/request-write-spec/references/specformat.md +64 -60
- package/package.json +1 -1
|
@@ -23,14 +23,24 @@ import {
|
|
|
23
23
|
sectionLines,
|
|
24
24
|
isHeadingLine,
|
|
25
25
|
SPEC_HEADINGS,
|
|
26
|
+
IMPACT_HEADINGS,
|
|
26
27
|
headingLanguage,
|
|
28
|
+
impactHeadingLanguage,
|
|
27
29
|
kandidatIn,
|
|
28
30
|
avvikelseEtikett,
|
|
29
31
|
AVVIKELSE_ETIKETTER,
|
|
32
|
+
AVVIKELSE_ALIAS,
|
|
33
|
+
STRUKET_PREFIX,
|
|
30
34
|
utfallsvalAlternativ,
|
|
31
35
|
utfallsvalProsa,
|
|
32
36
|
prisavgorandeVal,
|
|
33
37
|
prisavgorandeForm,
|
|
38
|
+
TILL_GRANSKAREN,
|
|
39
|
+
granskareRow,
|
|
40
|
+
granskareRadRe,
|
|
41
|
+
tillGranskarenRe,
|
|
42
|
+
TIDSORD_RE,
|
|
43
|
+
POANGORD_RE,
|
|
34
44
|
} from './lib.mjs'
|
|
35
45
|
|
|
36
46
|
const dir = process.argv[2]
|
|
@@ -54,14 +64,14 @@ let prisavgorande = null
|
|
|
54
64
|
// Kontraktsdriftvakt: rubrik- och vokabulärreglerna nedan speglar specformatet.
|
|
55
65
|
// Pinnen är markör-strippad (stabil över haus-releaser, som stämplar om filer
|
|
56
66
|
// med paketversionen) och sökvägen hedrar CLAUDE_CONFIG_DIR — se lib.mjs.
|
|
57
|
-
const SPECFORMAT_SHA = '
|
|
67
|
+
const SPECFORMAT_SHA = '2cd9912f8ea91edb04fb7772e809482236d72d1b3e3fb4db6c196d9f991b1ec3'
|
|
58
68
|
{
|
|
59
69
|
const w = refDriftWarning('request-write-spec/references/specformat.md', SPECFORMAT_SHA)
|
|
60
70
|
if (w) warnings.push(w)
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
// Tidsord förbjuds i både spec och analys — EN regex,
|
|
64
|
-
|
|
73
|
+
// Tidsord förbjuds i både spec och analys — EN regex, TIDSORD_RE i lib.mjs, så
|
|
74
|
+
// reglerna aldrig glider isär och driftvakten läser samma regex som grinden.
|
|
65
75
|
|
|
66
76
|
// JS \b bryter vid å/ä/ö — svenska ord kräver Unicode-medvetna gränser
|
|
67
77
|
// ("kräver" får aldrig matcha förbudet "kr").
|
|
@@ -87,15 +97,19 @@ const headingText = (line) =>
|
|
|
87
97
|
/** En specrubrik enligt tabellen, på något av språken. */
|
|
88
98
|
const isSpecHeading = (line) => SPEC_HEADINGS.some((row) => headingRe(row.key).test(line.trim()))
|
|
89
99
|
|
|
90
|
-
/** Vilka språkbärande rubriker blocket har, per språk
|
|
91
|
-
* SPEC_HEADINGS
|
|
92
|
-
|
|
100
|
+
/** Vilka språkbärande rubriker blocket har, per språk, för valfri rubriktabell
|
|
101
|
+
* (SPEC_HEADINGS eller IMPACT_HEADINGS). Anroparen väljer tabellen explicit,
|
|
102
|
+
* så specens och analysens rubriker aldrig kan blandas ihop av misstag —
|
|
103
|
+
* samma skäl som gör headingLanguage/impactHeadingLanguage till skilda
|
|
104
|
+
* funktioner i lib.mjs, men här räcker en parameter: den här hjälparen
|
|
105
|
+
* bara LISTAR träffar, den avgör aldrig blandad/sv/en åt någon. */
|
|
106
|
+
const headingsByLanguage = (block, table) => {
|
|
93
107
|
const sv = []
|
|
94
108
|
const en = []
|
|
95
109
|
for (const line of block.split('\n')) {
|
|
96
110
|
if (!isHeadingLine(line)) continue
|
|
97
111
|
const text = headingText(line)
|
|
98
|
-
for (const row of
|
|
112
|
+
for (const row of table) {
|
|
99
113
|
if (row.sv === row.en) continue
|
|
100
114
|
if (text === row.sv) sv.push(row.sv)
|
|
101
115
|
else if (text === row.en) en.push(row.en)
|
|
@@ -125,6 +139,39 @@ const checkKandidatForm = (block, kandidat, label) => {
|
|
|
125
139
|
problems.push(`${label}: kandidat med ${heading}: antingen spec eller kandidat`)
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
/** S1: en felrad ska namnge literalen i DOKUMENTETS eget språk, inte alltid
|
|
143
|
+
* engelska — annars skickar en rättning/omdispatch agenten tillbaka till
|
|
144
|
+
* det språk vi just lämnat. Signalen är den svenska "## Till granskaren"-
|
|
145
|
+
* rubriken: den finns bara om dokumentet självt är svenskt. Saknas rubriken
|
|
146
|
+
* helt (vilket är exakt när dess egen saknas-rad namnges) är resultatet
|
|
147
|
+
* detsamma som "inte svensk" -> engelska, det nya standardspråket. */
|
|
148
|
+
const harSvenskGranskarrubrik = (block) =>
|
|
149
|
+
new RegExp(`^#{1,6}\\s+${TILL_GRANSKAREN.sv}\\s*$`, 'imu').test(block)
|
|
150
|
+
|
|
151
|
+
/** Värdena rättningsraderna ber agenten skriva, i dokumentets språk (samma
|
|
152
|
+
* S1-mönster som rad- och rubrikliteralerna). Svenska dokument får exakt
|
|
153
|
+
* samma text som före uppdelningen. De engelska etiketterna härleds ur
|
|
154
|
+
* AVVIKELSE_ALIAS och prefixen ur STRUKET_PREFIX, så lib.mjs förblir den
|
|
155
|
+
* enda platsen för literalerna. */
|
|
156
|
+
const engelskEtikett = (sv) =>
|
|
157
|
+
Object.keys(AVVIKELSE_ALIAS).find((ord) => ord !== sv && AVVIKELSE_ALIAS[ord] === sv)
|
|
158
|
+
const RATTNINGSVARDEN = {
|
|
159
|
+
sv: {
|
|
160
|
+
nej: 'nej',
|
|
161
|
+
ja: 'ja: <valet i en mening>',
|
|
162
|
+
etikett: (sv) => sv,
|
|
163
|
+
struket: STRUKET_PREFIX[0],
|
|
164
|
+
inga: 'inga',
|
|
165
|
+
},
|
|
166
|
+
en: {
|
|
167
|
+
nej: 'no',
|
|
168
|
+
ja: 'yes: <the choice in one sentence>',
|
|
169
|
+
etikett: engelskEtikett,
|
|
170
|
+
struket: STRUKET_PREFIX[1],
|
|
171
|
+
inga: 'none',
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
128
175
|
// ---- spec.md ---------------------------------------------------------------
|
|
129
176
|
const spec = read('spec.md', true)
|
|
130
177
|
if (spec != null) {
|
|
@@ -149,7 +196,7 @@ if (spec != null) {
|
|
|
149
196
|
// ur båda språken i samma block fälls, kunden ser rubrikerna. Vilka som
|
|
150
197
|
// hittades per språk namnges så rättningen blir riktad.
|
|
151
198
|
if (headingLanguage(block) === 'blandad') {
|
|
152
|
-
const { sv, en } = headingsByLanguage(block)
|
|
199
|
+
const { sv, en } = headingsByLanguage(block, SPEC_HEADINGS)
|
|
153
200
|
problems.push(
|
|
154
201
|
`${label}: rubriker på två språk (sv: ${sv.join(', ')}, en: ${en.join(', ')}), välj ett`,
|
|
155
202
|
)
|
|
@@ -199,9 +246,15 @@ if (spec != null) {
|
|
|
199
246
|
// ---- granskarblock.md ------------------------------------------------------
|
|
200
247
|
const gb = read('granskarblock.md', true)
|
|
201
248
|
if (gb != null) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
249
|
+
// Felraden namnger literalen i granskarblockets EGET språk (S1, se
|
|
250
|
+
// harSvenskGranskarrubrik ovan): ett nytt dokument skrivs på engelska, och
|
|
251
|
+
// raden går in i RÄTTNINGAR vid omdispatch. Namnger den svenskan för en
|
|
252
|
+
// engelsk post skickar omdispatchen agenten tillbaka till det språk vi
|
|
253
|
+
// just lämnat.
|
|
254
|
+
const dokumentSprak = harSvenskGranskarrubrik(gb) ? 'sv' : 'en'
|
|
255
|
+
if (!tillGranskarenRe().test(gb))
|
|
256
|
+
problems.push(`granskarblock.md: rubriken "## ${TILL_GRANSKAREN[dokumentSprak]}" saknas`)
|
|
257
|
+
for (const key of [
|
|
205
258
|
'Antaganden',
|
|
206
259
|
'Vägval jag gjort',
|
|
207
260
|
'Öppna utfallsval',
|
|
@@ -209,17 +262,20 @@ if (gb != null) {
|
|
|
209
262
|
'Flaggor',
|
|
210
263
|
'Struket ur underlaget',
|
|
211
264
|
])
|
|
212
|
-
if (!
|
|
265
|
+
if (!granskareRadRe(key).test(gb))
|
|
266
|
+
problems.push(`granskarblock.md: raden "**${granskareRow(key)[dokumentSprak]}:**" saknas`)
|
|
213
267
|
// Prisavgörande öppet val (#505, ADR-0118): specifierns eget stoppsignal när
|
|
214
268
|
// ett öppet val avgör priset. Skrivs på stdout för orkestratorn, före
|
|
215
269
|
// KANDIDAT-raderna. En form som varken är nej eller ett ja med text (bart
|
|
216
270
|
// ja/yes, kolon utan text, fritext utan etikett) är en BRIST: den ser ut
|
|
217
271
|
// som ett svar men går inte att läsa, och ska aldrig tyst behandlas som
|
|
218
272
|
// nej (granskningsfynd, #505).
|
|
219
|
-
if (prisavgorandeForm(gb) === 'ja-utan-text')
|
|
273
|
+
if (prisavgorandeForm(gb) === 'ja-utan-text') {
|
|
274
|
+
const v = RATTNINGSVARDEN[dokumentSprak]
|
|
220
275
|
problems.push(
|
|
221
|
-
|
|
276
|
+
`granskarblock.md: raden "**${granskareRow('Prisavgörande öppet val')[dokumentSprak]}:**" ska vara "${v.nej}" eller "${v.ja}"`,
|
|
222
277
|
)
|
|
278
|
+
}
|
|
223
279
|
prisavgorande = prisavgorandeVal(gb)
|
|
224
280
|
}
|
|
225
281
|
|
|
@@ -227,35 +283,45 @@ if (gb != null) {
|
|
|
227
283
|
if (requireImpact) {
|
|
228
284
|
const impact = read('impact.md', true)
|
|
229
285
|
if (impact != null) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
286
|
+
// Rubrikerna kommer ur tabellen, aldrig egna literaler (ADR-0114-
|
|
287
|
+
// mekaniken, nu även för impact.md). Felraden namnger språket
|
|
288
|
+
// RUBRIKERNA faktiskt bär (impactHeadingLanguage), inte granskarblockets:
|
|
289
|
+
// specens och analysens tabeller ska inte kunna fälla varandra.
|
|
290
|
+
const impactSprak = impactHeadingLanguage(impact)
|
|
291
|
+
const rubrikSprak = impactSprak === 'sv' ? 'sv' : 'en'
|
|
292
|
+
for (const row of IMPACT_HEADINGS)
|
|
293
|
+
if (!headingRe(row.key).test(impact))
|
|
294
|
+
problems.push(`impact.md: rubriken "${row[rubrikSprak]}" saknas`)
|
|
295
|
+
if (impactSprak === 'blandad') {
|
|
296
|
+
const { sv, en } = headingsByLanguage(impact, IMPACT_HEADINGS)
|
|
297
|
+
problems.push(
|
|
298
|
+
`impact.md: rubrikerna är blandade (sv: ${sv.join(', ')}, en: ${en.join(', ')}), välj ett`,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
// Granskarblockets egna rader (Till granskaren-rubriken + de tre
|
|
302
|
+
// etikettraderna) namnger literalen i DOKUMENTETS eget språk (S1, se
|
|
303
|
+
// harSvenskGranskarrubrik ovan) — skilt från rubrikSprak ovan, som styr
|
|
304
|
+
// impact.md:s ämnesrubriker.
|
|
305
|
+
const dokumentSprak = harSvenskGranskarrubrik(impact) ? 'sv' : 'en'
|
|
306
|
+
if (!tillGranskarenRe().test(impact))
|
|
307
|
+
problems.push(`impact.md: "## ${TILL_GRANSKAREN[dokumentSprak]}" saknas`)
|
|
308
|
+
for (const key of ['Ej verifierat', 'Avvikelse mot specen', 'Obesvarade utfallsval'])
|
|
309
|
+
if (!granskareRadRe(key).test(impact))
|
|
310
|
+
problems.push(`impact.md: raden "**${granskareRow(key)[dokumentSprak]}:**" saknas`)
|
|
249
311
|
// Avvikelseetikett (ADR-0106): raden måste börja med en av de tre
|
|
250
312
|
// etiketterna, annars har scorern ingen väg att agera på den. Form, aldrig
|
|
251
313
|
// innehåll: etikettvärdet påverkar inte exitkoden, grinden för motsägelse
|
|
252
314
|
// ligger i orkestratorn och contract-check. En gammal analys utan etikett
|
|
253
315
|
// fälls också, kostnaden är en assessor-dispatch per pågående post.
|
|
254
|
-
if (
|
|
316
|
+
if (granskareRadRe('Avvikelse mot specen').test(impact)) {
|
|
317
|
+
// Literalerna raderna nedan namnger, i dokumentets språk (F2, som S1).
|
|
318
|
+
const v = RATTNINGSVARDEN[dokumentSprak]
|
|
319
|
+
const avvikelseRad = granskareRow('Avvikelse mot specen')[dokumentSprak]
|
|
320
|
+
const utfallsvalRad = granskareRow('Obesvarade utfallsval')[dokumentSprak]
|
|
255
321
|
avvikelse = avvikelseEtikett(impact)
|
|
256
322
|
if (avvikelse == null)
|
|
257
323
|
problems.push(
|
|
258
|
-
`impact.md: raden "
|
|
324
|
+
`impact.md: raden "**${avvikelseRad}:**" ska börja med en etikett: ${AVVIKELSE_ETIKETTER.map(v.etikett).join(', ')}`,
|
|
259
325
|
)
|
|
260
326
|
// #507 (ADR-0117): motsägelse betyder att inget listat alternativ
|
|
261
327
|
// levererar specens Problem. Bär analysen ändå alternativ är etiketten
|
|
@@ -263,7 +329,7 @@ if (requireImpact) {
|
|
|
263
329
|
const alternativ = utfallsvalAlternativ(impact)
|
|
264
330
|
if (avvikelse === 'motsägelse' && alternativ !== null && alternativ > 0)
|
|
265
331
|
problems.push(
|
|
266
|
-
`impact.md: motsägelse med ${alternativ} listade alternativ under "
|
|
332
|
+
`impact.md: ${v.etikett('motsägelse')} med ${alternativ} listade alternativ under "${utfallsvalRad}": välj ${v.etikett('precisering')} och låt fyrstegsregeln avgöra, eller stryk alternativen med prefixet "${v.struket}" och skälet`,
|
|
267
333
|
)
|
|
268
334
|
// #520: alternativ i löptext ("(a) … (b) …") räknas inte som listade
|
|
269
335
|
// och passerade grinden ovan som om raden vore "inga". Formen krävs:
|
|
@@ -271,14 +337,13 @@ if (requireImpact) {
|
|
|
271
337
|
// etiketten är rätt), vid precisering en VARNING (scorerns
|
|
272
338
|
// fyrstegsregel läser listan, prosa läser den inte). Aldrig en
|
|
273
339
|
// tolkning av vad "(a)" betyder.
|
|
274
|
-
const formkrav =
|
|
275
|
-
'skriv en listpunkt per alternativ på kolumn 0 ("- …"), stryk med prefixet "struket:" och skälet, eller skriv "inga"'
|
|
340
|
+
const formkrav = `skriv en listpunkt per alternativ på kolumn 0 ("- …"), stryk med prefixet "${v.struket}" och skälet, eller skriv "${v.inga}"`
|
|
276
341
|
if (
|
|
277
342
|
(avvikelse === 'motsägelse' || avvikelse === 'precisering') &&
|
|
278
343
|
utfallsvalProsa(impact) === true
|
|
279
344
|
)
|
|
280
345
|
(avvikelse === 'motsägelse' ? problems : warnings).push(
|
|
281
|
-
`impact.md: ${avvikelse} med alternativ i löptext under "
|
|
346
|
+
`impact.md: ${v.etikett(avvikelse)} med alternativ i löptext under "${utfallsvalRad}": ${formkrav}`,
|
|
282
347
|
)
|
|
283
348
|
}
|
|
284
349
|
// Analysen får inte läcka poängsättning eller tid (ADR-0098: kontrollen
|
|
@@ -287,7 +352,7 @@ if (requireImpact) {
|
|
|
287
352
|
// Poängord är dock en heuristik — "poängen med ändringen" och ett
|
|
288
353
|
// ordagrant citerat kod-enum är legitima — så träffar blir varningar för
|
|
289
354
|
// mänsklig blick, aldrig hårda stopp. Tidsord förblir hårda.
|
|
290
|
-
const m = impact.match(
|
|
355
|
+
const m = impact.match(POANGORD_RE)
|
|
291
356
|
if (m)
|
|
292
357
|
warnings.push(
|
|
293
358
|
`impact.md: poängord "${m[0]}" — kontrollera att analysen inte läcker poängsättning`,
|