hoodscript 1.4.0__tar.gz → 1.6.0__tar.gz

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.
Files changed (162) hide show
  1. {hoodscript-1.4.0 → hoodscript-1.6.0}/CHANGELOG.md +68 -0
  2. {hoodscript-1.4.0 → hoodscript-1.6.0}/PKG-INFO +1 -1
  3. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/curriculum.md +7 -5
  4. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/grammar.ebnf +4 -2
  5. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/keywords.md +5 -0
  6. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/_version.py +2 -2
  7. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/cli.py +2 -1
  8. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/constants.py +5 -0
  9. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/errors/catalog.py +28 -0
  10. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/errors/translator.py +20 -0
  11. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/lsp.py +1 -1
  12. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/patterns.py +41 -6
  13. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/sourcing.py +8 -2
  14. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_conformance.py +2 -1
  15. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_errors.py +10 -0
  16. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_fuzz_ebnf.py +3 -0
  17. hoodscript-1.6.0/tests/test_lint_fuzz.py +37 -0
  18. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_lsp_features.py +1 -1
  19. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_patterns.py +10 -0
  20. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_rule_zero.py +12 -0
  21. {hoodscript-1.4.0 → hoodscript-1.6.0}/.claude/skills/align-prompt/SKILL.md +0 -0
  22. {hoodscript-1.4.0 → hoodscript-1.6.0}/.gitignore +0 -0
  23. {hoodscript-1.4.0 → hoodscript-1.6.0}/CODE_OF_CONDUCT.md +0 -0
  24. {hoodscript-1.4.0 → hoodscript-1.6.0}/CONTRIBUTING.md +0 -0
  25. {hoodscript-1.4.0 → hoodscript-1.6.0}/LICENSE +0 -0
  26. {hoodscript-1.4.0 → hoodscript-1.6.0}/README.md +0 -0
  27. {hoodscript-1.4.0 → hoodscript-1.6.0}/SECURITY.md +0 -0
  28. {hoodscript-1.4.0 → hoodscript-1.6.0}/SPRINT_PLAN.md +0 -0
  29. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/adr/0001-token-pipeline-over-parser.md +0 -0
  30. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/adr/0002-keyword-reconciliation.md +0 -0
  31. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/adr/0003-inclusive-ranges.md +0 -0
  32. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/adr/0004-no-regional-dialect-overlays.md +0 -0
  33. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/adr/README.md +0 -0
  34. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/archive/HOODSCRIPT_COMPLETE_MASTER_SYSTEM.md +0 -0
  35. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/archive/HOODSCRIPT_FULL_ENGINEERING_SUITE.md +0 -0
  36. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/archive/HOODSCRIPT_UNSPRED_FEATURES_ROADMAP.md +0 -0
  37. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/hoodlib.md +0 -0
  38. {hoodscript-1.4.0 → hoodscript-1.6.0}/docs/onboarding.md +0 -0
  39. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/quickstart.ipynb +0 -0
  40. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/beatlab/stage1.hs +0 -0
  41. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/beatlab/stage2.hs +0 -0
  42. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/beatlab/stage3.hs +0 -0
  43. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/beatlab/stage4.hs +0 -0
  44. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/beatlab/stage5.hs +0 -0
  45. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/cookout/stage1.hs +0 -0
  46. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/cookout/stage2.hs +0 -0
  47. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/cookout/stage3.hs +0 -0
  48. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/cookout/stage4.hs +0 -0
  49. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/cookout/stage5.hs +0 -0
  50. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/dropday/stage1.hs +0 -0
  51. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/dropday/stage2.hs +0 -0
  52. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/dropday/stage3.hs +0 -0
  53. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/dropday/stage4.hs +0 -0
  54. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/dropday/stage5.hs +0 -0
  55. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level1.hs +0 -0
  56. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level2.hs +0 -0
  57. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level3.hs +0 -0
  58. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level4.hs +0 -0
  59. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level5.hs +0 -0
  60. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level6.hs +0 -0
  61. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level7.hs +0 -0
  62. {hoodscript-1.4.0 → hoodscript-1.6.0}/examples/solutions/level8.hs +0 -0
  63. {hoodscript-1.4.0 → hoodscript-1.6.0}/out.txt +0 -0
  64. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/app.js +0 -0
  65. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/bundle.js +0 -0
  66. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/examples.js +0 -0
  67. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/hoodscript-latest-py3-none-any.whl +0 -0
  68. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/index.html +0 -0
  69. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/style.css +0 -0
  70. {hoodscript-1.4.0 → hoodscript-1.6.0}/playground/worker.js +0 -0
  71. {hoodscript-1.4.0 → hoodscript-1.6.0}/pyproject.toml +0 -0
  72. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/align_prompt.py +0 -0
  73. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/build_playground.py +0 -0
  74. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/coraal_hits.py +0 -0
  75. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/gen_ebnf.py +0 -0
  76. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/gen_keywords.py +0 -0
  77. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/hooks/pre-commit +0 -0
  78. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/hooks/pre-push +0 -0
  79. {hoodscript-1.4.0 → hoodscript-1.6.0}/scripts/install-hooks.sh +0 -0
  80. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/__init__.py +0 -0
  81. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/cli.py +0 -0
  82. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/core/runtime.py +0 -0
  83. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/core/ui.py +0 -0
  84. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/games/beatlab.py +0 -0
  85. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/games/cookout.py +0 -0
  86. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodarcade/games/dropday.py +0 -0
  87. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/__init__.py +0 -0
  88. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/cli.py +0 -0
  89. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/drone.py +0 -0
  90. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/grid.py +0 -0
  91. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/levels.py +0 -0
  92. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/renderer.py +0 -0
  93. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodfarm/runner.py +0 -0
  94. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodlib/__init__.py +0 -0
  95. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodlib/chart.py +0 -0
  96. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodlib/disk.py +0 -0
  97. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/__init__.py +0 -0
  98. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/__main__.py +0 -0
  99. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/cache.py +0 -0
  100. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/debugger.py +0 -0
  101. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/errors/__init__.py +0 -0
  102. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/errors/renderer.py +0 -0
  103. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/formatter.py +0 -0
  104. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/importer.py +0 -0
  105. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/kernel/__init__.py +0 -0
  106. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/kernel/__main__.py +0 -0
  107. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/kernel/installer.py +0 -0
  108. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/kernel/kernel.py +0 -0
  109. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/kernel/magics.py +0 -0
  110. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/linter.py +0 -0
  111. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/migrate.py +0 -0
  112. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/migrator.py +0 -0
  113. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/repl.py +0 -0
  114. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/replay.py +0 -0
  115. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/sandbox.py +0 -0
  116. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/stubs.py +0 -0
  117. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/traceback_handler.py +0 -0
  118. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/transpiler.py +0 -0
  119. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/tutor.py +0 -0
  120. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/hoodscript/zen.py +0 -0
  121. {hoodscript-1.4.0 → hoodscript-1.6.0}/src/vibe.py +0 -0
  122. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/conftest.py +0 -0
  123. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/deep_nesting.hs +0 -0
  124. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/fork_bomb.hs +0 -0
  125. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/huge_literal.hs +0 -0
  126. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/import_escapes.hs +0 -0
  127. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/infinite_recursion.hs +0 -0
  128. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/open_file.hs +0 -0
  129. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/pathological_indent.hs +0 -0
  130. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/runaway_output.hs +0 -0
  131. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/corpus/runaway_time.hs +0 -0
  132. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/test_corpus.py +0 -0
  133. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/test_exec_sites.py +0 -0
  134. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/test_fuzz.py +0 -0
  135. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/security/test_sandbox.py +0 -0
  136. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_align_prompt.py +0 -0
  137. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_arcade.py +0 -0
  138. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_arcade_gates.py +0 -0
  139. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_chart.py +0 -0
  140. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_cli.py +0 -0
  141. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_coraal_hits.py +0 -0
  142. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_debugger.py +0 -0
  143. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_docs.py +0 -0
  144. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_fmt.py +0 -0
  145. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_game_engine.py +0 -0
  146. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_grammar_canonical.py +0 -0
  147. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_grammar_ebnf.py +0 -0
  148. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_hoodlib_disk.py +0 -0
  149. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_importer.py +0 -0
  150. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_kernel.py +0 -0
  151. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_kernel_zmq.py +0 -0
  152. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_keywords_doc.py +0 -0
  153. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_learn.py +0 -0
  154. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_lint.py +0 -0
  155. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_migrate_sourcing.py +0 -0
  156. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_playground.py +0 -0
  157. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_release.py +0 -0
  158. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_repl.py +0 -0
  159. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_replay.py +0 -0
  160. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_transpiler.py +0 -0
  161. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_vscode_extension.py +0 -0
  162. {hoodscript-1.4.0 → hoodscript-1.6.0}/tests/test_wheel_contents.py +0 -0
