@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
|
@@ -50,6 +50,8 @@ import {
|
|
|
50
50
|
CLASS_UTFALL,
|
|
51
51
|
SCORE_OUTCOMES,
|
|
52
52
|
CUSTOMER_BLACKLIST,
|
|
53
|
+
CUSTOMER_BLACKLIST_TVETYDIG,
|
|
54
|
+
TVA_SIGNAL_RE,
|
|
53
55
|
MODES,
|
|
54
56
|
STEP_RELATIONS,
|
|
55
57
|
KONFIDENS,
|
|
@@ -239,7 +241,12 @@ const ANCHOR_RE = /(?<![\p{L}\d_])A(?:1[0-7]|[1-9])(?![\p{L}\d_])/u
|
|
|
239
241
|
//
|
|
240
242
|
// "poängen är att" behöver ingen lookahead: "är att" följs alltid av en
|
|
241
243
|
// sats, aldrig av en poängangivelse.
|
|
242
|
-
const IDIOM_ALLOW = [
|
|
244
|
+
const IDIOM_ALLOW = [
|
|
245
|
+
/hela\s+poängen\s+(?:med|i)\b(?![^.!?]*\d)/giu,
|
|
246
|
+
/poängen\s+är\s+att\b/giu,
|
|
247
|
+
/the\s+whole\s+point\s+of\b(?![^.!?]*\d)/giu,
|
|
248
|
+
/the\s+point\s+is\s+that\b/giu,
|
|
249
|
+
]
|
|
243
250
|
const maskIdioms = (text) =>
|
|
244
251
|
IDIOM_ALLOW.reduce((t, re) => t.replace(re, (m) => '�'.repeat(m.length)), text)
|
|
245
252
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
@@ -261,6 +268,15 @@ const checkCustomerText = (text, field) => {
|
|
|
261
268
|
const m = masked.match(re)
|
|
262
269
|
if (m && !iUnderlaget(m[0])) problems.push(`${field}: protokollterm "${m[0]}" i kundtext`)
|
|
263
270
|
}
|
|
271
|
+
// Tvetydig tier: ordet ensamt är legitim engelska. Radformen är identisk med
|
|
272
|
+
// den entydiga tiern, så kontroll.mjs KUNDTEXT_RE väljer kundtextfixen för
|
|
273
|
+
// båda.
|
|
274
|
+
if (TVA_SIGNAL_RE.test(masked)) {
|
|
275
|
+
for (const re of CUSTOMER_BLACKLIST_TVETYDIG) {
|
|
276
|
+
const m = masked.match(re)
|
|
277
|
+
if (m && !iUnderlaget(m[0])) problems.push(`${field}: protokollterm "${m[0]}" i kundtext`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
264
280
|
// Ankarvarningen körs mot ORIGINALET, aldrig mot den maskade strängen.
|
|
265
281
|
// Maskningen bevarar längden men inte innehållet, och varningscitatet ska
|
|
266
282
|
// visa vad som faktiskt stod i texten.
|
|
@@ -310,12 +310,14 @@ export function hamtaFalt(doc, falt) {
|
|
|
310
310
|
|
|
311
311
|
export function omskrivningsPrompt(text, term) {
|
|
312
312
|
return [
|
|
313
|
-
'
|
|
313
|
+
'Rewrite the following customer-facing text so that the word "' +
|
|
314
314
|
term +
|
|
315
|
-
'" (
|
|
316
|
-
'
|
|
317
|
-
'
|
|
318
|
-
'
|
|
315
|
+
'" (and inflections of it) does not appear.',
|
|
316
|
+
'Keep the meaning and the length. Do not translate the text: answer in the same',
|
|
317
|
+
'language it is written in. Use no words that describe how a price was derived',
|
|
318
|
+
'(score, anchor, scale, reference assignment, bracketing, confidence, base class,',
|
|
319
|
+
'protocol; poäng, ankare, skala, referensuppdrag, inringning, konfidens, klass, protokoll).',
|
|
320
|
+
'Answer with the rewritten text only, no quotation marks and no commentary.',
|
|
319
321
|
'',
|
|
320
322
|
text,
|
|
321
323
|
].join('\n')
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
claudeDir,
|
|
58
58
|
canonicalModel,
|
|
59
59
|
SPEC_HEADINGS,
|
|
60
|
+
IMPACT_HEADINGS,
|
|
60
61
|
SCORE_OUTCOMES,
|
|
61
62
|
} from './lib.mjs'
|
|
62
63
|
|
|
@@ -228,6 +229,15 @@ export function assessorPrompt({ repoRot, deps, sprak, spec, granskarblock, mani
|
|
|
228
229
|
`REPO-ROT: ${repoRot}`,
|
|
229
230
|
`DEPS: ${deps}`,
|
|
230
231
|
sprakRad,
|
|
232
|
+
// Analysen är intern och skrivs alltid på engelska: assessorskillen
|
|
233
|
+
// föreskriver IMPACT_HEADINGS:s engelska literaler, och skelettet är
|
|
234
|
+
// därför den engelska kolumnen. Det är oberoende av SPRÅK, som bara styr
|
|
235
|
+
// analysens kundvända delar, inte denna interna struktur. Skelettet finns
|
|
236
|
+
// av samma skäl som specifierns (ADR-0114): modellen ska aldrig gissa
|
|
237
|
+
// literalen, och en gissning kostar en omdispatch. Arkivets svenska
|
|
238
|
+
// analyser läses fortfarande, headingRe matchar båda kolumnerna.
|
|
239
|
+
'RUBRIKER:',
|
|
240
|
+
...IMPACT_HEADINGS.map((row) => `**${row.en}**`),
|
|
231
241
|
'KÄLLOR:',
|
|
232
242
|
manifest.trim(),
|
|
233
243
|
'',
|
|
@@ -858,10 +868,13 @@ export const granskaBuntBlock = ({ specar, delat }) => {
|
|
|
858
868
|
const varningar = []
|
|
859
869
|
if (delat && specar.length > 1)
|
|
860
870
|
brister.push(
|
|
861
|
-
'- granskarblock delat över bunten, ett per spec krävs: skriv "
|
|
871
|
+
'- granskarblock delat över bunten, ett per spec krävs: skriv rubriken "For the reviewer" (svenska: "Till granskaren") direkt efter varje spec',
|
|
862
872
|
)
|
|
863
873
|
specar.forEach((s, i) => {
|
|
864
|
-
if (s.granskarblock === null)
|
|
874
|
+
if (s.granskarblock === null)
|
|
875
|
+
brister.push(
|
|
876
|
+
`- spec ${i + 1} saknar granskarblock ("For the reviewer", svenska: "Till granskaren")`,
|
|
877
|
+
)
|
|
865
878
|
})
|
|
866
879
|
for (let i = 0; i < specar.length; i++)
|
|
867
880
|
for (let j = i + 1; j < specar.length; j++)
|
|
@@ -940,7 +953,7 @@ function skrivSpec(del, spec, granskarblock, meta, ctx) {
|
|
|
940
953
|
writeFileSync(path.join(del.dir, 'spec.md'), spec.trim() + '\n')
|
|
941
954
|
if (granskarblock)
|
|
942
955
|
writeFileSync(path.join(del.dir, 'granskarblock.md'), granskarblock.trim() + '\n')
|
|
943
|
-
else logg(`VARNING ${path.basename(del.dir)}: svaret bar inget
|
|
956
|
+
else logg(`VARNING ${path.basename(del.dir)}: svaret bar inget granskarblock (For the reviewer)`)
|
|
944
957
|
addRun(
|
|
945
958
|
del.dir,
|
|
946
959
|
{
|
|
@@ -266,7 +266,7 @@ export const ankarPoang = (id) => ANKAR_INDEX.get(id)?.poang ?? null
|
|
|
266
266
|
export const toppAnkare = () => ANKARE[ANKARE.length - 1][0]
|
|
267
267
|
|
|
268
268
|
/** Uppdragstyperna ur översiktstabellen. `type` i score-kuvertet loggas för att
|
|
269
|
-
* mäta systematisk felplacering per typ (request-score/SKILL.md,
|
|
269
|
+
* mäta systematisk felplacering per typ (request-score/SKILL.md, The type), vilket
|
|
270
270
|
* förutsätter ett slutet värdeförråd (#509). */
|
|
271
271
|
export const TYPER = [
|
|
272
272
|
'Kundvänd funktion',
|
|
@@ -285,7 +285,6 @@ const B = (pat) => new RegExp(`(?<![\\p{L}\\d_])(?:${pat})`, 'iu')
|
|
|
285
285
|
export const CUSTOMER_BLACKLIST = [
|
|
286
286
|
B('poäng\\p{L}*'),
|
|
287
287
|
B('basklass\\p{L}*'),
|
|
288
|
-
B('bump\\p{L}*'),
|
|
289
288
|
B('ankar\\p{L}*'),
|
|
290
289
|
B('referensuppdrag\\p{L}*'),
|
|
291
290
|
B('inringning\\p{L}*'),
|
|
@@ -300,8 +299,46 @@ export const CUSTOMER_BLACKLIST = [
|
|
|
300
299
|
// u-flaggan är obligatorisk: utan den är \p{L} i lookaround teckenklassen
|
|
301
300
|
// "p{L" och "era B2B-kunder" blir ett falskt BROTT.
|
|
302
301
|
/(?<![\p{L}\d_])[SB][1-3](?![\p{L}\d_])/u,
|
|
302
|
+
// Engelska motsvarigheter (requestmotor engelska artefakter, spec §3):
|
|
303
|
+
// entydiga termer som fäller ensamma, precis som de svenska ovan.
|
|
304
|
+
B('reference\\s+assignment\\p{L}*'),
|
|
305
|
+
B('base\\s+class\\p{L}*'),
|
|
306
|
+
B('bracketing'),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Tvetydig tier (spec §3): ord som är protokollmekanik hos oss och vanlig
|
|
311
|
+
* engelska hos kunden ("scale the component", "a confidence interval"). De
|
|
312
|
+
* fäller bara tillsammans med en andra signal i samma fält — ett poängformat
|
|
313
|
+
* tal eller ett ankar-id. Ett falskt utslag kostar en omdispatch, ett missat
|
|
314
|
+
* läckage når kunden, och basfrekvensen är hög: därför två signaler, samma
|
|
315
|
+
* logik som tvåordsgränsen i språkgrinden (ADR-0122). "bump" hör hemma här
|
|
316
|
+
* (inte i den entydiga tiern ovan): ordet är vanlig engelska ("bump the
|
|
317
|
+
* version") lika ofta som det är protokollmekanik. Mönstret är däremot
|
|
318
|
+
* `bump\p{L}*`, inte `bumps?`: kundens text kan stå på svenska även när
|
|
319
|
+
* motorns egen prosa är engelsk, och som `bumps?` hade bumpa, bumpade och
|
|
320
|
+
* bumpning varit ofångbara vid varje signalnivå (ADR-0129).
|
|
321
|
+
*/
|
|
322
|
+
export const CUSTOMER_BLACKLIST_TVETYDIG = [
|
|
323
|
+
B('scores?(?![\\p{L}\\d_])'),
|
|
324
|
+
B('scoring(?![\\p{L}\\d_])'),
|
|
325
|
+
B('anchors?(?![\\p{L}\\d_])'),
|
|
326
|
+
B('scales?(?![\\p{L}\\d_])'),
|
|
327
|
+
B('confidence(?![\\p{L}\\d_])'),
|
|
328
|
+
B('bump\\p{L}*'),
|
|
303
329
|
]
|
|
304
330
|
|
|
331
|
+
/** Den andra signalen: ett ankar-id (A1-A17, alltid VERSAL A — en gemen "a"
|
|
332
|
+
* som i "a4 paper" räknas inte, lookbehind ger ingen skiftlägesfrihet här),
|
|
333
|
+
* ett poängformat tal i 0,25-steg (2.5, 2,75, 2.50 …), eller valfritt tal
|
|
334
|
+
* följt av "point(s)"/"poäng". Decimalgrenen är klämd mellan två speglade
|
|
335
|
+
* vakter, `(?<!\d\.)` och `(?!\.\d)`, så inget segment av ett punktat
|
|
336
|
+
* versionsnummer kan bli den andra signalen: varken sista ledet ("2.5.1")
|
|
337
|
+
* eller ett mellanled ("release 10.50.25", "v1.2.75"). Ett ensamt "2,75"
|
|
338
|
+
* eller "2.5" är oberört. */
|
|
339
|
+
export const TVA_SIGNAL_RE =
|
|
340
|
+
/(?<![\p{L}\d_])(?:A(?:1[0-7]|[1-9])|(?<!\d\.)\d+[.,](?:25|5|50|75)(?!\.\d)|\d+\s*(?:[Pp]oints?|[Pp]oäng))(?![\p{L}\d_])/u
|
|
341
|
+
|
|
305
342
|
/** Rubrikrad: fetstilsrad eller markdown-rubrik som utgör hela raden. Delad
|
|
306
343
|
* mellan precheck (formkontroll) och report (fältvis jämförelse) så rapporten
|
|
307
344
|
* läser specen med exakt samma regler som grinden. */
|
|
@@ -327,6 +364,23 @@ export const SPEC_HEADINGS = [
|
|
|
327
364
|
{ key: 'Villkor', sv: 'Villkor', en: 'Conditions' },
|
|
328
365
|
]
|
|
329
366
|
|
|
367
|
+
/** Påverkansanalysens rubriker (ADR-0114:s mekanism, nu även för impact.md).
|
|
368
|
+
* Analysen är intern och skrivs på engelska; den svenska literalen står kvar
|
|
369
|
+
* så att arkivet och pågående poster fortfarande läses. */
|
|
370
|
+
export const IMPACT_HEADINGS = [
|
|
371
|
+
{ key: 'Berörda repon och filer', sv: 'Berörda repon och filer', en: 'Affected repos and files' },
|
|
372
|
+
{ key: 'Datalagring', sv: 'Datalagring', en: 'Data storage' },
|
|
373
|
+
{ key: 'Integrationspunkter', sv: 'Integrationspunkter', en: 'Integration points' },
|
|
374
|
+
{ key: 'Flöden', sv: 'Flöden', en: 'Flows' },
|
|
375
|
+
{ key: 'Gränssnitt', sv: 'Gränssnitt', en: 'Interfaces' },
|
|
376
|
+
{ key: 'Verifiering', sv: 'Verifiering', en: 'Verification' },
|
|
377
|
+
{ key: 'Delbarhet', sv: 'Delbarhet', en: 'Splittability' },
|
|
378
|
+
{ key: 'Konsekvens av fel', sv: 'Konsekvens av fel', en: 'Consequence of failure' },
|
|
379
|
+
{ key: 'Vägval', sv: 'Vägval', en: 'Decisions taken' },
|
|
380
|
+
{ key: 'Antaganden', sv: 'Antaganden', en: 'Assumptions' },
|
|
381
|
+
{ key: 'Beroenden', sv: 'Beroenden', en: 'Dependencies' },
|
|
382
|
+
]
|
|
383
|
+
|
|
330
384
|
/** Kandidatformens markörrad (ADR-0105): står direkt under titeln i stället
|
|
331
385
|
* för Lösning och När det är klart, och routar posten till sitt stopputfall
|
|
332
386
|
* utan analys eller poäng. */
|
|
@@ -335,6 +389,23 @@ export const KANDIDAT_MARKORER = [
|
|
|
335
389
|
{ utfall: 'DIAGNOS', sv: 'Diagnos-kandidat', en: 'Diagnosis candidate' },
|
|
336
390
|
]
|
|
337
391
|
|
|
392
|
+
/** Tidsord förbjuds i både spec och analys (precheck: BRISTER). EN regex, så
|
|
393
|
+
* reglerna aldrig glider isär, och den bor här i stället för i precheck.mjs så
|
|
394
|
+
* att driftvakten kan läsa samma regex som grinden: en skill som själv primar
|
|
395
|
+
* modellen med ett ord grinden fäller (uppgift 13: "estimated") fälls i
|
|
396
|
+
* testet, inte i nästa skarpa körning. Båda språken, eftersom interna
|
|
397
|
+
* dokument nu skrivs på engelska. Tidsord matchas utan inledande gräns så
|
|
398
|
+
* sammansättningar (arbetstimmar, halvtimme) fälls. */
|
|
399
|
+
export const TIDSORD_RE =
|
|
400
|
+
/[\p{L}]*tim(?:me|mar|marna|men)[\p{L}]*|[\p{L}]*estim(?:at|er)[\p{L}]*|(?<![\p{L}\d_])hours?(?![\p{L}\d_])|(?<![\p{L}\d_])man-?days?(?![\p{L}\d_])/iu
|
|
401
|
+
|
|
402
|
+
/** Poängord i analysen (precheck: VARNING, aldrig brist — "poängen med
|
|
403
|
+
* ändringen" och ett ordagrant citerat kod-enum är legitima). Unicode-medvetna
|
|
404
|
+
* gränser: JS \b bryter vid å/ä/ö. Delad med driftvakten av samma skäl som
|
|
405
|
+
* TIDSORD_RE. */
|
|
406
|
+
export const POANGORD_RE =
|
|
407
|
+
/(?<![\p{L}\d_])(?:poäng(?:en|er|sätt\p{L}*|sättning\p{L}*)?|scor(?:e|es|ing))(?![\p{L}\d_])/iu
|
|
408
|
+
|
|
338
409
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
339
410
|
|
|
340
411
|
/** Regex som matchar en hel rubrikrad för NÅGON av literalerna. */
|
|
@@ -346,10 +417,10 @@ const literalsRe = (literals, flags = 'm') => {
|
|
|
346
417
|
return new RegExp(`^(\\*\\*(?:${alt})\\*\\*|#{1,6}\\s+(?:${alt}))\\s*$`, flags)
|
|
347
418
|
}
|
|
348
419
|
|
|
349
|
-
/** Rubrikregex för en nyckel: en
|
|
350
|
-
* en okänd nyckel
|
|
420
|
+
/** Rubrikregex för en nyckel: en spec- eller impact-rubrik matchar båda
|
|
421
|
+
* språkens literal, en okänd nyckel matchas som literal precis som förut. */
|
|
351
422
|
export const headingRe = (h) => {
|
|
352
|
-
const row = SPEC_HEADINGS.find((r) => r.key === h)
|
|
423
|
+
const row = SPEC_HEADINGS.find((r) => r.key === h) ?? IMPACT_HEADINGS.find((r) => r.key === h)
|
|
353
424
|
return literalsRe(row ? [row.sv, row.en] : [h])
|
|
354
425
|
}
|
|
355
426
|
|
|
@@ -374,6 +445,23 @@ export const headingLanguage = (block) => {
|
|
|
374
445
|
return found.size === 2 ? 'blandad' : [...found][0]
|
|
375
446
|
}
|
|
376
447
|
|
|
448
|
+
/** Som headingLanguage, fast över IMPACT_HEADINGS. Egen funktion, aldrig en
|
|
449
|
+
* flagga på headingLanguage: specens och analysens tabeller ska inte kunna
|
|
450
|
+
* fälla varandra när ett dokument citerar den andras rubrik. */
|
|
451
|
+
export const impactHeadingLanguage = (block) => {
|
|
452
|
+
const found = new Set()
|
|
453
|
+
for (const line of block.split('\n')) {
|
|
454
|
+
const t = line.trim()
|
|
455
|
+
for (const row of IMPACT_HEADINGS) {
|
|
456
|
+
if (row.sv === row.en) continue
|
|
457
|
+
if (literalsRe([row.sv], '').test(t)) found.add('sv')
|
|
458
|
+
else if (literalsRe([row.en], '').test(t)) found.add('en')
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (found.size === 0) return null
|
|
462
|
+
return found.size === 2 ? 'blandad' : [...found][0]
|
|
463
|
+
}
|
|
464
|
+
|
|
377
465
|
/** Kandidatmarkören i ett block: { utfall, line } eller null. Bara en hel
|
|
378
466
|
* rubrikrad räknas, aldrig ett omnämnande i löptext. */
|
|
379
467
|
export const kandidatIn = (block) => {
|
|
@@ -386,28 +474,88 @@ export const kandidatIn = (block) => {
|
|
|
386
474
|
return null
|
|
387
475
|
}
|
|
388
476
|
|
|
477
|
+
export const TILL_GRANSKAREN = { sv: 'Till granskaren', en: 'For the reviewer' }
|
|
478
|
+
|
|
479
|
+
/** Granskarblockets etikettrader, `{ key, sv, en }` per rad — nyckeln är den
|
|
480
|
+
* svenska literalen som anroparna alltid använt. Tabellen är den enda platsen
|
|
481
|
+
* för literalerna; UTFALLSVAL_RAD och PRISAVGORANDE_RAD härleds ur den så att
|
|
482
|
+
* de befintliga konsumenterna i lib.mjs behåller sin form. Frusen: raderna
|
|
483
|
+
* läses av precheck, korning och kontroll, och en muterad rad hade tyst
|
|
484
|
+
* ändrat vad grindarna kräver. */
|
|
485
|
+
export const GRANSKARE_ROWS = Object.freeze(
|
|
486
|
+
[
|
|
487
|
+
{ key: 'Antaganden', sv: 'Antaganden', en: 'Assumptions' },
|
|
488
|
+
{ key: 'Vägval jag gjort', sv: 'Vägval jag gjort', en: 'Decisions I made' },
|
|
489
|
+
{ key: 'Öppna utfallsval', sv: 'Öppna utfallsval', en: 'Open outcome choices' },
|
|
490
|
+
{
|
|
491
|
+
key: 'Prisavgörande öppet val',
|
|
492
|
+
sv: 'Prisavgörande öppet val',
|
|
493
|
+
en: 'Price-decisive open choice',
|
|
494
|
+
},
|
|
495
|
+
{ key: 'Flaggor', sv: 'Flaggor', en: 'Flags' },
|
|
496
|
+
{ key: 'Struket ur underlaget', sv: 'Struket ur underlaget', en: 'Struck from the source' },
|
|
497
|
+
{ key: 'Ej verifierat', sv: 'Ej verifierat', en: 'Not verified' },
|
|
498
|
+
{ key: 'Avvikelse mot specen', sv: 'Avvikelse mot specen', en: 'Deviation from the spec' },
|
|
499
|
+
{
|
|
500
|
+
key: 'Obesvarade utfallsval',
|
|
501
|
+
sv: 'Obesvarade utfallsval',
|
|
502
|
+
en: 'Unanswered outcome choices',
|
|
503
|
+
},
|
|
504
|
+
].map((row) => Object.freeze(row)),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
export const granskareRow = (key) => {
|
|
508
|
+
const row = GRANSKARE_ROWS.find((r) => r.key === key)
|
|
509
|
+
if (!row) throw new Error(`okänd granskarblocksrad: ${key}`)
|
|
510
|
+
return row
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Regex för en granskarblocksrad `**Etikett:**` i BÅDA språken. Fetstilen och
|
|
514
|
+
* kolonet är formkravet: "Flaggor: inga" utan fetstil är inte raden. */
|
|
515
|
+
export const granskareRadRe = (key) => {
|
|
516
|
+
const row = granskareRow(key)
|
|
517
|
+
const alt = [...new Set([row.sv, row.en])].map(escapeRe).join('|')
|
|
518
|
+
return new RegExp(`^\\s*\\*\\*(?:${alt}):\\*\\*`, 'imu')
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Regex för rubriken `## Till granskaren` i båda språken. */
|
|
522
|
+
export const tillGranskarenRe = () => {
|
|
523
|
+
const alt = [TILL_GRANSKAREN.sv, TILL_GRANSKAREN.en].map(escapeRe).join('|')
|
|
524
|
+
return new RegExp(`^#{1,6}\\s+(?:${alt})\\s*$`, 'imu')
|
|
525
|
+
}
|
|
526
|
+
|
|
389
527
|
export const AVVIKELSE_ETIKETTER = ['ingen', 'precisering', 'motsägelse']
|
|
390
528
|
|
|
529
|
+
/** Engelsk literal -> svensk nyckel. Nyckeln är kanonisk i hela motorn:
|
|
530
|
+
* contract-check, kontroll, korning och archive jämför mot den svenska. */
|
|
531
|
+
export const AVVIKELSE_ALIAS = {
|
|
532
|
+
ingen: 'ingen',
|
|
533
|
+
precisering: 'precisering',
|
|
534
|
+
motsägelse: 'motsägelse',
|
|
535
|
+
none: 'ingen',
|
|
536
|
+
refinement: 'precisering',
|
|
537
|
+
contradiction: 'motsägelse',
|
|
538
|
+
}
|
|
539
|
+
|
|
391
540
|
/**
|
|
392
|
-
* Etiketten på analysens rad `**Avvikelse mot specen:**`
|
|
393
|
-
* direkt efter kolonet, skiftlägesokänsligt,
|
|
394
|
-
* skiljetecken
|
|
395
|
-
*
|
|
541
|
+
* Etiketten på analysens rad `**Avvikelse mot specen:**` / `**Deviation from
|
|
542
|
+
* the spec:**` (ADR-0106): ordet direkt efter kolonet, skiftlägesokänsligt,
|
|
543
|
+
* följt av radslut eller ett skiljetecken. Fritext utan etikett ger null.
|
|
544
|
+
* RADETIKETTEN är tvåspråkig, inte bara värdet: missas den går en engelsk
|
|
545
|
+
* motsägelse tyst förbi precheck:s includes-grind till poängsättning.
|
|
396
546
|
*/
|
|
397
547
|
export const avvikelseEtikett = (text) => {
|
|
398
|
-
const
|
|
548
|
+
const rad = granskareRadRe('Avvikelse mot specen').source
|
|
549
|
+
const m = new RegExp(`${rad}\\s*(\\p{L}+)(?=$|[\\s.,;:!?])`, 'imu').exec(String(text))
|
|
399
550
|
if (!m) return null
|
|
400
551
|
const ord = m[1].toLowerCase()
|
|
401
|
-
return
|
|
552
|
+
return Object.hasOwn(AVVIKELSE_ALIAS, ord) ? AVVIKELSE_ALIAS[ord] : null
|
|
402
553
|
}
|
|
403
554
|
|
|
404
|
-
export const UTFALLSVAL_RAD =
|
|
555
|
+
export const UTFALLSVAL_RAD = granskareRow('Obesvarade utfallsval')
|
|
405
556
|
export const STRUKET_PREFIX = ['struket:', 'struck:']
|
|
406
557
|
|
|
407
|
-
export const PRISAVGORANDE_RAD =
|
|
408
|
-
sv: 'Prisavgörande öppet val',
|
|
409
|
-
en: 'Price-decisive open choice',
|
|
410
|
-
}
|
|
558
|
+
export const PRISAVGORANDE_RAD = granskareRow('Prisavgörande öppet val')
|
|
411
559
|
|
|
412
560
|
/** Texten efter `ja:`/`yes:` på raden Prisavgörande öppet val, annars null.
|
|
413
561
|
* Raden är specifierns eget stoppsignal (#505, ADR-0118): ett val som
|
|
@@ -583,7 +731,7 @@ export const sectionLines = (block, heading) => {
|
|
|
583
731
|
* prövar resultatet av. */
|
|
584
732
|
export function delaSpecifierSvar(body) {
|
|
585
733
|
const lines = String(body).replace(/\r\n/g, '\n').split('\n')
|
|
586
|
-
const isGranskare = (l) =>
|
|
734
|
+
const isGranskare = (l) => tillGranskarenRe().test(l.trim())
|
|
587
735
|
const isSpecHeading = (l) => SPEC_HEADINGS.some((row) => headingRe(row.key).test(l.trim()))
|
|
588
736
|
const isTitle = (l) =>
|
|
589
737
|
isHeadingLine(l) && !isSpecHeading(l) && kandidatIn(l) === null && !isGranskare(l)
|
|
@@ -728,3 +876,127 @@ export async function schemalagg(tickets, taket, korTicket) {
|
|
|
728
876
|
await Promise.all(Array.from({ length: Math.min(taket, tickets.length) }, arbetare))
|
|
729
877
|
return resultat
|
|
730
878
|
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Literaler som ALDRIG översätts när motorns prosa skrivs om till engelska
|
|
882
|
+
* (spec §4). De är identifierare, inte språk: script jämför dem literalt, och
|
|
883
|
+
* ett byte bryter arkivets uppspelbarhet eller Asanas uppslag. Registret har
|
|
884
|
+
* två uppgifter — det säger vad omskrivningen ska citera ordagrant, och
|
|
885
|
+
* språkvakten (tests/request-sprakkontrakt.test.js) stryker dem ur texten
|
|
886
|
+
* innan den mäter språk. En literal som bara finns på ett av ställena hade
|
|
887
|
+
* gjort vakten fel, därför bor listan här.
|
|
888
|
+
*
|
|
889
|
+
* `rubriker` härleds ur de tvåspråkiga tabellernas svenska kolumn i stället
|
|
890
|
+
* för att kopieras för hand: SPEC_HEADINGS, IMPACT_HEADINGS, GRANSKARE_ROWS,
|
|
891
|
+
* TILL_GRANSKAREN, KANDIDAT_MARKORER, AVVIKELSE_ETIKETTER och STRUKET_PREFIX:s
|
|
892
|
+
* svenska entré. Tabellerna är sanningen; en handkopierad lista hade kunnat
|
|
893
|
+
* glida isär från dem tyst.
|
|
894
|
+
*/
|
|
895
|
+
const RUBRIK_LITERALER = [
|
|
896
|
+
...SPEC_HEADINGS.map((r) => r.sv),
|
|
897
|
+
...IMPACT_HEADINGS.map((r) => r.sv),
|
|
898
|
+
...GRANSKARE_ROWS.map((r) => r.sv),
|
|
899
|
+
TILL_GRANSKAREN.sv,
|
|
900
|
+
...KANDIDAT_MARKORER.map((m) => m.sv),
|
|
901
|
+
...AVVIKELSE_ETIKETTER,
|
|
902
|
+
STRUKET_PREFIX[0],
|
|
903
|
+
]
|
|
904
|
+
|
|
905
|
+
export const FRYSTA_LITERALER = Object.freeze({
|
|
906
|
+
promptnycklar: Object.freeze([
|
|
907
|
+
'SPRÅK:',
|
|
908
|
+
'KÄLLOR:',
|
|
909
|
+
'REPO-ROT:',
|
|
910
|
+
'DEPS:',
|
|
911
|
+
'MODE:',
|
|
912
|
+
'DELPOST:',
|
|
913
|
+
'ESKALERAD:',
|
|
914
|
+
'REQUEST_ID:',
|
|
915
|
+
'RUBRIKER:',
|
|
916
|
+
'LÄSTA:',
|
|
917
|
+
'MODELL:',
|
|
918
|
+
'AVVIKELSE:',
|
|
919
|
+
'RÄTTNINGAR:',
|
|
920
|
+
'ÖNSKEMÅL:',
|
|
921
|
+
'SPEC:',
|
|
922
|
+
'PÅVERKANSANALYS:',
|
|
923
|
+
'SPEC (godkänd):',
|
|
924
|
+
'GRANSKARBLOCK',
|
|
925
|
+
'GRANSKARBLOCK (internt underlag, följer aldrig med i analysens kundtext):',
|
|
926
|
+
'FULLPLAN:',
|
|
927
|
+
'TIDIGARE_POANG:',
|
|
928
|
+
]),
|
|
929
|
+
promptvarden: Object.freeze([
|
|
930
|
+
'installerade',
|
|
931
|
+
'saknas',
|
|
932
|
+
'ja',
|
|
933
|
+
'nej',
|
|
934
|
+
'inga',
|
|
935
|
+
'svenska',
|
|
936
|
+
'engelska',
|
|
937
|
+
'ja, del N av M',
|
|
938
|
+
]),
|
|
939
|
+
jsonEnum: Object.freeze([
|
|
940
|
+
...SCORE_OUTCOMES,
|
|
941
|
+
...STEP_RELATIONS,
|
|
942
|
+
...KONFIDENS,
|
|
943
|
+
...MODES,
|
|
944
|
+
...TYPER,
|
|
945
|
+
'grund',
|
|
946
|
+
'yta',
|
|
947
|
+
'omfattning',
|
|
948
|
+
'ankare',
|
|
949
|
+
'kalla',
|
|
950
|
+
'delstrang',
|
|
951
|
+
'citat',
|
|
952
|
+
'pastaende',
|
|
953
|
+
]),
|
|
954
|
+
asana: Object.freeze([
|
|
955
|
+
'Analys',
|
|
956
|
+
'Påverkansanalys',
|
|
957
|
+
'Poängsättning',
|
|
958
|
+
'## Ursprunglig förfrågan',
|
|
959
|
+
'## Specifikation',
|
|
960
|
+
'## Lösning och omfattning',
|
|
961
|
+
'Beställd',
|
|
962
|
+
'Ej beställd',
|
|
963
|
+
'Uppskjuten',
|
|
964
|
+
'Mottaget',
|
|
965
|
+
'Under bedömning',
|
|
966
|
+
'Inkommet',
|
|
967
|
+
'Poängsättning redo för granskning',
|
|
968
|
+
// Fullständiga fältnamn och statusvärden ur request-sync/SKILL.md.
|
|
969
|
+
// Redan engelska fältnamn (t.ex. "Original request") räknas också som
|
|
970
|
+
// identifierare — Asana slår upp dem literalt.
|
|
971
|
+
'Original request',
|
|
972
|
+
'Original request title',
|
|
973
|
+
'Source',
|
|
974
|
+
'Customer',
|
|
975
|
+
'Requested by',
|
|
976
|
+
'Request type',
|
|
977
|
+
'Funding',
|
|
978
|
+
'Score',
|
|
979
|
+
'Has conditions',
|
|
980
|
+
'Decision',
|
|
981
|
+
'Decision reason',
|
|
982
|
+
'Spec iterations',
|
|
983
|
+
'Score iterations',
|
|
984
|
+
'Offer status',
|
|
985
|
+
'Request status',
|
|
986
|
+
'Kundstatus',
|
|
987
|
+
'Class rationale',
|
|
988
|
+
'Kunden skrev själv',
|
|
989
|
+
'Mejl',
|
|
990
|
+
'Möte',
|
|
991
|
+
'Haus initierade',
|
|
992
|
+
'Bug',
|
|
993
|
+
'Improvement',
|
|
994
|
+
'Wear',
|
|
995
|
+
'📥 Inkommet',
|
|
996
|
+
'🔍 Spec redo för granskning',
|
|
997
|
+
'✅ Spec ok',
|
|
998
|
+
'⚖️ Poängsättning redo för granskning',
|
|
999
|
+
'📤 Klar – till kund',
|
|
1000
|
+
]),
|
|
1001
|
+
rubriker: Object.freeze([...new Set(RUBRIK_LITERALER)]),
|
|
1002
|
+
})
|