@christang/keel 5.68.0 → 5.69.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.68.0 -->
1
+ <!-- keel:start version=5.69.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.68.0",
5
+ "version": "5.69.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.68.0",
3
+ "version": "5.69.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.68.0",
3
+ "version": "5.69.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.68.0"
42
- PROTOCOL_VERSION = "5.68.0"
41
+ PACKAGE_VERSION = "5.69.0"
42
+ PROTOCOL_VERSION = "5.69.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
@@ -2302,6 +2302,61 @@ def validate_domain_lenses_scenario() -> int:
2302
2302
  return 0
2303
2303
 
2304
2304
 
2305
+ # `bump_version.js` writes a stub section for the new release, and the convention
2306
+ # is to fill it in. Writing the entry *above* it instead leaves an orphan, and
2307
+ # the stub carries the `Version alignment:` line this scenario reads — so the
2308
+ # section describing nothing was the section that passed (#151). It happened in
2309
+ # 5.67.0 and 5.68.0, with the suite green both times.
2310
+ #
2311
+ # Takes the text rather than reading the file, so the rule can be exercised on a
2312
+ # planted copy: a rule that could only ever see the changelog already known to be
2313
+ # correct would pass forever without anyone learning whether it fires.
2314
+ def release_description_problem(changelog: str, version: str) -> str | None:
2315
+ """Return the problem the released version's section has, or None.
2316
+
2317
+ Scoped to the version being released. An older section is history a current
2318
+ author cannot act on, and a rule that swept the whole file would fail on the
2319
+ archive — which is how a check gets disabled rather than fixed. The file also
2320
+ legitimately quotes `TODO` when describing the rules that refuse it.
2321
+ """
2322
+ pattern = re.compile(
2323
+ r"^## " + re.escape(version) + r"\b[^\n]*$", re.M
2324
+ )
2325
+ headings = pattern.findall(changelog)
2326
+ if not headings:
2327
+ return (
2328
+ f"the changelog carries no `## {version}` section, so the release "
2329
+ "describes nothing at all."
2330
+ )
2331
+ if len(headings) > 1:
2332
+ return (
2333
+ f"the changelog describes {version} twice — {len(headings)} `## "
2334
+ f"{version}` headings: {', '.join(h.strip() for h in headings)}. A "
2335
+ "release entry written above the stub instead of into it leaves both."
2336
+ )
2337
+ for heading in headings:
2338
+ if "TODO" in heading:
2339
+ return (
2340
+ f"the changelog still carries the stub heading `{heading.strip()}`"
2341
+ f", so the {version} release was never described. Fill that "
2342
+ "section in rather than writing the entry above it."
2343
+ )
2344
+ # The section body, bounded by the next `## ` heading, so a `TODO` belonging
2345
+ # to an older release is not read as this one's.
2346
+ start = changelog.index(headings[0])
2347
+ rest = changelog[start + len(headings[0]):]
2348
+ end = rest.find("\n## ")
2349
+ body = rest if end < 0 else rest[:end]
2350
+ for line in body.splitlines():
2351
+ if re.match(r"^\s*-\s*TODO\b", line):
2352
+ return (
2353
+ f"the {version} section still carries the stub bullet "
2354
+ f"`{line.strip().lstrip('- ')}`, so the release is titled but "
2355
+ "not described."
2356
+ )
2357
+ return None
2358
+
2359
+
2305
2360
  def validate_version_alignment_scenario() -> int:
2306
2361
  package = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
2307
2362
  if package.get("version") != PACKAGE_VERSION:
@@ -2366,6 +2421,103 @@ def validate_version_alignment_scenario() -> int:
2366
2421
  )
2367
2422
  return 1
2368
2423
 