@@ -5,6 +5,66 @@ Versioning: the keyword set is the public API; changing it is at least a minor b
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.6.0] — 2026-09-12
9
+
10
+ Generalized intensive continuative `steady` (Rule R6), 5 new exception mappings
11
+ in the `*Trip` hierarchy, VS Code extension v1.5.0 package, and curriculum updates.
12
+
13
+ ### Added
14
+
15
+ - **Generalized Intensive Continuative `steady` (Rule R6):**
16
+ `steady` now supports conditional loops in addition to infinite loops:
17
+ `steady <expr>:` → `while <expr>:` (and `steady:` → `while True:`).
18
+ Fully bidirectional: `python_to_hood` (`hood fmt`) maps `while <expr>:` → `steady <expr>:`
19
+ and `while True:` → `steady:`. Grounded in the Yale Grammatical Diversity Project
20
+ intensive continuative aspect (*Steady*, Baugh 1984). EBNF and Hypothesis fuzzer updated.
21
+ - **Five New Exception Mappings in the `*Trip` Hierarchy:**
22
+ `TimeoutTrip` (`TimeoutError`), `InterruptTrip` (`KeyboardInterrupt`), `SystemProblem`
23
+ (`OSError`), `NotDoneTrip` (`NotImplementedError`), and `DisconnectTrip` (`ConnectionError`).
24
+ All 1:1 token-substituted, reverse-translated via `python_to_hood`, and equipped with rich
25
+ conversational plain-English diagnostic cards in the JPE error catalog (HS0140, HS0150,
26
+ HS0160, HS0170).
27
+ - **VS Code Extension Update (`v1.5.0`):**
28
+ Added syntax highlighting in `hoodscript.tmLanguage.json` for `be done`, `finna be done`,
29
+ and `SystemProblem`. Added snippets for `be done` and `steady cond`. Built standalone
30
+ `hoodscript-1.5.0.vsix` distribution package.
31
+ - **Curriculum & CLI Updates:**
32
+ Updated `docs/curriculum.md` Lessons 3, 4, and 8 to showcase `steady <cond>:` and `be done`
33
+ file handling, verified by `tests/test_docs.py` and `hoodscript learn`.
34
+
35
+ ## [1.5.0] — 2026-09-12
36
+
37
+ Rule R13 (future resultative `be done` → `with`), JPE guidance for retired words,
38
+ and owner sign-off on the sourcing audit (`cap`/`no cap` retained with liability notes).
39
+
40
+ ### Added
41
+
42
+ - `tests/test_lint_fuzz.py`: the grammar fuzzer from `test_fuzz_ebnf.py`
43
+ driven against the linter's pure-`ast` rules (HL002, HL004–HL006) — 2,000
44
+ generated programs on the thorough profile, no crashes, every finding has a
45
+ filled what/why/fix. PROMPTS.md §2.2 now asks for both fuzz files together
46
+ after a `patterns.py`/`linter.py` change.
47
+ - **Sourcing audit signed off by owner (2026-09-12):**
48
+ Owner decision on `cap` / `no cap`: kept in Tier B, with the liability screen
49
+ hit (GDoS/UD secondary sense) and 2005-test failure (2012 earliest) notes
50
+ recorded in `GRAMMAR.md` and `hoodscript/docs/linguistics.md`. Sourcing audit
51
+ status corrections applied to the spec: `trip` single-source caveat lifted
52
+ (two sources confirmed); `dip` updated to dictionary + corpus; `chill` cited
53
+ to Lee 1999 (*American Speech*); `fam` cited to OED and Dictionary.com;
54
+ `tryna` re-ranked below `finna`; Labov 1972 cited for `ain't`.
55
+ - **Rule R13: Future Resultative `be done` → Context Manager (`with`):**
56
+ Added normative rule R13 implementing `be done expr [as target]:` → `with expr [as target]:`
57
+ and `finna be done expr [as target]:` → `async with expr [as target]:`. Sourced to
58
+ Labov (1972), Dayton (1996), and Green (2002) as the future resultative aspect marking
59
+ guaranteed, inevitable completion — matching Python's context manager contract. Reversible
60
+ via `python_to_hood` (`hood fmt`). EBNF updated in `docs/grammar.ebnf` and tested in
61
+ `tests/test_patterns.py` and `tests/test_fuzz_ebnf.py`.
62
+ - **JPE Did-You-Mean for Retired Lexicon:**
63
+ Added retired/cut keyword recognition in `errors.translator._suggest()` so typos or legacy
64
+ code (`cook`, `serve`, `Facts`, `Ghost`, `stand_on_it`, `nada`, `peep`, `crew`, `bounce`)
65
+ receive immediate, conversational guidance pointing to their canonical HoodScript keywords
66
+ (`bet`, `dip`, `no cap`, `MissingFileTrip`, `AssertTrip`, `None`, `import`, `list`).
67
+
8
68
  ## [1.4.0] — 2026-09-12
