@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.
@@ -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 = '2e26f9270ced172cd9903f53384a4f7ad0f972084045040c263bf01dc7a6e422'
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, så reglerna aldrig glider isär.
64
- const TIDSORD_RE = /[\p{L}]*tim(?:me|mar|marna|men)[\p{L}]*|[\p{L}]*estim(?:at|er)[\p{L}]*/iu
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. Literalerna kommer ur
91
- * SPEC_HEADINGS, aldrig härifrån; Problem är neutral och räknas inte. */
92
- const headingsByLanguage = (block) => {
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 SPEC_HEADINGS) {
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
- if (!gb.includes('## Till granskaren'))
203
- problems.push('granskarblock.md: rubriken "## Till granskaren" saknas')
204
- for (const row of [
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 (!gb.includes(`**${row}:**`)) problems.push(`granskarblock.md: raden "**${row}:**" saknas`)
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
- 'granskarblock.md: raden "**Prisavgörande öppet val:**" ska vara "nej" eller "ja: <valet i en mening>"',
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
- const headings = [
231
- 'Berörda repon och filer',
232
- 'Datalagring',
233
- 'Integrationspunkter',
234
- 'Flöden',
235
- 'Gränssnitt',
236
- 'Verifiering',
237
- 'Delbarhet',
238
- 'Konsekvens av fel',
239
- 'Vägval',
240
- 'Antaganden',
241
- 'Beroenden',
242
- ]
243
- for (const h of headings)
244
- if (!headingRe(h).test(impact)) problems.push(`impact.md: rubriken "${h}" saknas`)
245
- if (!impact.includes('## Till granskaren'))
246
- problems.push('impact.md: "## Till granskaren" saknas')
247
- for (const row of ['Ej verifierat', 'Avvikelse mot specen', 'Obesvarade utfallsval'])
248
- if (!impact.includes(`**${row}:**`)) problems.push(`impact.md: raden "**${row}:**" saknas`)
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 (impact.includes('**Avvikelse mot specen:**')) {
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 "**Avvikelse mot specen:**" ska börja med en etikett: ${AVVIKELSE_ETIKETTER.join(', ')}`,
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 "Obesvarade utfallsval": välj precisering och låt fyrstegsregeln avgöra, eller stryk alternativen med prefixet "struket:" och skälet`,
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 "Obesvarade utfallsval": ${formkrav}`,
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(W('poäng(?:en|er|sätt\\p{L}*|sättning\\p{L}*)?'))
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`,