@llm4ts/flow 2.1.0 → 2.2.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/dist/Approval.d.ts +1 -1
- package/dist/Approval.d.ts.map +1 -1
- package/dist/Artifacts.d.ts +2 -2
- package/dist/Artifacts.d.ts.map +1 -1
- package/dist/BenchReport.d.ts +2 -2
- package/dist/BenchReport.d.ts.map +1 -1
- package/dist/CostLedger.d.ts +2 -2
- package/dist/CostLedger.d.ts.map +1 -1
- package/dist/Decisions.d.ts +187 -0
- package/dist/Decisions.d.ts.map +1 -0
- package/dist/Decisions.js +677 -0
- package/dist/Decisions.js.map +1 -0
- package/dist/Domains.d.ts +125 -0
- package/dist/Domains.d.ts.map +1 -0
- package/dist/Domains.js +528 -0
- package/dist/Domains.js.map +1 -0
- package/dist/Equiv.d.ts +3 -3
- package/dist/Equiv.d.ts.map +1 -1
- package/dist/Flow.d.ts +1 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/FlowError.d.ts +25 -1
- package/dist/FlowError.d.ts.map +1 -1
- package/dist/FlowError.js +33 -1
- package/dist/FlowError.js.map +1 -1
- package/dist/Pack.d.ts +16 -0
- package/dist/Pack.d.ts.map +1 -1
- package/dist/Pack.js +53 -1
- package/dist/Pack.js.map +1 -1
- package/dist/PageSpec.d.ts +21 -7
- package/dist/PageSpec.d.ts.map +1 -1
- package/dist/PageSpec.js +83 -14
- package/dist/PageSpec.js.map +1 -1
- package/dist/Persistence.d.ts +2 -2
- package/dist/Persistence.d.ts.map +1 -1
- package/dist/PlanExecution.d.ts +1 -1
- package/dist/PlanExecution.d.ts.map +1 -1
- package/dist/ProgramJudge.d.ts +1 -1
- package/dist/ProgramJudge.d.ts.map +1 -1
- package/dist/Replay.d.ts +2 -2
- package/dist/Replay.d.ts.map +1 -1
- package/dist/Review.d.ts +2 -2
- package/dist/Review.d.ts.map +1 -1
- package/dist/ReviewCache.d.ts +1 -1
- package/dist/ReviewCache.d.ts.map +1 -1
- package/dist/SpecChecks.d.ts +8 -0
- package/dist/SpecChecks.d.ts.map +1 -1
- package/dist/SpecChecks.js +7 -1
- package/dist/SpecChecks.js.map +1 -1
- package/dist/Stories.d.ts +1 -1
- package/dist/Stories.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/Decisions.ts +844 -0
- package/src/Domains.ts +650 -0
- package/src/FlowError.ts +41 -1
- package/src/Pack.ts +79 -1
- package/src/PageSpec.ts +132 -8
- package/src/SpecChecks.ts +15 -1
package/src/Pack.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect"
|
|
2
2
|
import * as Schema from "effect/Schema"
|
|
3
3
|
import { Dimension } from "@llm4ts/core/eval/Eval"
|
|
4
|
+
import type { ConsolidateRules } from "./Domains.ts"
|
|
4
5
|
import { PlanParseError } from "./FlowError.ts"
|
|
5
6
|
import { parseReviewer, type Reviewer } from "./Reviewer.ts"
|
|
6
7
|
import { CoverageRule } from "./SpecChecks.ts"
|
|
@@ -41,12 +42,25 @@ export interface Pack {
|
|
|
41
42
|
// paths), `<NAME>` substituted with the program name. The seam that makes
|
|
42
43
|
// per-program judging possible.
|
|
43
44
|
readonly programFiles: string | undefined
|
|
45
|
+
/**
|
|
46
|
+
* Regex template locating a DOMAIN FEATURE's target files (ADR 0012
|
|
47
|
+
* addendum): `<NAME>` is the feature id, `<PAGES>` an alternation of its
|
|
48
|
+
* page names. A feature's scope is this template plus every page's own
|
|
49
|
+
* `program-files` scope.
|
|
50
|
+
*/
|
|
51
|
+
readonly featureFiles: string | undefined
|
|
44
52
|
/**
|
|
45
53
|
* The schema every program spec must embed, validated deterministically
|
|
46
54
|
* by the extraction gate: `pagespec` (a ```json pagespec block decodable
|
|
47
55
|
* as `PageSpec`) is the only one today; absent means prose-only specs.
|
|
48
56
|
*/
|
|
49
57
|
readonly specSchema: string | undefined
|
|
58
|
+
/**
|
|
59
|
+
* The `## Consolidate` section (ADR 0015): which survey edge kinds put two
|
|
60
|
+
* units in one domain feature (`cluster:`) and which only attach a shared
|
|
61
|
+
* fragment as context (`context:`). Absent: every program is its own feature.
|
|
62
|
+
*/
|
|
63
|
+
readonly consolidate: ConsolidateRules | undefined
|
|
50
64
|
readonly dir: string
|
|
51
65
|
readonly gate: (name: string) => ReadonlyArray<string> | undefined
|
|
52
66
|
readonly prompt: (name: string) => string | undefined
|
|
@@ -54,6 +68,8 @@ export interface Pack {
|
|
|
54
68
|
// template with `<NAME>` substituted, or a case-insensitive "path contains
|
|
55
69
|
// the program name" fallback.
|
|
56
70
|
readonly filesFor: (program: string) => RegExp
|
|
71
|
+
/** The regex for a feature's files: its template (when set) or any of its pages' scopes. */
|
|
72
|
+
readonly filesForFeature: (feature: string, pages: ReadonlyArray<string>) => RegExp
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
interface ParsedManifest {
|
|
@@ -152,6 +168,12 @@ const dimensions = (body: string | undefined): ReadonlyArray<Dimension> =>
|
|
|
152
168
|
: [Dimension.make({ name, rubric, maxScore })]
|
|
153
169
|
})
|
|
154
170
|
|
|
171
|
+
const commaList = (value: string | undefined): ReadonlyArray<string> =>
|
|
172
|
+
(value ?? "")
|
|
173
|
+
.split(",")
|
|
174
|
+
.map((item) => item.trim())
|
|
175
|
+
.filter((item) => item.length > 0)
|
|
176
|
+
|
|
155
177
|
const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
156
178
|
|
|
157
179
|
const isValidRegExp = (source: string): boolean => {
|
|
@@ -219,12 +241,48 @@ export const loadPack = Effect.fn("@llm4ts/flow/Pack.load")(function* (
|
|
|
219
241
|
message: `pack manifest 'programFiles:' is not a valid regex template: ${programFiles}`
|
|
220
242
|
})
|
|
221
243
|
}
|
|
244
|
+
const featureFiles = fields["feature-files"]
|
|
245
|
+
if (
|
|
246
|
+
featureFiles !== undefined &&
|
|
247
|
+
!isValidRegExp(featureFiles.replaceAll("<NAME>", "PROBE").replaceAll("<PAGES>", "PROBE"))
|
|
248
|
+
) {
|
|
249
|
+
return yield* PlanParseError.make({
|
|
250
|
+
message: `pack manifest 'feature-files:' is not a valid regex template: ${featureFiles}`
|
|
251
|
+
})
|
|
252
|
+
}
|
|
222
253
|
const specSchema = fields["spec-schema"]
|
|
223
254
|
if (specSchema !== undefined && specSchema !== "pagespec") {
|
|
224
255
|
return yield* PlanParseError.make({
|
|
225
256
|
message: `pack manifest 'spec-schema:' must be 'pagespec' when set, got: ${specSchema}`
|
|
226
257
|
})
|
|
227
258
|
}
|
|
259
|
+
const consolidateValues = namedItems(section(manifest.sections, "Consolidate"))
|
|
260
|
+
const consolidate: ConsolidateRules | undefined =
|
|
261
|
+
section(manifest.sections, "Consolidate") === undefined
|
|
262
|
+
? undefined
|
|
263
|
+
: {
|
|
264
|
+
cluster: commaList(consolidateValues.cluster),
|
|
265
|
+
context: commaList(consolidateValues.context)
|
|
266
|
+
}
|
|
267
|
+
if (consolidate !== undefined) {
|
|
268
|
+
const surveyNames = new Set(rules(manifest.sections, "## Survey: ").map((rule) => rule.name))
|
|
269
|
+
const unknown = [...consolidate.cluster, ...consolidate.context].filter(
|
|
270
|
+
(kind) => !surveyNames.has(kind) && !kind.startsWith("llm-") && !kind.endsWith("*")
|
|
271
|
+
)
|
|
272
|
+
if (unknown.length > 0) {
|
|
273
|
+
return yield* PlanParseError.make({
|
|
274
|
+
message:
|
|
275
|
+
`pack manifest '## Consolidate' names edge kinds no '## Survey:' rule produces: ${unknown.join(", ")} ` +
|
|
276
|
+
`(known: ${[...surveyNames].join(", ") || "none"}; 'llm-*' matches refined edges)`
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
const both = consolidate.cluster.filter((kind) => consolidate.context.includes(kind))
|
|
280
|
+
if (both.length > 0) {
|
|
281
|
+
return yield* PlanParseError.make({
|
|
282
|
+
message: `pack manifest '## Consolidate' lists ${both.join(", ")} as both cluster and context`
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
228
286
|
const exclude = fields.exclude
|
|
229
287
|
if (exclude !== undefined && !isValidRegExp(exclude)) {
|
|
230
288
|
return yield* PlanParseError.make({
|
|
@@ -258,7 +316,9 @@ export const loadPack = Effect.fn("@llm4ts/flow/Pack.load")(function* (
|
|
|
258
316
|
lenses,
|
|
259
317
|
lessons: lessons === undefined || lessons.length === 0 ? undefined : lessons,
|
|
260
318
|
programFiles,
|
|
319
|
+
featureFiles,
|
|
261
320
|
specSchema,
|
|
321
|
+
consolidate,
|
|
262
322
|
dir: directory,
|
|
263
323
|
gate: (name) => gates[name],
|
|
264
324
|
prompt: (name) => prompts[name],
|
|
@@ -267,7 +327,25 @@ export const loadPack = Effect.fn("@llm4ts/flow/Pack.load")(function* (
|
|
|
267
327
|
filesFor: (program) =>
|
|
268
328
|
programFiles === undefined
|
|
269
329
|
? new RegExp(escapeRegExp(program), "i")
|
|
270
|
-
: new RegExp(`^(?:${programFiles.replaceAll("<NAME>", program)})$`)
|
|
330
|
+
: new RegExp(`^(?:${programFiles.replaceAll("<NAME>", program)})$`),
|
|
331
|
+
filesForFeature: (feature, pages) => {
|
|
332
|
+
const pageScopes =
|
|
333
|
+
programFiles === undefined
|
|
334
|
+
? pages.map((page) => escapeRegExp(page))
|
|
335
|
+
: pages.map((page) => programFiles.replaceAll("<NAME>", page))
|
|
336
|
+
const own =
|
|
337
|
+
featureFiles === undefined
|
|
338
|
+
? []
|
|
339
|
+
: [
|
|
340
|
+
featureFiles
|
|
341
|
+
.replaceAll("<NAME>", feature)
|
|
342
|
+
.replaceAll("<PAGES>", pages.map(escapeRegExp).join("|") || "PROBE")
|
|
343
|
+
]
|
|
344
|
+
const alternatives = [...own, ...pageScopes]
|
|
345
|
+
return programFiles === undefined && featureFiles === undefined
|
|
346
|
+
? new RegExp(alternatives.join("|"), "i")
|
|
347
|
+
: new RegExp(`^(?:${alternatives.join("|")})$`)
|
|
348
|
+
}
|
|
271
349
|
}
|
|
272
350
|
return pack
|
|
273
351
|
})
|
package/src/PageSpec.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect"
|
|
2
2
|
import * as Schema from "effect/Schema"
|
|
3
|
-
import { PlanParseError } from "./FlowError.ts"
|
|
3
|
+
import { ContractConflict, PlanParseError } from "./FlowError.ts"
|
|
4
|
+
|
|
5
|
+
export { ContractConflict } from "./FlowError.ts"
|
|
4
6
|
|
|
5
7
|
// The Page Spec is the per-page contract of the J2EE→SPA conversion scenario
|
|
6
8
|
// (ADR 0012): extraction embeds it in the spec markdown as a ```json pagespec
|
|
@@ -251,9 +253,20 @@ const propertyLines = (
|
|
|
251
253
|
* future B4F implement. Emitted by code, not by a model: the contract must
|
|
252
254
|
* be a projection of the reviewed page spec, never an invention.
|
|
253
255
|
*/
|
|
254
|
-
|
|
256
|
+
interface ContractInfo {
|
|
257
|
+
readonly title: string
|
|
258
|
+
readonly description: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The YAML writer shared by the per-page and the per-feature contracts. */
|
|
262
|
+
const renderOpenApi = (
|
|
263
|
+
info: ContractInfo,
|
|
264
|
+
apiCalls: ReadonlyArray<PageApiCall>,
|
|
265
|
+
dtos: ReadonlyArray<PageDto>,
|
|
266
|
+
origins?: ReadonlyMap<string, ReadonlyArray<string>>
|
|
267
|
+
): string => {
|
|
255
268
|
const byPath = new Map<string, Array<PageApiCall>>()
|
|
256
|
-
for (const call of
|
|
269
|
+
for (const call of apiCalls) {
|
|
257
270
|
const bucket = byPath.get(call.path) ?? []
|
|
258
271
|
bucket.push(call)
|
|
259
272
|
byPath.set(call.path, bucket)
|
|
@@ -262,8 +275,8 @@ export const openApiFor = (spec: PageSpec): string => {
|
|
|
262
275
|
const lines: Array<string> = [
|
|
263
276
|
"openapi: 3.0.3",
|
|
264
277
|
"info:",
|
|
265
|
-
` title: ${yamlText(
|
|
266
|
-
` description: ${yamlText(
|
|
278
|
+
` title: ${yamlText(info.title)}`,
|
|
279
|
+
` description: ${yamlText(info.description)}`,
|
|
267
280
|
" version: 0.1.0",
|
|
268
281
|
"paths:"
|
|
269
282
|
]
|
|
@@ -286,8 +299,12 @@ export const openApiFor = (spec: PageSpec): string => {
|
|
|
286
299
|
const call = variants[0]!
|
|
287
300
|
lines.push(` ${method}:`)
|
|
288
301
|
lines.push(` operationId: ${call.operation}`)
|
|
302
|
+
const origin = origins?.get(`${call.method.toUpperCase()} ${call.path}`)
|
|
289
303
|
const notes = [
|
|
290
304
|
...(call.esbService === undefined ? [] : [`backed by ESB service ${call.esbService}`]),
|
|
305
|
+
...(origin === undefined
|
|
306
|
+
? []
|
|
307
|
+
: [`declared by page${origin.length > 1 ? "s" : ""} ${origin.join(", ")}`]),
|
|
291
308
|
...(variants.length > 1
|
|
292
309
|
? [
|
|
293
310
|
`also serves: ${variants
|
|
@@ -342,14 +359,14 @@ export const openApiFor = (spec: PageSpec): string => {
|
|
|
342
359
|
}
|
|
343
360
|
lines.push("components:")
|
|
344
361
|
lines.push(" schemas:")
|
|
345
|
-
const schemaCalls = [...
|
|
362
|
+
const schemaCalls = [...apiCalls].sort((a, b) => a.operation.localeCompare(b.operation))
|
|
346
363
|
let wroteSchema = false
|
|
347
364
|
// Every DTO a response names becomes a component the calls reference —
|
|
348
365
|
// one schema per domain entity, shared by every endpoint returning it.
|
|
349
366
|
const referencedDtos = new Set(
|
|
350
|
-
|
|
367
|
+
apiCalls.flatMap((call) => (call.responseDto === undefined ? [] : [call.responseDto]))
|
|
351
368
|
)
|
|
352
|
-
for (const dto of [...
|
|
369
|
+
for (const dto of [...dtos].sort((a, b) => a.domainName.localeCompare(b.domainName))) {
|
|
353
370
|
if (!referencedDtos.has(dto.domainName)) {
|
|
354
371
|
continue
|
|
355
372
|
}
|
|
@@ -388,6 +405,113 @@ export const openApiFor = (spec: PageSpec): string => {
|
|
|
388
405
|
return lines.join("\n") + "\n"
|
|
389
406
|
}
|
|
390
407
|
|
|
408
|
+
export const openApiFor = (spec: PageSpec): string =>
|
|
409
|
+
renderOpenApi(
|
|
410
|
+
{
|
|
411
|
+
title: `${spec.title} service contract`,
|
|
412
|
+
description: `Anti-corruption contract for page ${spec.page} (${spec.route})`
|
|
413
|
+
},
|
|
414
|
+
spec.apiCalls,
|
|
415
|
+
spec.dtos
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
const sameFields = (
|
|
419
|
+
left: ReadonlyArray<FieldMapping>,
|
|
420
|
+
right: ReadonlyArray<FieldMapping>
|
|
421
|
+
): boolean =>
|
|
422
|
+
left.length === right.length &&
|
|
423
|
+
left.every(
|
|
424
|
+
(field, index) =>
|
|
425
|
+
field.legacyName === right[index]?.legacyName &&
|
|
426
|
+
field.domainName === right[index]?.domainName &&
|
|
427
|
+
field.type === right[index]?.type
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
const sameCall = (left: PageApiCall, right: PageApiCall): boolean =>
|
|
431
|
+
left.operation === right.operation &&
|
|
432
|
+
left.responseDto === right.responseDto &&
|
|
433
|
+
left.responseShape === right.responseShape &&
|
|
434
|
+
sameFields(left.request, right.request) &&
|
|
435
|
+
sameFields(left.response, right.response)
|
|
436
|
+
|
|
437
|
+
export interface FeatureContract {
|
|
438
|
+
readonly yaml: string
|
|
439
|
+
/** `<method> <path>` of every operation and the pages that declare it. */
|
|
440
|
+
readonly operations: ReadonlyArray<{
|
|
441
|
+
readonly key: string
|
|
442
|
+
readonly pages: ReadonlyArray<string>
|
|
443
|
+
}>
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* ONE contract for a domain feature (ADR 0012 addendum): the union of its
|
|
448
|
+
* pages' API sections by method + path, DTOs by domain name. Two pages that
|
|
449
|
+
* declare the same method + path (or the same operation name) with different
|
|
450
|
+
* shapes, or the same DTO with different fields, are a `ContractConflict`
|
|
451
|
+
* listing every disagreement — never a silent merge. The page each
|
|
452
|
+
* operation came from is recorded in its description.
|
|
453
|
+
*/
|
|
454
|
+
export const openApiForFeature = (
|
|
455
|
+
feature: { readonly id: string; readonly name: string },
|
|
456
|
+
specs: ReadonlyArray<PageSpec>
|
|
457
|
+
): Effect.Effect<FeatureContract, ContractConflict> => {
|
|
458
|
+
const conflicts: Array<string> = []
|
|
459
|
+
const calls = new Map<string, { call: PageApiCall; pages: Array<string> }>()
|
|
460
|
+
const byOperation = new Map<string, string>()
|
|
461
|
+
for (const spec of specs) {
|
|
462
|
+
for (const call of spec.apiCalls) {
|
|
463
|
+
const key = `${call.method.toUpperCase()} ${call.path}`
|
|
464
|
+
const existing = calls.get(key)
|
|
465
|
+
if (existing === undefined) {
|
|
466
|
+
const owner = byOperation.get(call.operation)
|
|
467
|
+
if (owner !== undefined && owner !== key) {
|
|
468
|
+
conflicts.push(
|
|
469
|
+
`operation '${call.operation}' is declared on ${owner} and on ${key} (${spec.page})`
|
|
470
|
+
)
|
|
471
|
+
continue
|
|
472
|
+
}
|
|
473
|
+
byOperation.set(call.operation, key)
|
|
474
|
+
calls.set(key, { call, pages: [spec.page] })
|
|
475
|
+
} else if (sameCall(existing.call, call)) {
|
|
476
|
+
existing.pages.push(spec.page)
|
|
477
|
+
} else {
|
|
478
|
+
conflicts.push(
|
|
479
|
+
`${key} differs between ${existing.pages.join(", ")} (${existing.call.operation}) and ${spec.page} (${call.operation})`
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const dtos = new Map<string, { dto: PageDto; page: string }>()
|
|
485
|
+
for (const spec of specs) {
|
|
486
|
+
for (const dto of spec.dtos) {
|
|
487
|
+
const existing = dtos.get(dto.domainName)
|
|
488
|
+
if (existing === undefined) {
|
|
489
|
+
dtos.set(dto.domainName, { dto, page: spec.page })
|
|
490
|
+
} else if (!sameFields(existing.dto.fields, dto.fields)) {
|
|
491
|
+
conflicts.push(
|
|
492
|
+
`DTO '${dto.domainName}' has different fields in ${existing.page} and ${spec.page}`
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (conflicts.length > 0) {
|
|
498
|
+
return Effect.fail(ContractConflict.make({ feature: feature.id, conflicts }))
|
|
499
|
+
}
|
|
500
|
+
const operations = [...calls.entries()].map(([key, { pages }]) => ({ key, pages }))
|
|
501
|
+
const yaml = renderOpenApi(
|
|
502
|
+
{
|
|
503
|
+
title: `${feature.name} service contract`,
|
|
504
|
+
description:
|
|
505
|
+
`Anti-corruption contract for domain feature ${feature.id} — pages ` +
|
|
506
|
+
specs.map((spec) => spec.page).join(", ")
|
|
507
|
+
},
|
|
508
|
+
[...calls.values()].map(({ call }) => call),
|
|
509
|
+
[...dtos.values()].map(({ dto }) => dto),
|
|
510
|
+
new Map(operations.map(({ key, pages }) => [key, pages]))
|
|
511
|
+
)
|
|
512
|
+
return Effect.succeed({ yaml, operations })
|
|
513
|
+
}
|
|
514
|
+
|
|
391
515
|
/** Human-readable summary — the review surface next to the JSON contract. */
|
|
392
516
|
export const renderPageSpec = (spec: PageSpec): string => {
|
|
393
517
|
const lines: Array<string> = [
|
package/src/SpecChecks.ts
CHANGED
|
@@ -100,12 +100,20 @@ export interface CoverageOptions {
|
|
|
100
100
|
* rather than failing this wave's gate. Absent: every unit gates.
|
|
101
101
|
*/
|
|
102
102
|
readonly inScope?: (path: string) => boolean
|
|
103
|
+
/**
|
|
104
|
+
* Units a decisions overlay waives (ADR 0015): the legacy has them, a
|
|
105
|
+
* human decided not to carry them, so they never gate and are reported
|
|
106
|
+
* as waived rather than uncovered.
|
|
107
|
+
*/
|
|
108
|
+
readonly waived?: ReadonlySet<string>
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
export interface CoverageReport {
|
|
106
112
|
readonly result: ReviewResult
|
|
107
113
|
/** Uncovered units the scope excluded from the gate, as `rule: unit`. */
|
|
108
114
|
readonly outOfScope: ReadonlyArray<string>
|
|
115
|
+
/** Uncovered units a decision waived, as `rule: unit`. */
|
|
116
|
+
readonly waived: ReadonlyArray<string>
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
const uncoveredIssue = (rule: string, unit: string): ReviewIssue =>
|
|
@@ -126,10 +134,15 @@ export const coverageReport = Effect.fn("@llm4ts/flow/SpecChecks.coverageReport"
|
|
|
126
134
|
const captured = yield* capturedUnits(workspace, rules)
|
|
127
135
|
const issues: Array<ReviewIssue> = []
|
|
128
136
|
const outOfScope: Array<string> = []
|
|
137
|
+
const waived: Array<string> = []
|
|
129
138
|
for (const entry of captured) {
|
|
130
139
|
if (traceability.includes(entry.unit)) {
|
|
131
140
|
continue
|
|
132
141
|
}
|
|
142
|
+
if (options.waived?.has(entry.unit) === true) {
|
|
143
|
+
waived.push(`${entry.rule}: ${entry.unit}`)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
133
146
|
if (options.inScope === undefined || entry.paths.some(options.inScope)) {
|
|
134
147
|
issues.push(uncoveredIssue(entry.rule, entry.unit))
|
|
135
148
|
} else {
|
|
@@ -141,7 +154,8 @@ export const coverageReport = Effect.fn("@llm4ts/flow/SpecChecks.coverageReport"
|
|
|
141
154
|
issues,
|
|
142
155
|
summary: issues.length === 0 ? "coverage complete" : `${issues.length} unit(s) uncovered`
|
|
143
156
|
}),
|
|
144
|
-
outOfScope
|
|
157
|
+
outOfScope,
|
|
158
|
+
waived
|
|
145
159
|
}
|
|
146
160
|
})
|
|
147
161
|
|