9
69
 
10
70
  New library module, new CLI command, three new lint rules, and the prompt
@@ -25,6 +85,14 @@ aligner. Keyword set unchanged (v1.1).
25
85
  2026-09-12 — DRAFT" (`cap`/`no cap` liability + 2018 dating; `trip` now
26
86
  two-source; `dip` meets the corpus clause; `tryna` re-ranked; Rickford →
27
87
  Labov on the `ain't` row). No keyword changed.
88
+ - **Tier B sourcing sprint** (PROMPTS.md §3.8): an evidence ledger for the
89
+ eight naming rows in `hoodscript/docs/linguistics.md` — per row, the
90
+ source stating Black/AAVE use (quoted, opened 2026-09-13), the second
91
+ source, CORAAL count, the clause of the Tier B rule it meets, and the
92
+ 2005 test. `trip` is two-source; `dip` meets dictionary + corpus; `chill`'s
93
+ second source is Lee 1999 (*American Speech*); `no cap` fails the 2005
94
+ test (2012 earliest). Primaries (Smitherman, Major, Widawski, ODAAE)
95
+ remain unopenable from here and the section says exactly why.
28
96
  - **`hoodscript migrate <file.hs>`** — rewrites v1.0.0 code to the v1.1
29
97
  lexicon from the CHANGELOG migration table: two line-preserving token
30
98
  passes (v1.0.0 map → Python → `py2hood`), so `cook`→`bet`, `serve`→`dip`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hoodscript
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: A production-grade Python 3 conlang based on AAVE with 1:1 AST parity.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -101,7 +101,8 @@ n is 4
101
101
 