2424
+ # The released version's section has to say what the release did. Asserted on
2425
+ # planted copies first, so the rule is known to fire, and then on the
2426
+ # repository's own changelog.
2427
+ stub = (
2428
+ f"# Keel Changelog\n\n## {PACKAGE_VERSION} - TODO: summarize this "
2429
+ f"release\n\n- TODO: describe the change.\n- Version alignment: "
2430
+ f"shares Keel {PACKAGE_VERSION}.\n\n## 5.0.0 - older\n\n- Done.\n"
2431
+ )
2432
+ planted = release_description_problem(stub, PACKAGE_VERSION)
2433
+ if not planted:
2434
+ report(
2435
+ "version-alignment scenario: an unfilled stub was accepted — a "
2436
+ f"changelog whose {PACKAGE_VERSION} section is the stub "
2437
+ "`bump_version.js` writes produced no problem, so a release can "
2438
+ "ship describing nothing while this scenario passes."
2439
+ )
2440
+ return 1
2441
+ if "TODO: summarize this release" not in planted:
2442
+ report(
2443
+ "version-alignment scenario: the refusal does not name the offending "
2444
+ f"line, so an author is sent to read the whole changelog; got "
2445
+ f"{planted!r}."
2446
+ )
2447
+ return 1
2448
+
2449
+ # Still M1 — the stub survives renaming its heading. The bullet is the other
2450
+ # half of what `bump_version.js` writes, and a section titled correctly while
2451
+ # its only content is `- TODO: describe the change.` describes nothing.
2452
+ renamed = (
2453
+ f"# Keel Changelog\n\n## {PACKAGE_VERSION} - a real sounding title\n\n"
2454
+ f"- TODO: describe the change.\n- Version alignment: shares Keel "
2455
+ f"{PACKAGE_VERSION}.\n"
2456
+ )
2457
+ bullet = release_description_problem(renamed, PACKAGE_VERSION)
2458
+ if not bullet:
2459
+ report(
2460
+ "version-alignment scenario: an unfilled stub was accepted — a "
2461
+ "section with a real heading whose content is still `- TODO: describe "
2462
+ "the change.` produced no problem, so renaming the stub is enough to "
2463
+ "ship an undescribed release."
2464
+ )
2465
+ return 1
2466
+ if "TODO: describe the change." not in bullet:
2467
+ report(
2468
+ "version-alignment scenario: the bullet refusal does not name the "
2469
+ f"line it found; got {bullet!r}."
2470
+ )
2471
+ return 1
2472
+
2473
+ # M2 — two sections for one version. This is the shape the real defect had,
2474
+ # and neither TODO rule catches it once the stub's heading has been renamed:
2475
+ # the reader then sees two identical version headings, one of which describes
2476
+ # nothing.
2477
+ doubled = (
2478
+ f"# Keel Changelog\n\n## {PACKAGE_VERSION} - the real entry\n\n"
2479
+ f"- Did the thing.\n- Version alignment: shares Keel {PACKAGE_VERSION}."
2480
+ f"\n\n## {PACKAGE_VERSION} - summarized\n\n- Also this.\n"
2481
+ )
2482
+ problem_found = release_description_problem(doubled, PACKAGE_VERSION)
2483
+ if not problem_found:
2484
+ report(
2485
+ "version-alignment scenario: a version described twice was accepted "
2486
+ f"— two `## {PACKAGE_VERSION}` sections produced no problem, which is "
2487
+ "the shape the defect this rule exists for actually had."
2488
+ )
2489
+ return 1
2490
+ if "twice" not in problem_found:
2491
+ report(
2492
+ "version-alignment scenario: the duplicate-section refusal does not "
2493
+ f"say the version is described twice; got {problem_found!r}."
2494
+ )
2495
+ return 1
2496
+
2497
+ # M3 — only the version being released is judged. An older section is history,
2498
+ # and this file legitimately quotes `TODO` when describing the rules that
2499
+ # refuse it, so a rule sweeping the whole file would fire on its own
2500
+ # documentation.
2501
+ older = (
2502
+ f"# Keel Changelog\n\n## {PACKAGE_VERSION} - the real entry\n\n"
2503
+ f"- Did the thing.\n- Version alignment: shares Keel {PACKAGE_VERSION}."
2504
+ "\n\n## 5.0.0 - TODO: summarize this release\n\n- TODO: describe it.\n"
2505
+ )
2506
+ if release_description_problem(older, PACKAGE_VERSION):
2507
+ report(
2508
+ "version-alignment scenario: an older section was refused — a `TODO` "
2509
+ "in a section for another version failed the current release, which "
2510
+ "is a check the archive can only be edited to satisfy."
2511
+ )
2512
+ return 1
2513
+ if release_description_problem(changelog, PACKAGE_VERSION):
2514
+ report(
2515
+ "version-alignment scenario: an older section was refused — the "
2516
+ "repository's own changelog does not satisfy the rule: "
2517
+ f"{release_description_problem(changelog, PACKAGE_VERSION)}"
2518
+ )
2519
+ return 1
2520
+
2369
2521
  with tempfile.TemporaryDirectory(prefix="keel-version-") as raw_tmp:
2370
2522
  repo = Path(raw_tmp) / "repo"
2371
2523
  repo.mkdir()