@christang/keel 5.16.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.
@@ -416,10 +416,24 @@ function requiredFieldProblems(task) {
416
416
  function missingFieldProblems(task, names) {
417
417
  return names
418
418
  .filter((name) => !isConcrete(field(task, name)))
419
- .map((name) => ({
420
- code: "missing-field",
421
- message: `${name} must be concrete.`,
422
- }));
419
+ .map((name) => {
420
+ // Name the matched slot, the way the check and Verify diagnostics already
421
+ // do. The unqualified wording stated only the verdict, so an author whose
422
+ // field held a token in ordinary prose had nothing to search for — and
423
+ // the first problem they saw named a field of the other schema instead.
424
+ // The code is unchanged: the verdict is the same either way, and a new
425
+ // one would hide this case from every consumer keying on `missing-field`.
426
+ const token = unfilledToken(field(task, name));
427
+ return {
428
+ code: "missing-field",
429
+ message: token
430
+ ? `${name} carries the unfilled slot \`${token}\`, so it is not `
431
+ + "concrete. Replace that slot with the value it stands for, or "
432
+ + "fence it in inline code when it is literal text — a numeric "
433
+ + "range or test output — rather than a slot left to fill."
434
+ : `${name} must be concrete.`,
435
+ };
436
+ });
423
437
  }
424
438
 
425
439
  function canonical(value) {
@@ -504,6 +518,67 @@ function collisionHint(repo, change, capability) {
504
518
  );
505
519
  }
506
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
+
507
582
  function specAuthority(repo, change, reference) {
508
583
  const parts = reference.split("/").map((part) => part.trim());
509
584
  const [capability, requirementName, scenarioName] = parts;
@@ -520,9 +595,8 @@ function specAuthority(repo, change, reference) {
520
595
  diagnostic: {
521
596
  code: "unresolved-covers",
522
597
  message:
523
- `Covers reference has ${parts.length} segments; the hierarchy is `
524
- + "capability / requirement, or capability / requirement / "
525
- + `scenario: ${reference}.`
598
+ `Covers reference has ${parts.length} segments; `
599
+ + `${COVERS_HIERARCHY}: ${reference}.`
526
600
  + collisionHint(repo, change, capability),
527
601
  },
528
602
  };
@@ -589,6 +663,7 @@ function specAuthority(repo, change, reference) {
589
663
  code: "unresolved-covers",
590
664
  message:
591
665
  `Covers reference could not be resolved: ${reference}.`
666
+ + unresolvedDetail(repo, change, capability, requirementName)
592
667
  + collisionHint(repo, change, capability),
593
668
  },
594
669
  };
@@ -618,6 +693,26 @@ function criticalAuthority(repo, change, reference) {
618
693
  ),
619
694
  ];
620
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
+ }
621
716
  return {
622
717
  diagnostic: {
623
718
  code: matches.length > 1 ? "ambiguous-covers" : "unresolved-covers",
@@ -1035,4 +1130,5 @@ module.exports = {
1035
1130
  loadTaskContract,
1036
1131
  parseTasks,
1037
1132
  taskStartContractProblems,
1133
+ unfilledToken,
1038
1134
  };