102
102
  `be x in xs` is `for x in xs` — habitual *be*, the thing that happens over
103
103
  and over. `1..3` is 1, 2, 3 — inclusive, unlike Python's `range`. `steady:`
104
- is `while True:` — keep going until `break`. `skip` is `continue`.
104
+ is `while True:` — keep going until `break`. `steady <cond>:` is `while <cond>:`.
105
+ `skip` is `continue`.
105
106
 
106
107
  ---
107
108
 
@@ -115,7 +116,7 @@ bet greet(who, greeting="wassup"):
115
116
  double = lambda x: x * 2
116
117
  holla(greet("fam"), greet("Khris", "yo"), double(21))
117
118
  bet countdown(n):
118
- while n > 0:
119
+ steady n > 0:
119
120
  yield n
120
121
  n = n - 1
121
122
  holla(list(countdown(3)))
@@ -243,14 +244,14 @@ exception without a `*Trip` name is caught under its Python name.
243
244
 
244
245
  ## Lesson 8 — Modules and files
245
246
 
246
- **Keywords:** none
247
+ **Keywords:** `be done`
247
248
 
248
249
  ```hood
249
250
  import json
250
251
  from pathlib import Path
251
252
  import math as m
252
253
  holla(json.dumps({"ok": True}), m.sqrt(16), Path("."))
253
- with open("out.txt", "w") as f:
254
+ be done open("out.txt", "w") as f:
254
255
  f.write("written from HoodScript\n")
255
256
  holla(open("out.txt").read().strip())
256
257
  ```
@@ -260,7 +261,8 @@ holla(open("out.txt").read().strip())
260
261
  written from HoodScript
