@llm4ts/flow 2.17.0 → 2.18.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/CoverageLedger.d.ts +180 -0
- package/dist/CoverageLedger.d.ts.map +1 -0
- package/dist/CoverageLedger.js +415 -0
- package/dist/CoverageLedger.js.map +1 -0
- package/dist/EpicBrief.d.ts +54 -0
- package/dist/EpicBrief.d.ts.map +1 -1
- package/dist/EpicBrief.js +92 -5
- package/dist/EpicBrief.js.map +1 -1
- package/package.json +3 -2
- package/src/CoverageLedger.ts +549 -0
- package/src/EpicBrief.ts +123 -9
package/src/EpicBrief.ts
CHANGED
|
@@ -473,6 +473,24 @@ export const BriefProblem = Schema.Union([
|
|
|
473
473
|
scenario: Schema.String,
|
|
474
474
|
disposition: Schema.String
|
|
475
475
|
}),
|
|
476
|
+
Schema.Struct({
|
|
477
|
+
kind: Schema.Literal("AlreadyOwned"),
|
|
478
|
+
program: Schema.String,
|
|
479
|
+
scenario: Schema.String,
|
|
480
|
+
/** The approved epic that already has the scenario in scope. */
|
|
481
|
+
epic: Schema.String
|
|
482
|
+
}),
|
|
483
|
+
Schema.Struct({
|
|
484
|
+
kind: Schema.Literal("ContradictsBrief"),
|
|
485
|
+
program: Schema.String,
|
|
486
|
+
scenario: Schema.String,
|
|
487
|
+
/** The approved epic that decided otherwise. */
|
|
488
|
+
epic: Schema.String,
|
|
489
|
+
/** This brief's disposition, in words. */
|
|
490
|
+
here: Schema.String,
|
|
491
|
+
/** The other epic's disposition, in words. */
|
|
492
|
+
disposition: Schema.String
|
|
493
|
+
}),
|
|
476
494
|
Schema.Struct({
|
|
477
495
|
kind: Schema.Literal("ApprovedWithOpenPoints"),
|
|
478
496
|
numbers: Schema.Array(Schema.Int)
|
|
@@ -498,11 +516,77 @@ export const renderBriefProblem = (problem: BriefProblem): string => {
|
|
|
498
516
|
return `${problem.program}${cite}${problem.scenario} has no disposition: put it in scope, or drop, mark provided or defer it`
|
|
499
517
|
case "RefineConflict":
|
|
500
518
|
return `${problem.program}${cite}${problem.scenario} is in scope, but modernize-refine marked it ${problem.disposition}; answer \`keep: <why>\` to keep it in scope, or move it out`
|
|
519
|
+
case "AlreadyOwned":
|
|
520
|
+
return `${problem.program}${cite}${problem.scenario} is in scope here and in epic ${problem.epic}; answer \`keep: <why>\` to share it, or move it out`
|
|
521
|
+
case "ContradictsBrief":
|
|
522
|
+
return `${problem.program}${cite}${problem.scenario} is ${problem.here} here, but epic ${problem.epic} has it ${problem.disposition}; answer \`keep: <why>\` to keep this brief's decision, or follow the other`
|
|
501
523
|
case "ApprovedWithOpenPoints":
|
|
502
524
|
return `the brief is approved with unanswered open points: ${problem.numbers.join(", ")}`
|
|
503
525
|
}
|
|
504
526
|
}
|
|
505
527
|
|
|
528
|
+
/** How a brief disposes of a scenario. */
|
|
529
|
+
export type BriefDisposition = "in-scope" | "dropped" | "provided" | "deferred"
|
|
530
|
+
|
|
531
|
+
export const dispositionWords: Readonly<Record<BriefDisposition, string>> = {
|
|
532
|
+
"in-scope": "in scope",
|
|
533
|
+
dropped: "dropped",
|
|
534
|
+
provided: "provided",
|
|
535
|
+
deferred: "deferred"
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** What an approved brief of another epic decided about a scenario (the coverage ledger). */
|
|
539
|
+
export interface InheritedDecision {
|
|
540
|
+
readonly program: string
|
|
541
|
+
readonly scenario: string
|
|
542
|
+
readonly epic: string
|
|
543
|
+
readonly disposition: BriefDisposition
|
|
544
|
+
readonly note: string
|
|
545
|
+
/** The epics this decision was kept against on purpose (`keep: <why>` in that brief). */
|
|
546
|
+
readonly keptAgainst?: ReadonlyArray<string>
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The one rule for two briefs on one scenario, read from the side of the
|
|
551
|
+
* brief that has `here`. A deferred scenario is free on either side; two
|
|
552
|
+
* briefs that agree are fine; both in scope is ownership shared or disputed;
|
|
553
|
+
* anything else is a contradiction. The ledger and the per-brief check both
|
|
554
|
+
* use it, so they cannot disagree about what a conflict is.
|
|
555
|
+
*/
|
|
556
|
+
export const crossProblem = (
|
|
557
|
+
program: string,
|
|
558
|
+
scenario: string,
|
|
559
|
+
here: BriefDisposition,
|
|
560
|
+
other: { readonly epic: string; readonly disposition: BriefDisposition }
|
|
561
|
+
): BriefProblem | undefined => {
|
|
562
|
+
if (here === "deferred" || other.disposition === "deferred") return undefined
|
|
563
|
+
if (here === other.disposition) {
|
|
564
|
+
return here === "in-scope"
|
|
565
|
+
? { kind: "AlreadyOwned", program, scenario, epic: other.epic }
|
|
566
|
+
: undefined
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
kind: "ContradictsBrief",
|
|
570
|
+
program,
|
|
571
|
+
scenario,
|
|
572
|
+
epic: other.epic,
|
|
573
|
+
here: dispositionWords[here],
|
|
574
|
+
disposition: dispositionWords[other.disposition]
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Whether the brief overrides a check on purpose: the check's own `[check]`
|
|
580
|
+
* question, answered `keep: <why>`. Any other answer, or an answer to
|
|
581
|
+
* another question, overrides nothing.
|
|
582
|
+
*/
|
|
583
|
+
export const hasOverride = (brief: EpicBrief, problem: BriefProblem): boolean =>
|
|
584
|
+
brief.openPoints.some(
|
|
585
|
+
(point) =>
|
|
586
|
+
point.question === `${checkMark} ${renderBriefProblem(problem)}` &&
|
|
587
|
+
(point.answer ?? "").trim().toLowerCase().startsWith("keep")
|
|
588
|
+
)
|
|
589
|
+
|
|
506
590
|
/** The pointers of the provided entries: what the caller verifies before checking. */
|
|
507
591
|
export const providedPointers = (brief: EpicBrief): ReadonlyArray<string> => [
|
|
508
592
|
...new Set(brief.provided.map((entry) => entry.note).filter((note) => note.length > 0))
|
|
@@ -515,6 +599,14 @@ export interface BriefCheckInputs {
|
|
|
515
599
|
readonly pack: PackIndex
|
|
516
600
|
/** The provided pointers that were found in the target (or its pack). */
|
|
517
601
|
readonly pointers: ReadonlySet<string>
|
|
602
|
+
/**
|
|
603
|
+
* What approved briefs of other epics decided (the coverage ledger). Their
|
|
604
|
+
* decisions are inherited: they count towards completeness, and deciding
|
|
605
|
+
* otherwise here is a problem. Absent: the brief is checked on its own.
|
|
606
|
+
*/
|
|
607
|
+
readonly others?: ReadonlyArray<InheritedDecision>
|
|
608
|
+
/** This brief's epic, as the other briefs name it. Default: the brief's own id. */
|
|
609
|
+
readonly epicId?: string
|
|
518
610
|
}
|
|
519
611
|
|
|
520
612
|
/** Every problem of a brief against the pack, all at once; empty means acceptable. */
|
|
@@ -593,6 +685,11 @@ export const checkEpicBrief = (
|
|
|
593
685
|
problems.push({ kind: "DuplicateDisposition", program, scenario, lists: where })
|
|
594
686
|
}
|
|
595
687
|
}
|
|
688
|
+
const inherited = new Map<string, Array<InheritedDecision>>()
|
|
689
|
+
for (const decision of inputs.others ?? []) {
|
|
690
|
+
const key = slot(decision.program, decision.scenario)
|
|
691
|
+
inherited.set(key, [...(inherited.get(key) ?? []), decision])
|
|
692
|
+
}
|
|
596
693
|
// Complete means every program the brief touches: the ones it lists as
|
|
597
694
|
// considered, and any it cites or disposes of without listing.
|
|
598
695
|
const touched = [
|
|
@@ -610,20 +707,13 @@ export const checkEpicBrief = (
|
|
|
610
707
|
continue
|
|
611
708
|
}
|
|
612
709
|
for (const scenario of known.scenarios) {
|
|
613
|
-
if (!lists.has(slot(known.name, scenario))) {
|
|
710
|
+
if (!lists.has(slot(known.name, scenario)) && !inherited.has(slot(known.name, scenario))) {
|
|
614
711
|
problems.push({ kind: "Unaccounted", program: known.name, scenario })
|
|
615
712
|
}
|
|
616
713
|
}
|
|
617
714
|
}
|
|
618
715
|
|
|
619
716
|
// A scenario refine took out of the pack's scope can come back only on purpose.
|
|
620
|
-
// The override is explicit: the check's own question, answered `keep: <why>`.
|
|
621
|
-
const justified = (conflict: BriefProblem): boolean =>
|
|
622
|
-
brief.openPoints.some(
|
|
623
|
-
(point) =>
|
|
624
|
-
point.question === `${checkMark} ${renderBriefProblem(conflict)}` &&
|
|
625
|
-
(point.answer ?? "").trim().toLowerCase().startsWith("keep")
|
|
626
|
-
)
|
|
627
717
|
for (const item of brief.scope) {
|
|
628
718
|
for (const citation of item.citations) {
|
|
629
719
|
const refined = inputs.pack.refine.find(
|
|
@@ -639,11 +729,35 @@ export const checkEpicBrief = (
|
|
|
639
729
|
scenario: citation.scenario,
|
|
640
730
|
disposition: refined.disposition
|
|
641
731
|
}
|
|
642
|
-
if (!
|
|
732
|
+
if (!hasOverride(brief, conflict)) problems.push(conflict)
|
|
643
733
|
}
|
|
644
734
|
}
|
|
645
735
|
}
|
|
646
736
|
|
|
737
|
+
// Approved briefs of other epics stand; `crossProblem` says when two briefs
|
|
738
|
+
// clash. A clash one side kept on purpose is settled: shared ownership when
|
|
739
|
+
// either keeps it, a contradiction when exactly one does.
|
|
740
|
+
const self = inputs.epicId ?? brief.epicId
|
|
741
|
+
const byWords = new Map<string, BriefDisposition>([
|
|
742
|
+
["in scope", "in-scope"],
|
|
743
|
+
["dropped", "dropped"],
|
|
744
|
+
["provided", "provided"],
|
|
745
|
+
["deferred", "deferred"]
|
|
746
|
+
])
|
|
747
|
+
for (const [key, where] of lists) {
|
|
748
|
+
const [program = "", scenario = ""] = key.split("\u0000")
|
|
749
|
+
const here = byWords.get(where[0] ?? "")
|
|
750
|
+
if (here === undefined) continue
|
|
751
|
+
for (const other of inherited.get(key) ?? []) {
|
|
752
|
+
const problem = crossProblem(program, scenario, here, other)
|
|
753
|
+
if (problem === undefined) continue
|
|
754
|
+
const theyKept = (other.keptAgainst ?? []).includes(self)
|
|
755
|
+
const iKept = hasOverride(brief, problem)
|
|
756
|
+
const settled = problem.kind === "AlreadyOwned" ? theyKept || iKept : theyKept !== iKept
|
|
757
|
+
if (!settled) problems.push(problem)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
647
761
|
const pending = unanswered(brief)
|
|
648
762
|
if (brief.status === "approved" && pending.length > 0) {
|
|
649
763
|
problems.push({ kind: "ApprovedWithOpenPoints", numbers: pending.map((point) => point.number) })
|