@christang/keel 5.65.0 → 5.66.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.
@@ -1,4 +1,4 @@
1
- <!-- keel:start version=5.65.0 -->
1
+ <!-- keel:start version=5.66.0 -->
2
2
  ## Keel Bootstrap
3
3
 
4
4
  - Start every session with `keel context`; OpenSpec artifacts and Git are the only durable authority — never native memory, goals, or transcripts.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@christang/keel",
3
3
  "displayName": "Keel",
4
4
  "description": "Keel OpenSpec execution discipline CLI for Claude Code, Codex, and OpenCode.",
5
- "version": "5.65.0",
5
+ "version": "5.66.0",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keel",
3
- "version": "5.65.0",
3
+ "version": "5.66.0",
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.65.0",
3
+ "version": "5.66.0",
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",
@@ -38,8 +38,8 @@ REQUIRED_SCRIPTS = [
38
38
  "scripts/validate_plugin.py",
39
39
  ]
40
40
 
41
- PACKAGE_VERSION = "5.65.0"
42
- PROTOCOL_VERSION = "5.65.0"
41
+ PACKAGE_VERSION = "5.66.0"
42
+ PROTOCOL_VERSION = "5.66.0"
43
43
  LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
44
44
  OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
45
45
  # Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
@@ -17027,6 +17027,42 @@ STANDING_AUTHORIZATION_ACTIONS = (
17027
17027
  )
17028
17028
 
17029
17029
 
17030
+ def run_node_expression(root: Path, expression: str) -> str:
17031
+ """Evaluate a Node expression with `root` as the working directory.
17032
+
17033
+ The declaration set is read from the module that defines it rather than by
17034
+ regexing its source: a regex over the source would be a second copy of the
17035
+ same literal the check exists to remove, one level down (issue #143).
17036
+ """
17037
+ result = subprocess.run(
17038
+ ["node", "-e", expression],
17039
+ cwd=root,
17040
+ text=True,
17041
+ encoding="utf-8",
17042
+ errors="replace",
17043
+ capture_output=True,
17044
+ check=False,
17045
+ )
17046
+ return result.stdout
17047
+
17048
+
17049
+ def config_header_problem(names: list, flat_header: str) -> str:
17050
+ """The one rule for "the header names every declaration", or "" when it does.
17051
+
17052
+ Returns the diagnostic naming the missing declaration. Deliberately reports
17053
+ no count: a count tells an author that two numbers differ, and the name tells
17054
+ them which line to write.
17055
+ """
17056
+ for name in names:
17057
+ if name not in flat_header:
17058
+ return (
17059
+ f"keel/config.yaml's header does not name the `{name}` "
17060
+ "declaration, so a project reading it would never learn it may "
17061
+ "write one. Add it to the header's list."
17062
+ )
17063
+ return ""
17064
+
17065
+
17030
17066
  def write_authorize_config(repo: Path, body: str) -> None:
17031
17067
  (repo / "keel").mkdir(parents=True, exist_ok=True)
17032
17068
  (repo / "keel" / "config.yaml").write_text(body, encoding="utf-8")
@@ -18117,11 +18153,27 @@ def validate_delegation_resident_text_scenario() -> int:
18117
18153
  # M1 — the config header counts its declarations correctly.
18118
18154
  config = ROOT / "keel/config.yaml"
18119
18155
  cfg = flat(config)
18120
- if re.sub(r"\s+", " ", "Five independent declarations") in cfg:
18121
- report("delegation-resident-text: the config header still says five declarations.")
18156
+ # Derived from the module that reads the declarations, not restated here. The
18157
+ # literal this replaces had been hand-bumped twice, and the second bump was
18158
+ # found by a red suite run in a scenario named for something else (#143).
18159
+ declarations = [
18160
+ name
18161
+ for name in run_node_expression(
18162
+ ROOT,
18163
+ "process.stdout.write("
18164
+ "require('./src/core/config.js').CONFIG_DECLARATIONS.join(','))",
18165
+ ).strip().split(",")
18166
+ if name
18167
+ ]
18168
+ if not declarations:
18169
+ report(
18170
+ "delegation-resident-text: src/core/config.js exports no declaration "
18171
+ "list, so the header cannot be checked against what Keel reads."
18172
+ )
18122
18173
  return 1
18123
- if re.sub(r"\s+", " ", "Six independent declarations") not in cfg:
18124
- report("delegation-resident-text: the config header does not name six declarations.")
18174
+ header_problem = config_header_problem(declarations, cfg)
18175
+ if header_problem:
18176
+ report(f"delegation-resident-text: {header_problem}")
18125
18177
  return 1
18126
18178
  if "delegation" not in cfg:
18127
18179
  report("delegation-resident-text: the config header does not document delegation.")
@@ -28591,6 +28643,104 @@ def validate_a_negation_is_not_a_marker_scenario() -> int:
28591
28643
  return 0
28592
28644
 
28593
28645
 