261
262
  ```
262
263
 
263
- The whole Python ecosystem is available, spelled the Python way. To import
264
+ The whole Python ecosystem is available. A context manager (`with`) is
265
+ `be done ... as f:` — future resultative: guaranteed completion. To import
264
266
  your own `.hs` files, see the onboarding guide §6–7.
265
267
 
266
268
  ---
@@ -10,7 +10,7 @@ program = { statement } ;
10
10
 
11
11
  statement = output_stmt | assignment | const_decl | func_def | class_def
12
12
  | return_stmt | habitual_loop | continuative_loop | while_loop
13
- | conditional | try_stmt | throw_stmt | import_stmt
13
+ | context_stmt | conditional | try_stmt | throw_stmt | import_stmt
14
14
  | loop_control | pass_stmt | python_stmt | expr_stmt ;
15
15
 
16
16
  output_stmt = "holla" [ expression { "," expression } | "(" [ args ] ")" ] ;
@@ -18,12 +18,14 @@ input_expr = "ask" ( expression | "(" [ args ] ")" ) ;
18
18
  assignment = target "=" expression ;
19
19
  const_decl = "BIN" IDENT "=" expression ;
20
20
 
21
+ context_stmt = [ "finna" ] "be" "done" expression [ "as" target ] ":" block ;
22
+
21
23
  func_def = [ "finna" ] "bet" IDENT "(" [ params ] ")" ":" block ;
22
24
  return_stmt = ( "dip" | "return" ) [ expression ] ;
23
25
  class_def = "fam" IDENT [ "(" IDENT { "," IDENT } ")" ] ":" block ;
24
26
 
25
27
  habitual_loop = "be" target "in" expression ":" block ;
26
- continuative_loop = "steady" ":" block ;
28
+ continuative_loop = "steady" [ expression ] ":" block ;
27
29
  while_loop = "while" expression ":" block ;
28
30
  loop_control = "skip" | "break" ;
29
31
  pass_stmt = "chill" ;
@@ -72,6 +72,11 @@ Every other Python keyword and builtin is used under its Python name.
72
72
  | `MissingModuleTrip` | `ModuleNotFoundError` |
73
73
  | `BadImportTrip` | `ImportError` |
74
74
  | `NoAccessTrip` | `PermissionError` |
75
+ | `TimeoutTrip` | `TimeoutError` |
76
+ | `InterruptTrip` | `KeyboardInterrupt` |
77
+ | `SystemProblem` | `OSError` |
78
+ | `NotDoneTrip` | `NotImplementedError` |
79
+ | `DisconnectTrip` | `ConnectionError` |
75
80
 
76
81
  ## Reserved words
77
82
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.4.0'
22
- __version_tuple__ = version_tuple = (1, 4, 0)
21
+ __version__ = version = '1.6.0'
22
+ __version_tuple__ = version_tuple = (1, 6, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -74,12 +74,13 @@ def print_tokens() -> None:
74
74
  ("no cap", "True", "R3 Truth assertion"),
75
75
  ("cap", "False", "R3 Falsehood"),
76
76
  ("it's x", "(x is not None)", "R4 Existence test"),
77
- ("steady:", "while True:", "R5 Persistent loop"),
77
+ ("steady [x]:", "while True: / while x:", "R5 Persistent loop"),
78
78
  ("a..b", "range(a, (b) + 1)", "R6 Inclusive range"),
79
79
  ("BIN X = v", "X: Final = v", "R7 Constant declaration"),
80
80
  ("else if", "elif", "R8 Chained conditional"),
81
81
  ("holla x, y", "print(x, y)", "R9 Statement-form print"),
82
82
  ("ask x", "input(x)", "R10 Statement-form input"),
83
+ ("be done x:", "with x:", "R13 Context manager"),
83
84
  ]
84
85
  for src, py, desc in patterns:
85
86
  print(f" {src:<20} -> {py:<25} ({desc})")
@@ -54,6 +54,11 @@ HOOD_EXCEPTION_NAMES = {
54
54
  "MissingModuleTrip": "ModuleNotFoundError",
55
55
  "BadImportTrip": "ImportError",
56
56
  "NoAccessTrip": "PermissionError",
57
+ "TimeoutTrip": "TimeoutError",
58
+ "InterruptTrip": "KeyboardInterrupt",
59
+ "SystemProblem": "OSError",
60
+ "NotDoneTrip": "NotImplementedError",
61
+ "DisconnectTrip": "ConnectionError",
57
62
  }
58
63
 
59
64
  HOOD_KEYWORD_MAP = {**TIER_A, **TIER_B, **TIER_C, **HOOD_EXCEPTION_NAMES}
@@ -215,6 +215,34 @@ CATALOG: list[Entry] = [
215
215
  pattern=r"'(?P<value>[^']+)'$",
216
216
  fields=("value",)),
217
217
 
218
+ # --- TimeoutError ------------------------------------------------------
219
+ Entry("HS0140", TimeoutError,
220
+ what="The operation took too long and timed out: {raw}.",
221
+ why="Something you asked for took longer than the time limit allowed.",
222
+ fix="Increase the timeout limit, or check whether the network or task is stuck.",
223
+ example="throw TimeoutTrip(\"timed out\")\n"),
224
+
225
+ # --- NotImplementedError -----------------------------------------------
226
+ Entry("HS0150", NotImplementedError,
227
+ what="This part hasn't been built yet.",
228
+ why="A function or method was called that was left as a placeholder or needs to be overridden.",
229
+ fix="Fill in the body of the function, or provide the implementation in your class.",
230
+ example="throw NotDoneTrip(\"not ready\")\n"),
231
+
232
+ # --- ConnectionError ---------------------------------------------------
233
+ Entry("HS0160", ConnectionError,
234
+ what="Could not connect to the network service.",
235
+ why="The network connection was refused, dropped, or the destination could not be reached.",
236
+ fix="Check your internet connection, verify the address and port, or try again later.",
237
+ example="throw DisconnectTrip(\"connection refused\")\n"),
238
+
239
+ # --- OSError -----------------------------------------------------------
240
+ Entry("HS0170", OSError,
241
+ what="The operating system reported a problem: {raw}.",
242
+ why="The operating system could not complete a disk, network, or hardware operation.",
243
+ fix="Check system resources, permissions, and available disk space.",
244
+ example="throw SystemProblem(\"device unavailable\")\n"),
245
+
218
246
  # ---------------------------------------------------------------------------
219
247
  # Compile-time (SyntaxError family). Line/col come from the exception itself.
220
248
  # ---------------------------------------------------------------------------
@@ -70,13 +70,33 @@ def _module_names(filename):
70
70
  return names
71
71
 
72
72
 
73
+ RETIRED_SUGGESTIONS = {
74
+ "cook": "bet",
75
+ "serve": "dip",
76
+ "Facts": "no cap",
77
+ "Ghost": "MissingFileTrip",
78
+ "GhostFileTrip": "MissingFileTrip",
79
+ "stand_on_it": "assert",
80
+ "StandOnItTrip": "AssertTrip",
81
+ "nada": "None",
82
+ "peep": "import",
83
+ "crew": "list",
84
+ "bounce": "dip",
85
+ "holler": "holla",
86
+ }
87
+
88
+
73
89
  def _suggest(exc, tb, filename):
74
90
  if isinstance(exc, NameError) and not isinstance(exc, UnboundLocalError):
75
91
  name = getattr(exc, "name", None)
92
+ if name in RETIRED_SUGGESTIONS:
93
+ return RETIRED_SUGGESTIONS[name]
76
94
  return _close(name, _scope_names(tb)) if name else None
77
95
  if isinstance(exc, AttributeError):
78
96
  obj, name = getattr(exc, "obj", None), getattr(exc, "name", None)
79
97
  if name is not None and obj is not None:
98
+ if name in RETIRED_SUGGESTIONS:
99
+ return RETIRED_SUGGESTIONS[name]
80
100
  return _close(name, dir(obj))
81
101
  if isinstance(exc, ModuleNotFoundError):
82
102
  name = getattr(exc, "name", None)
@@ -17,7 +17,7 @@ server = LanguageServer("hoodscript-lsp", __version__)
17
17
  _WORD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
18
18
 
19
19
  _PATTERNS = {
20
- "steady": "`steady:` → `while True:`",
20
+ "steady": "`steady:` → `while True:` / `steady expr:` → `while expr:`",
21
21
  "BIN": "`BIN X = v` → `X: Final = v`",
22
22
  "no": "`no cap` → `True`",
23
23
  "nobody": "`ain't nobody x` → `not x`",
