@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/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 (!justified(conflict)) problems.push(conflict)
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) })