@christang/keel 5.20.0 → 5.39.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.
@@ -518,6 +518,67 @@ function collisionHint(repo, change, capability) {
518
518
  );
519
519
  }
520
520
 
521
+ // One phrasing, so an author who has read the over-segmented refusal recognizes
522
+ // the unresolved one. It used to be reachable only by writing too many
523
+ // segments, which withheld it from the reference people actually write wrong.
524
+ const COVERS_HIERARCHY =
525
+ "the hierarchy is capability / requirement, or capability / requirement "
526
+ + "/ scenario";
527
+
528
+ // Say which segment failed. The candidate specs were opened by the caller and
529
+ // the name the author typed is very often a heading one level below where they
530
+ // put it — the shipped task template taught exactly that reference — so the
531
+ // refusal can name the requirement it belongs to instead of handing the
532
+ // reference back. Reporting what a spec contains is not heuristic matching: the
533
+ // reference still fails, and no near miss is resolved on the author's behalf.
534
+ function unresolvedDetail(repo, change, capability, name) {
535
+ const candidates = specCandidatePaths(repo, change, capability);
536
+ const existing = candidates.filter((specPath) => fs.existsSync(specPath));
537
+ if (existing.length === 0) {
538
+ const looked = candidates
539
+ .map((specPath) => path.relative(repo, specPath).replace(/\\/g, "/"))
540
+ .join(" and ");
541
+ return ` No spec declares capability ${capability}; ${looked} do not exist.`;
542
+ }
543
+ const parents = [];
544
+ for (const specPath of existing) {
545
+ const content = fs.readFileSync(specPath, "utf8");
546
+ for (const requirement of headingSections(
547
+ content,
548
+ /^### Requirement:\s*(.+?)\s*$/
549
+ )) {
550
+ const holdsName = headingSections(
551
+ requirement.content,
552
+ /^#### Scenario:\s*(.+?)\s*$/
553
+ ).some((item) => item.title === name);
554
+ if (holdsName && !parents.includes(requirement.title)) {
555
+ parents.push(requirement.title);
556
+ }
557
+ }
558
+ }
559
+ if (parents.length === 1) {
560
+ return (
561
+ ` "${name}" is a Scenario of Requirement "${parents[0]}", not a `
562
+ + `Requirement; ${COVERS_HIERARCHY}. Write it as: `
563
+ + `${capability} / ${parents[0]} / ${name}.`
564
+ );
565
+ }
566
+ if (parents.length > 1) {
567
+ // No single reference corrects this one, so none is offered: sending the
568
+ // author to a reference that fails as ambiguous would cost them the round
569
+ // this diagnostic exists to save.
570
+ const named = parents.map((title) => `"${title}"`).join(", ");
571
+ return (
572
+ ` "${name}" is a Scenario of more than one Requirement — ${named} — so `
573
+ + `no single reference corrects it; ${COVERS_HIERARCHY}.`
574
+ );
575
+ }
576
+ return (
577
+ ` Capability ${capability} declares no Requirement or Scenario named `
578
+ + `"${name}"; ${COVERS_HIERARCHY}.`
579
+ );
580
+ }
581
+
521
582
  function specAuthority(repo, change, reference) {
522
583
  const parts = reference.split("/").map((part) => part.trim());
523
584
  const [capability, requirementName, scenarioName] = parts;
@@ -534,9 +595,8 @@ function specAuthority(repo, change, reference) {
534
595
  diagnostic: {
535
596
  code: "unresolved-covers",
536
597
  message:
537
- `Covers reference has ${parts.length} segments; the hierarchy is `
538
- + "capability / requirement, or capability / requirement / "
539
- + `scenario: ${reference}.`
598
+ `Covers reference has ${parts.length} segments; `
599
+ + `${COVERS_HIERARCHY}: ${reference}.`
540
600
  + collisionHint(repo, change, capability),
541
601
  },
542
602
  };
@@ -603,6 +663,7 @@ function specAuthority(repo, change, reference) {
603
663
  code: "unresolved-covers",
604
664
  message:
605
665
  `Covers reference could not be resolved: ${reference}.`
666
+ + unresolvedDetail(repo, change, capability, requirementName)
606
667
  + collisionHint(repo, change, capability),
607
668
  },
608
669
  };
@@ -632,6 +693,26 @@ function criticalAuthority(repo, change, reference) {
632
693
  ),
633
694
  ];
634
695
  if (matches.length !== 1) {
696
+ // Zero matches is ambiguous: the identifier may never appear in design.md,
697
+ // or it may appear in some other shape (bulleted, bold) that the strict
698
+ // regex above does not accept. A whole-word scan tells those apart so the
699
+ // message sends the author to the actual defect — a shape fix, not a
700
+ // statement that already exists — instead of collapsing both into "Missing".
701
+ if (
702
+ matches.length === 0
703
+ && new RegExp(`\\b${reference}\\b`).test(content)
704
+ ) {
705
+ return {
706
+ diagnostic: {
707
+ code: "unresolved-covers",
708
+ message:
709
+ `Unparsed Covers critical statement: ${reference}. It appears in `
710
+ + "design.md but not in the required shape — write it starting "
711
+ + `the line as \`${reference} — one-line statement\` (no leading `
712
+ + "`-`, `**`, or other decoration) so it can be resolved.",
713
+ },
714
+ };
715
+ }
635
716
  return {
636
717
  diagnostic: {
637
718
  code: matches.length > 1 ? "ambiguous-covers" : "unresolved-covers",
@@ -1049,4 +1130,5 @@ module.exports = {
1049
1130
  loadTaskContract,
1050
1131
  parseTasks,
1051
1132
  taskStartContractProblems,
1133
+ unfilledToken,
1052
1134
  };