@@ -211,9 +211,28 @@ def _edits(text):
211
211
  edits.append((row, end, end, " is not None)")) # wrap; inner edits survive
212
212
  i += 1; continue # keep scanning inside the expression
213
213
 
214
- # R6 steady: -> while True:
215
- if _is(t, tokenize.NAME, "steady") and _stmt_start(toks, i) and nxt is not None and _is(nxt, tokenize.OP, ":"):
216
- edits.append((row, col, t.end[1], "while True")); i += 1; continue
214
+ # R6 steady: -> while True: / steady expr: -> while expr:
215
+ if _is(t, tokenize.NAME, "steady") and _stmt_start(toks, i):
216
+ if nxt is not None and _is(nxt, tokenize.OP, ":"):
217
+ edits.append((row, col, t.end[1], "while True")); i += 1; continue
218
+ else:
219
+ k = i + 1
220
+ has_header_colon = False
221
+ depth = 0
222
+ while k < len(toks) and toks[k].type not in (tokenize.NEWLINE, tokenize.ENDMARKER):
223
+ if toks[k].type == tokenize.OP:
224
+ if toks[k].string in _OPENERS:
225
+ depth += 1
226
+ elif toks[k].string in _CLOSERS:
227
+ depth -= 1
228
+ elif depth == 0 and toks[k].string == ":":
229
+ has_header_colon = True
230
+ break
231
+ elif depth == 0 and toks[k].string == "=":
232
+ break
233
+ k += 1
234
+ if has_header_colon:
235
+ edits.append((row, col, t.end[1], "while")); i += 1; continue
217
236
 
218
237
  # R9 BIN X = v -> [from typing import Final; ]X: Final = v
219
238
  if _is(t, tokenize.NAME, "BIN") and _stmt_start(toks, i):
@@ -270,6 +289,15 @@ def _edits(text):
270
289
  and _is(nxt, tokenize.NAME, "if") and nxt.start[0] == row):
271
290
  edits.append((row, col, nxt.end[1], "elif")); i += 2; continue
272
291
 
292
+ # R13 be done expr: -> with expr: (or finna be done -> async with)
293
+ if (_is(t, tokenize.NAME, "be") and nxt is not None and _is(nxt, tokenize.NAME, "done")
294
+ and nxt.start[0] == row):
295
+ at_be_done = _stmt_start(toks, i) or (
296
+ i > 0 and _is(toks[i - 1], tokenize.NAME, "finna") and _stmt_start(toks, i - 1)
297
+ )
298
+ if at_be_done:
299
+ edits.append((row, col, nxt.end[1], "with")); i += 2; continue
300
+
273
301
  i += 1
274
302
  return lines, edits
275
303
 
@@ -331,9 +359,16 @@ def _reverse_edits(text):
331
359
  same_row = i + 1 < len(toks) and toks[i + 1].start[0] == row
332
360
  stmt = _stmt_start(toks, i)
333
361
 
334
- # while True: -> steady:
335
- if name(i, "while") and stmt and same_row and name(i + 1, "True") and op(i + 2, ":"):
336
- edits.append((row, col, toks[i + 1].end[1], "steady")); i += 2; continue
362
+ # while True: -> steady: / while expr: -> steady expr:
363
+ if name(i, "while") and stmt:
364
+ if same_row and name(i + 1, "True") and op(i + 2, ":"):
365
+ edits.append((row, col, toks[i + 1].end[1], "steady")); i += 2; continue
366
+ else:
367
+ edits.append((row, col, t.end[1], "steady")); i += 1; continue
368
+
369
+ # with expr: -> be done expr: (also async with -> finna be done)
370
+ if name(i, "with") and (stmt or (i > 0 and name(i - 1, "async") and _stmt_start(toks, i - 1))):
371
+ edits.append((row, col, t.end[1], "be done")); i += 1; continue
337
372
 
338
373
  # elif -> else if
339
374
  if name(i, "elif") and stmt:
@@ -57,8 +57,14 @@ CUT: frozenset[str] = frozenset({
57
57
  })
58
58
 
59
59
  # Multi-token forms (patterns.py) that are keywords without being NAME entries.
60
- PATTERN_FORMS = {"steady": "while True:", "BIN": "X: Final = v", "ain't": "not", "it's": "(x is not None)",
61
- "no cap": "True", "trip": "*Trip exception suffix"}
60
+ PATTERN_FORMS = {
61
+ "steady": "while True: / while expr:",
62
+ "BIN": "X: Final = v",
63
+ "ain't": "not",
64
+ "it's": "(x is not None)",
65
+ "no cap": "True",
66
+ "trip": "*Trip exception suffix",
67
+ }
62
68
 
63
69
 
64
70
  # ----------------------------------------------------------------------------- 1. standing
@@ -80,4 +80,5 @@ def test_exception_name_prefers_most_specific():
80
80
  assert get_hood_exception_name(SyntaxError) == "BadSyntaxTrip"