28646
+ def validate_a_count_is_derived_from_what_it_counts_scenario() -> int:
28647
+ """Issue #143: the header assertion restated a set as an English numeral.
28648
+
28649
+ It had been hand-bumped twice, and the second bump was discovered by a red
28650
+ suite run in a scenario whose name has nothing to do with what was changed.
28651
+ An assertion about a set belongs to the set: read it from the module that
28652
+ defines it, and name the member that is missing rather than a count.
28653
+ """
28654
+ label = "a-count-is-derived-from-what-it-counts"
28655
+
28656
+ def declared_names(root: Path) -> list[str]:
28657
+ result = run_node_expression(
28658
+ root,
28659
+ "process.stdout.write("
28660
+ "require('./src/core/config.js').CONFIG_DECLARATIONS.join(','))",
28661
+ )
28662
+ return [name for name in result.strip().split(",") if name]
28663
+
28664
+ names = declared_names(ROOT)
28665
+ if not names:
28666
+ report(
28667
+ f"{label}: the assertion is a literal — `src/core/config.js` exports "
28668
+ "no declaration list, so nothing the header is checked against comes "
28669
+ "from the code that reads it."
28670
+ )
28671
+ return 1
28672
+ for expected in (
28673
+ "fast_check",
28674
+ "authorize",
28675
+ "precedents",
28676
+ "triage",
28677
+ "delegation",
28678
+ "full_mode_paths",
28679
+ ):
28680
+ if expected not in names:
28681
+ report(
28682
+ f"{label}: the exported declaration list omits {expected!r}, so "
28683
+ f"the header is not checked for it; got {names!r}."
28684
+ )
28685
+ return 1
28686
+
28687
+ header = (ROOT / "keel/config.yaml").read_text(encoding="utf-8")
28688
+ flat_header = re.sub(r"\s+", " ", header)
28689
+ for name in names:
28690
+ if name not in flat_header:
28691
+ report(
28692
+ f"{label}: keel/config.yaml's header does not name {name!r}, "
28693
+ "which is the declaration a new project would never learn it may "
28694
+ "write."
28695
+ )
28696
+ return 1
28697
+
28698
+ # The derivation is proved by feeding the check a member the header cannot
28699
+ # contain. Without this the whole scenario is satisfied by a list that
28700
+ # happens to agree with a header nobody compared it to.
28701
+ missing = config_header_problem(names + ["invented_declaration"], flat_header)
28702
+ if not missing:
28703
+ report(
28704
+ f"{label}: the assertion is a literal — a declaration absent from "
28705
+ "the header produced no failure, so the check is not reading the "
28706
+ "exported list."
28707
+ )
28708
+ return 1
28709
+ if "invented_declaration" not in missing:
28710
+ report(
28711
+ f"{label}: the failure does not name the missing declaration; got "
28712
+ f"{missing!r}."
28713
+ )
28714
+ return 1
28715
+ if re.search(r"\b(?:four|five|six|seven)\b", missing, re.IGNORECASE):
28716
+ report(
28717
+ f"{label}: still says six declarations — the failure reports a "
28718
+ f"count rather than the missing name; got {missing!r}."
28719
+ )
28720
+ return 1
28721
+
28722
+ # A header naming every declaration while miscounting them in prose passes:
28723
+ # membership is the checkable property, and a count is a lossy restatement.
28724
+ miscounted = flat_header.replace(
28725
+ "Six independent declarations", "Five independent declarations"
28726
+ )
28727
+ if miscounted == flat_header:
28728
+ report(f"{label}: the header's prose count could not be located to vary it.")
28729
+ return 1
28730
+ if config_header_problem(names, miscounted):
28731
+ report(
28732
+ f"{label}: still says six declarations — a header naming all of them "
28733
+ "while miscounting them in prose was refused."
28734
+ )
28735
+ return 1
28736
+
28737
+ if label not in {name for name, _ in SCENARIOS}:
28738
+ report(f"{label}: the scenario registry does not include it.")
28739
+ return 1
28740
+ report(f"{label} scenario passed.")
28741
+ return 0
28742
+
28743
+
28594
28744
  SCENARIOS: tuple = (
28595
28745
  ("stateless-continuity", validate_stateless_continuity_scenario),
28596
28746
  ("core-gates", validate_core_gates_scenario),
@@ -28953,6 +29103,10 @@ SCENARIOS: tuple = (
28953
29103
  "a-negation-is-not-a-marker",
28954
29104
  validate_a_negation_is_not_a_marker_scenario,
28955
29105
  ),
29106
+ (
29107
+ "a-count-is-derived-from-what-it-counts",
29108
+ validate_a_count_is_derived_from_what_it_counts_scenario,
29109
+ ),
28956
29110
  )
28957
29111
 
28958
29112
 
@@ -68,6 +68,21 @@ function classifyAuthorizationEntry(entry) {
68
68
  // and the declaration must still be correct after both.
69
69
  const DELEGATION_TIERS = ["routine", "standard", "deep"];
70
70
 
71
+ // Every declaration this module reads, in the order the file documents them. One
72
+ // list, so an assertion about the set can be derived from the set instead of
73
+ // restating it: the header check used to pin an English numeral and had been
74
+ // hand-bumped twice, each time discovered by a red suite run in a scenario named
75
+ // for something else (issue #143). Adding a declaration now costs a header line
76
+ // and an entry here, and nothing has to agree about a number.
77
+ const CONFIG_DECLARATIONS = [
78
+ "fast_check",
79
+ "authorize",
80
+ "precedents",
81
+ "triage",
82
+ "delegation",
83
+ "full_mode_paths",
84
+ ];
85
+
71
86
  const CONFIG_RELATIVE_PATH = path.join("keel", "config.yaml");
72
87
 
73
88
  // The declarations share keel/config.yaml with fast_check, so the reader stays
@@ -533,6 +548,7 @@ function triageIssue(repo, labels, issue = null) {
533
548
  module.exports = {
534
549
  CONFIG_RELATIVE_PATH,
535
550
  DELEGATION_TIERS,
551
+ CONFIG_DECLARATIONS,
536
552
  STANDING_AUTHORIZATION_ACTIONS,
537
553
  SCOPED_AUTHORIZATION_ACTIONS,
538
554
  readFullModePaths,