@christang/keel 5.3.5 → 5.3.6
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/bin/keel.js
CHANGED
|
@@ -1082,15 +1082,10 @@ function refreshOpenSpecSurfaceOverlay(repo, target, options = {}) {
|
|
|
1082
1082
|
missing: 0,
|
|
1083
1083
|
};
|
|
1084
1084
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
return { status: 0, ...counts };
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1085
|
+
// A dry run and the real run classify each surface the same way, from the
|
|
1086
|
+
// same read-and-compare, so the plan cannot drift from the outcome. The dry
|
|
1087
|
+
// run used to list every surface without reading one, which over-reported as
|
|
1088
|
+
// badly as `--check` under-reported by never reaching this step at all.
|
|
1094
1089
|
for (const surface of surfaces) {
|
|
1095
1090
|
if (!fs.existsSync(surface.path)) {
|
|
1096
1091
|
counts.missing += 1;
|
|
@@ -1102,14 +1097,20 @@ function refreshOpenSpecSurfaceOverlay(repo, target, options = {}) {
|
|
|
1102
1097
|
counts.current += 1;
|
|
1103
1098
|
continue;
|
|
1104
1099
|
}
|
|
1105
|
-
fs.writeFileSync(surface.path, next, "utf8");
|
|
1106
1100
|
counts.refreshed += 1;
|
|
1101
|
+
if (options.dryRun) {
|
|
1102
|
+
process.stdout.write(
|
|
1103
|
+
`keel: would refresh OpenSpec ${surface.action} overlay in ${surface.path}\n`
|
|
1104
|
+
);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
fs.writeFileSync(surface.path, next, "utf8");
|
|
1107
1108
|
}
|
|
1108
1109
|
|
|
1109
1110
|
if (counts.refreshed > 0 || counts.current > 0) {
|
|
1110
1111
|
process.stdout.write(
|
|
1111
|
-
|
|
1112
|
-
+ `refreshed=${counts.refreshed} current=${counts.current} `
|
|
1112
|
+
`keel: ${options.dryRun ? "would refresh " : ""}OpenSpec apply/archive `
|
|
1113
|
+
+ `overlay refreshed=${counts.refreshed} current=${counts.current} `
|
|
1113
1114
|
+ `missing=${counts.missing}\n`
|
|
1114
1115
|
);
|
|
1115
1116
|
}
|
|
@@ -1758,7 +1759,20 @@ function runAction(options) {
|
|
|
1758
1759
|
return checkStatus;
|
|
1759
1760
|
}
|
|
1760
1761
|
process.stdout.write("\nDry-run install plan:\n");
|
|
1761
|
-
|
|
1762
|
+
const planStatus = runPython(
|
|
1763
|
+
INSTALL_SCRIPT,
|
|
1764
|
+
installerArgs({ ...options, dryRun: true })
|
|
1765
|
+
);
|
|
1766
|
+
if (planStatus !== 0) {
|
|
1767
|
+
return planStatus;
|
|
1768
|
+
}
|
|
1769
|
+
// The overlay refresh is a Node-side step the installer's plan never sees,
|
|
1770
|
+
// so without this `--check` reports an empty plan for a run that writes.
|
|
1771
|
+
return refreshOpenSpecSurfaceOverlay(
|
|
1772
|
+
path.resolve(options.repo || process.cwd()),
|
|
1773
|
+
options.target,
|
|
1774
|
+
{ dryRun: true }
|
|
1775
|
+
).status;
|
|
1762
1776
|
}
|
|
1763
1777
|
|
|
1764
1778
|
if (options.action === "doctor") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.6",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.6",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -37,8 +37,8 @@ REQUIRED_SCRIPTS = [
|
|
|
37
37
|
"scripts/validate_plugin.py",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
PACKAGE_VERSION = "5.3.
|
|
41
|
-
PROTOCOL_VERSION = "5.3.
|
|
40
|
+
PACKAGE_VERSION = "5.3.6"
|
|
41
|
+
PROTOCOL_VERSION = "5.3.6"
|
|
42
42
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
43
43
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
44
44
|
# Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
|
|
@@ -3683,6 +3683,88 @@ SCHEMA_COPY_PAIRS = (
|
|
|
3683
3683
|
)
|
|
3684
3684
|
|
|
3685
3685
|
|
|
3686
|
+
def validate_dry_run_overlay_accounting_scenario() -> int:
|
|
3687
|
+
label = "dry-run-overlay-accounting"
|
|
3688
|
+
|
|
3689
|
+
# A dry run is relied on, so it is wrong in both directions: naming a write
|
|
3690
|
+
# that will not happen trains the reader to ignore it, and omitting one
|
|
3691
|
+
# breaks the promise the dry run exists to make. `--check` used to omit the
|
|
3692
|
+
# overlay step entirely while `--install --dry-run` claimed every surface.
|
|
3693
|
+
def overlay_lines(text: str) -> list[str]:
|
|
3694
|
+
return [line for line in text.splitlines() if "overlay" in line]
|
|
3695
|
+
|
|
3696
|
+
def counts(text: str) -> str | None:
|
|
3697
|
+
found = re.search(r"refreshed=(\d+) current=(\d+) missing=(\d+)", text)
|
|
3698
|
+
return found.group(0) if found else None
|
|
3699
|
+
|
|
3700
|
+
with tempfile.TemporaryDirectory(prefix="keel-dry-run-overlay-") as raw_tmp:
|
|
3701
|
+
repo = Path(raw_tmp) / "repo"
|
|
3702
|
+
repo.mkdir()
|
|
3703
|
+
# The overlay surfaces are files OpenSpec generates; install merges into
|
|
3704
|
+
# them and skips the ones that are absent. Create them so this scenario
|
|
3705
|
+
# exercises the classification rather than the missing branch.
|
|
3706
|
+
for relative in (
|
|
3707
|
+
".claude/skills/openspec-propose/SKILL.md",
|
|
3708
|
+
".claude/skills/openspec-apply-change/SKILL.md",
|
|
3709
|
+
".claude/skills/openspec-archive-change/SKILL.md",
|
|
3710
|
+
".claude/commands/opsx/propose.md",
|
|
3711
|
+
".claude/commands/opsx/apply.md",
|
|
3712
|
+
".claude/commands/opsx/archive.md",
|
|
3713
|
+
):
|
|
3714
|
+
write_text(repo / relative, "# OpenSpec surface\n\nGenerated body.\n")
|
|
3715
|
+
if run_keel(repo, "--install", "--target", "claude").returncode != 0:
|
|
3716
|
+
report(f"{label} could not install a fixture repository.")
|
|
3717
|
+
return 1
|
|
3718
|
+
|
|
3719
|
+
# Nothing stale: neither dry run may name a file.
|
|
3720
|
+
check = run_keel(repo, "--check", "--target", "claude")
|
|
3721
|
+
if any("would refresh OpenSpec" in line and ".md" in line
|
|
3722
|
+
for line in overlay_lines(check.stdout)):
|
|
3723
|
+
report(f"{label} named a surface that would not change.")
|
|
3724
|
+
report("\n".join(overlay_lines(check.stdout)))
|
|
3725
|
+
return 1
|
|
3726
|
+
|
|
3727
|
+
# Make exactly one surface stale and require both dry runs to say so.
|
|
3728
|
+
stale = repo / ".claude/skills/openspec-apply-change/SKILL.md"
|
|
3729
|
+
if not stale.is_file():
|
|
3730
|
+
report(f"{label} fixture has no overlay surface to make stale.")
|
|
3731
|
+
return 1
|
|
3732
|
+
original = stale.read_text(encoding="utf-8")
|
|
3733
|
+
stale.write_text(
|
|
3734
|
+
re.sub(r"(keel:openspec-surface-overlay version=)[0-9.]+", r"\g<1>0.0.1",
|
|
3735
|
+
original, count=1),
|
|
3736
|
+
encoding="utf-8",
|
|
3737
|
+
)
|
|
3738
|
+
|
|
3739
|
+
check = run_keel(repo, "--check", "--target", "claude")
|
|
3740
|
+
dry = run_keel(repo, "--install", "--dry-run", "--target", "claude")
|
|
3741
|
+
named = [line for line in overlay_lines(check.stdout) if ".md" in line]
|
|
3742
|
+
if len(named) != 1 or "openspec-apply-change" not in named[0]:
|
|
3743
|
+
report(f"{label} --check did not name exactly the one stale surface.")
|
|
3744
|
+
report("\n".join(overlay_lines(check.stdout)) or "(no overlay output)")
|
|
3745
|
+
return 1
|
|
3746
|
+
if counts(check.stdout) != counts(dry.stdout):
|
|
3747
|
+
report(f"{label} the two dry-run entry points disagree.")
|
|
3748
|
+
report(f"--check: {counts(check.stdout)} --install --dry-run: {counts(dry.stdout)}")
|
|
3749
|
+
return 1
|
|
3750
|
+
if "0.0.1" not in stale.read_text(encoding="utf-8"):
|
|
3751
|
+
report(f"{label} a dry run wrote to the surface it was describing.")
|
|
3752
|
+
return 1
|
|
3753
|
+
|
|
3754
|
+
# And the real run reports what the dry run promised.
|
|
3755
|
+
real = run_keel(repo, "--install", "--target", "claude")
|
|
3756
|
+
if counts(real.stdout) != counts(check.stdout):
|
|
3757
|
+
report(f"{label} the real run's counts differ from the plan's.")
|
|
3758
|
+
report(f"plan: {counts(check.stdout)} real: {counts(real.stdout)}")
|
|
3759
|
+
return 1
|
|
3760
|
+
if "0.0.1" in stale.read_text(encoding="utf-8"):
|
|
3761
|
+
report(f"{label} the real run did not refresh the stale surface.")
|
|
3762
|
+
return 1
|
|
3763
|
+
|
|
3764
|
+
report(f"{label} scenario passed.")
|
|
3765
|
+
return 0
|
|
3766
|
+
|
|
3767
|
+
|
|
3686
3768
|
def validate_anchor_reverification_bound_scenario() -> int:
|
|
3687
3769
|
label = "anchor-reverification-bound"
|
|
3688
3770
|
|
|
@@ -13073,6 +13155,7 @@ SCENARIOS: tuple = (
|
|
|
13073
13155
|
("task-start-invalidation", validate_task_start_invalidation_scenario),
|
|
13074
13156
|
("regression-check-tag", validate_regression_check_tag_scenario),
|
|
13075
13157
|
("durable-owner-vocabulary", validate_durable_owner_vocabulary_scenario),
|
|
13158
|
+
("dry-run-overlay-accounting", validate_dry_run_overlay_accounting_scenario),
|
|
13076
13159
|
("anchor-reverification-bound", validate_anchor_reverification_bound_scenario),
|
|
13077
13160
|
(
|
|
13078
13161
|
"authoring-surface-owner-and-tags",
|