81
81
  assert get_hood_exception_name(KeyError) == "MissingKeyTrip"
82
82
  assert get_hood_exception_name(RuntimeError) == "Problem"
83
- assert get_hood_exception_name(KeyboardInterrupt) == "BaseProblem"
83
+ assert get_hood_exception_name(KeyboardInterrupt) == "InterruptTrip"
84
+ assert get_hood_exception_name(SystemExit) == "BaseProblem"
@@ -85,6 +85,16 @@ def test_did_you_mean_knows_hoodscript_keywords():
85
85
  assert d.suggestion == "holla"
86
86
 
87
87
 
88
+ def test_did_you_mean_knows_retired_words():
89
+ exc = _run_example("x = cook\n")
90
+ d = diagnose(type(exc), exc, exc.__traceback__)
91
+ assert d.suggestion == "bet"
92
+ exc = _run_example("x = Facts\n")
93
+ assert diagnose(type(exc), exc, exc.__traceback__).suggestion == "no cap"
94
+ exc = _run_example("x = Ghost\n")
95
+ assert diagnose(type(exc), exc, exc.__traceback__).suggestion == "MissingFileTrip"
96
+
97
+
88
98
  def test_did_you_mean_for_attributes_and_modules():
89
99
  exc = _run_example("fam Dog:\n bet speak(self):\n chill\nDog().speek()\n")
90
100
  assert diagnose(type(exc), exc, exc.__traceback__).suggestion == "speak"
@@ -113,12 +113,15 @@ def _stmt(depth=0):
113
113
  st.tuples(_ident, _postfix(), _postfix(), body).map(
114
114
  lambda t: f"be {t[0]} in {t[1]}..{t[2]}:\n{_indent(t[3])}"),
115
115
  body.map(lambda b: f"steady:\n{_indent(b)}"),
116
+ st.tuples(_expr(), body).map(lambda t: f"steady {t[0]}:\n{_indent(t[1])}"),
116
117
  st.tuples(_expr(), body).map(lambda t: f"while {t[0]}:\n{_indent(t[1])}"),
117
118
  st.tuples(_ident, st.lists(_ident, max_size=2, unique=True), body).map(
118
119
  lambda t: f"bet {t[0]}({', '.join(t[1])}):\n{_indent(t[2])}"),
119
120
  st.tuples(_ident, st.lists(_ident, max_size=2, unique=True), body).map(
120
121
  lambda t: f"finna bet {t[0]}({', '.join(t[1])}):\n{_indent(t[2])}"),
121
122
  st.tuples(_ident, body).map(lambda t: f"fam {t[0]}:\n{_indent(t[1])}"),
123
+ st.tuples(_ident, body).map(lambda t: f"be done {t[0]}:\n{_indent(t[1])}"),
124
+ st.tuples(_ident, _ident, body).map(lambda t: f"be done {t[0]} as {t[1]}:\n{_indent(t[2])}"),
122
125
  st.tuples(body, body).map(lambda t: f"tryna:\n{_indent(t[0])}\ncatch BadValueTrip as e:\n{_indent(t[1])}"),
123
126
  st.tuples(body, body, body).map(
124
127
  lambda t: f"tryna:\n{_indent(t[0])}\ncatch:\n{_indent(t[1])}\nregardless:\n{_indent(t[2])}"),
@@ -0,0 +1,37 @@
1
+ """Property test for the linter: `lint_source`'s ast-only checks (HL002, HL004,
2
+ HL005, HL006) must never crash, no matter what valid HoodScript the grammar
3
+ fuzzer generates. tests/test_fuzz_ebnf.py already builds a grammar-directed
4
+ generator of valid programs; this reuses it rather than duplicating one.
5
+
6
+ ruff- and mypy-backed findings (HL001, HL003) are not exercised here — they
7
+ shell out, which is what test_lint.py's few hand-written cases already cover;
8
+ this test is about the pure-ast rules added with hoodlib.chart/replay."""
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from hypothesis import HealthCheck, given, settings
15
+
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
17
+
18
+ from hoodscript.linter import _check_ast_rules, _check_unreachable # noqa: E402
19
+ from hoodscript.migrator import hood_to_python # noqa: E402
20
+ from test_fuzz_ebnf import _python_accepts, programs # noqa: E402
21
+
22
+ settings.register_profile("thorough", max_examples=2000, deadline=None)
23
+ settings.register_profile("default", max_examples=200, deadline=None,
24
+ suppress_health_check=[HealthCheck.too_slow])
25
+ settings.load_profile(os.environ.get("HYPOTHESIS_PROFILE", "default"))
26
+
27
+
28
+ @given(programs)
29
+ def test_ast_lint_rules_never_crash_on_valid_programs(src):
30
+ py = hood_to_python(src)
31
+ if not _python_accepts(py):
32
+ return
33
+ findings = _check_unreachable(py, "fuzz.hs") + _check_ast_rules(py, "fuzz.hs")
34
+ for f in findings:
35
+ assert f.line >= 1 and f.col >= 1
36
+ assert f.what and f.why and f.fix
37
+ assert "{" not in f.what and "{" not in f.why and "{" not in f.fix # no unfilled template braces
@@ -31,7 +31,7 @@ def test_lsp_hover_keywords_and_patterns():
31
31
  assert _describe("bet") == "**`bet`** → Python `def`"
