@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.
- package/README.md +28 -9
- package/assets/bootstrap/AGENTS.md +1 -1
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +4 -1
- package/bin/keel.js +127 -22
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +4 -18
- package/plugins/keel/skills/keel-review-checklist/SKILL.md +1 -1
- package/scripts/install_to_repo.py +56 -3
- package/scripts/validate_plugin.py +3782 -228
- package/src/core/config.js +192 -26
- package/src/core/context.js +9 -0
- package/src/core/gates.js +305 -54
- package/src/core/guard.js +77 -16
- package/src/core/task-contract.js +103 -7
|
@@ -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
|
-
|
|
421
|
-
|
|
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;
|
|
524
|
-
+
|
|
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
|
};
|