32
32
  assert _describe("holla") == "**`holla`** → Python `print`"
33
33
  assert _describe("ZeroSplitTrip") == "**`ZeroSplitTrip`** → Python exception `ZeroDivisionError`"
34
- assert _describe("steady") == "`steady:` → `while True:`"
34
+ assert _describe("steady") == "`steady:` → `while True:` / `steady expr:` → `while expr:`"
35
35
  assert _describe("unknown_word") is None
36
36
 
37
37
 
@@ -15,6 +15,8 @@ RULES = [
15
15
  ("R5b", "if it's a and b:\n chill\n", "if (a is not None) and b:\n pass\n"),
16
16
  ("R5c", "f(it's x, 2)\n", "f((x is not None), 2)\n"),
17
17
  ("R6", "steady:\n break\n", "while True:\n break\n"),
18
+ ("R6b", "steady count < 5:\n chill\n", "while count < 5:\n pass\n"),
19
+ ("R6c", "steady it's item:\n chill\n", "while (item is not None):\n pass\n"),
18
20
  ("R7", "be x in xs:\n chill\n", "for x in xs:\n pass\n"),
19
21
  ("R8", "finna bet f():\n chill\n", "async def f():\n pass\n"),
20
22
  ("R9", "BIN LIMIT = 5\n", "from typing import Final; LIMIT: Final = 5\n"),
@@ -30,6 +32,11 @@ RULES = [
30
32
  ("R11d", 'holla("call form")\n', 'print("call form")\n'),
31
33
  ("R12", 'ask "name? "\n', 'input("name? ")\n'),
32
34
  ("R12b", 'line = ask("> ")\n', 'line = input("> ")\n'),
35
+ ("R13", "be done open('f') as f:\n chill\n", "with open('f') as f:\n pass\n"),
36
+ ("R13b", "finna bet f():\n finna be done open('f') as f:\n chill\n",
37
+ "async def f():\n async with open('f') as f:\n pass\n"),
38
+ ("R13c", "be done lock:\n chill\n", "with lock:\n pass\n"),
39
+ ("R13d", "be done a() as x, b() as y:\n chill\n", "with a() as x, b() as y:\n pass\n"),
33
40
  ("elif", "if x:\n chill\nelse if y:\n chill\n", "if x:\n pass\nelif y:\n pass\n"),
34
41
  ]
35
42
 
@@ -84,6 +91,7 @@ def test_patterns_compose():
84
91
 
85
92
  @pytest.mark.parametrize("py,hood", [
86
93
  ("while True:\n break\n", "steady:\n break\n"),
94
+ ("while count < 5:\n pass\n", "steady count < 5:\n chill\n"),
87
95
  ("if a:\n pass\nelif b:\n pass\n", "if a:\n chill\nelse if b:\n chill\n"),
88
96
  ("x = True\ny = not x\nz = a is not b\nw = a not in b\n",
89
97
  "x = no cap\ny = ain't x\nz = a is not b\nw = a not in b\n"),
@@ -95,6 +103,8 @@ def test_patterns_compose():
95
103
  ("print(a, b)\nprint()\nx = print(1)\ninput(\"> \")\n", 'holla a, b\nholla\nx = holla(1)\nask "> "\n'),
96
104
  ("print(i, True, not x)\n", "holla i, no cap, ain't x\n"),
97
105
  ("s = \"it's not True\" # not a comment\n", "s = \"it's not True\" # not a comment\n"),
106
+ ("with open('f') as f:\n pass\n", "be done open('f') as f:\n chill\n"),
107
+ ("async with open('f') as f:\n pass\n", "finna be done open('f') as f:\n chill\n"),
98
108
  ])
99
109
  def test_reverse_patterns(py, hood):
100
110
  from hoodscript.migrator import python_to_hood
@@ -110,3 +110,15 @@ def test_screen_script_uses_word_boundaries():
110
110
  def test_cut_list_is_not_empty_and_has_no_live_keyword():
111
111
  assert len(CUT) >= 50
112
112
  assert not (CUT & set(HOOD_KEYWORD_MAP))
113
+
114
+
115
+ @pytest.mark.parametrize("word", sorted(TIER_B) + ["no cap", "trip"])
116
+ def test_tier_b_ledger_names_the_clause_met(word):
117
+ """The Tier B rule has two clauses (two dictionaries, or one + CORAAL).
118
+ The evidence ledger must say which one each row meets."""
119
+ text = LINGUISTICS.read_text(encoding="utf-8")
120
+ ledger = text.split("### Evidence ledger")[1].split("### What changes")[0]
121
+ rows = [ln for ln in ledger.splitlines() if ln.startswith(f"| `{word}`")]
122
+ assert rows, f"no ledger row for `{word}`"
123
+ clauses = ("two dictionaries", "dictionary + corpus", "two sources")
124
+ assert any(c in rows[0] for c in clauses), f"ledger row for `{word}` does not name a clause"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes