hoodscript 1.3.1__tar.gz → 1.4.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 (161) hide show
  1. hoodscript-1.4.0/.claude/skills/align-prompt/SKILL.md +32 -0
  2. {hoodscript-1.3.1 → hoodscript-1.4.0}/CHANGELOG.md +100 -0
  3. {hoodscript-1.3.1 → hoodscript-1.4.0}/CONTRIBUTING.md +7 -0
  4. {hoodscript-1.3.1 → hoodscript-1.4.0}/PKG-INFO +6 -2
  5. {hoodscript-1.3.1 → hoodscript-1.4.0}/README.md +5 -1
  6. {hoodscript-1.3.1 → hoodscript-1.4.0}/SECURITY.md +3 -2
  7. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/adr/0002-keyword-reconciliation.md +11 -5
  8. hoodscript-1.4.0/docs/hoodlib.md +105 -0
  9. hoodscript-1.4.0/scripts/align_prompt.py +288 -0
  10. hoodscript-1.4.0/scripts/coraal_hits.py +123 -0
  11. hoodscript-1.4.0/scripts/hooks/pre-commit +13 -0
  12. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodlib/__init__.py +2 -2
  13. hoodscript-1.4.0/src/hoodlib/chart.py +167 -0
  14. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/_version.py +2 -2
  15. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/cli.py +18 -4
  16. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/linter.py +96 -0
  17. hoodscript-1.4.0/src/hoodscript/migrate.py +146 -0
  18. hoodscript-1.4.0/src/hoodscript/replay.py +325 -0
  19. hoodscript-1.4.0/src/hoodscript/sourcing.py +410 -0
  20. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/test_exec_sites.py +1 -1
  21. hoodscript-1.4.0/tests/test_align_prompt.py +108 -0
  22. hoodscript-1.4.0/tests/test_chart.py +91 -0
  23. hoodscript-1.4.0/tests/test_coraal_hits.py +74 -0
  24. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_docs.py +1 -1
  25. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_fmt.py +1 -1
  26. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_lint.py +54 -0
  27. hoodscript-1.4.0/tests/test_migrate_sourcing.py +235 -0
  28. hoodscript-1.4.0/tests/test_replay.py +125 -0
  29. hoodscript-1.4.0/tests/test_rule_zero.py +112 -0
  30. {hoodscript-1.3.1 → hoodscript-1.4.0}/.gitignore +0 -0
  31. {hoodscript-1.3.1 → hoodscript-1.4.0}/CODE_OF_CONDUCT.md +0 -0
  32. {hoodscript-1.3.1 → hoodscript-1.4.0}/LICENSE +0 -0
  33. {hoodscript-1.3.1 → hoodscript-1.4.0}/SPRINT_PLAN.md +0 -0
  34. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/adr/0001-token-pipeline-over-parser.md +0 -0
  35. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/adr/0003-inclusive-ranges.md +0 -0
  36. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/adr/0004-no-regional-dialect-overlays.md +0 -0
  37. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/adr/README.md +0 -0
  38. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/archive/HOODSCRIPT_COMPLETE_MASTER_SYSTEM.md +0 -0
  39. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/archive/HOODSCRIPT_FULL_ENGINEERING_SUITE.md +0 -0
  40. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/archive/HOODSCRIPT_UNSPRED_FEATURES_ROADMAP.md +0 -0
  41. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/curriculum.md +0 -0
  42. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/grammar.ebnf +0 -0
  43. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/keywords.md +0 -0
  44. {hoodscript-1.3.1 → hoodscript-1.4.0}/docs/onboarding.md +0 -0
  45. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/quickstart.ipynb +0 -0
  46. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/beatlab/stage1.hs +0 -0
  47. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/beatlab/stage2.hs +0 -0
  48. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/beatlab/stage3.hs +0 -0
  49. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/beatlab/stage4.hs +0 -0
  50. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/beatlab/stage5.hs +0 -0
  51. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/cookout/stage1.hs +0 -0
  52. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/cookout/stage2.hs +0 -0
  53. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/cookout/stage3.hs +0 -0
  54. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/cookout/stage4.hs +0 -0
  55. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/cookout/stage5.hs +0 -0
  56. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/dropday/stage1.hs +0 -0
  57. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/dropday/stage2.hs +0 -0
  58. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/dropday/stage3.hs +0 -0
  59. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/dropday/stage4.hs +0 -0
  60. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/dropday/stage5.hs +0 -0
  61. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level1.hs +0 -0
  62. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level2.hs +0 -0
  63. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level3.hs +0 -0
  64. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level4.hs +0 -0
  65. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level5.hs +0 -0
  66. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level6.hs +0 -0
  67. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level7.hs +0 -0
  68. {hoodscript-1.3.1 → hoodscript-1.4.0}/examples/solutions/level8.hs +0 -0
  69. {hoodscript-1.3.1 → hoodscript-1.4.0}/out.txt +0 -0
  70. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/app.js +0 -0
  71. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/bundle.js +0 -0
  72. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/examples.js +0 -0
  73. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/hoodscript-latest-py3-none-any.whl +0 -0
  74. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/index.html +0 -0
  75. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/style.css +0 -0
  76. {hoodscript-1.3.1 → hoodscript-1.4.0}/playground/worker.js +0 -0
  77. {hoodscript-1.3.1 → hoodscript-1.4.0}/pyproject.toml +0 -0
  78. {hoodscript-1.3.1 → hoodscript-1.4.0}/scripts/build_playground.py +0 -0
  79. {hoodscript-1.3.1 → hoodscript-1.4.0}/scripts/gen_ebnf.py +0 -0
  80. {hoodscript-1.3.1 → hoodscript-1.4.0}/scripts/gen_keywords.py +0 -0
  81. {hoodscript-1.3.1 → hoodscript-1.4.0}/scripts/hooks/pre-push +0 -0
  82. {hoodscript-1.3.1 → hoodscript-1.4.0}/scripts/install-hooks.sh +0 -0
  83. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/__init__.py +0 -0
  84. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/cli.py +0 -0
  85. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/core/runtime.py +0 -0
  86. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/core/ui.py +0 -0
  87. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/games/beatlab.py +0 -0
  88. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/games/cookout.py +0 -0
  89. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodarcade/games/dropday.py +0 -0
  90. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/__init__.py +0 -0
  91. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/cli.py +0 -0
  92. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/drone.py +0 -0
  93. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/grid.py +0 -0
  94. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/levels.py +0 -0
  95. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/renderer.py +0 -0
  96. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodfarm/runner.py +0 -0
  97. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodlib/disk.py +0 -0
  98. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/__init__.py +0 -0
  99. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/__main__.py +0 -0
  100. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/cache.py +0 -0
  101. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/constants.py +0 -0
  102. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/debugger.py +0 -0
  103. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/errors/__init__.py +0 -0
  104. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/errors/catalog.py +0 -0
  105. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/errors/renderer.py +0 -0
  106. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/errors/translator.py +0 -0
  107. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/formatter.py +0 -0
  108. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/importer.py +0 -0
  109. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/kernel/__init__.py +0 -0
  110. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/kernel/__main__.py +0 -0
  111. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/kernel/installer.py +0 -0
  112. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/kernel/kernel.py +0 -0
  113. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/kernel/magics.py +0 -0
  114. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/lsp.py +0 -0
  115. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/migrator.py +0 -0
  116. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/patterns.py +0 -0
  117. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/repl.py +0 -0
  118. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/sandbox.py +0 -0
  119. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/stubs.py +0 -0
  120. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/traceback_handler.py +0 -0
  121. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/transpiler.py +0 -0
  122. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/tutor.py +0 -0
  123. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/hoodscript/zen.py +0 -0
  124. {hoodscript-1.3.1 → hoodscript-1.4.0}/src/vibe.py +0 -0
  125. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/conftest.py +0 -0
  126. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/deep_nesting.hs +0 -0
  127. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/fork_bomb.hs +0 -0
  128. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/huge_literal.hs +0 -0
  129. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/import_escapes.hs +0 -0
  130. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/infinite_recursion.hs +0 -0
  131. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/open_file.hs +0 -0
  132. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/pathological_indent.hs +0 -0
  133. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/runaway_output.hs +0 -0
  134. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/corpus/runaway_time.hs +0 -0
  135. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/test_corpus.py +0 -0
  136. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/test_fuzz.py +0 -0
  137. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/security/test_sandbox.py +0 -0
  138. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_arcade.py +0 -0
  139. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_arcade_gates.py +0 -0
  140. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_cli.py +0 -0
  141. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_conformance.py +0 -0
  142. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_debugger.py +0 -0
  143. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_errors.py +0 -0
  144. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_fuzz_ebnf.py +0 -0
  145. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_game_engine.py +0 -0
  146. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_grammar_canonical.py +0 -0
  147. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_grammar_ebnf.py +0 -0
  148. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_hoodlib_disk.py +0 -0
  149. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_importer.py +0 -0
  150. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_kernel.py +0 -0
  151. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_kernel_zmq.py +0 -0
  152. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_keywords_doc.py +0 -0
  153. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_learn.py +0 -0
  154. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_lsp_features.py +0 -0
  155. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_patterns.py +0 -0
  156. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_playground.py +0 -0
  157. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_release.py +0 -0
  158. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_repl.py +0 -0
  159. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_transpiler.py +0 -0
  160. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_vscode_extension.py +0 -0
  161. {hoodscript-1.3.1 → hoodscript-1.4.0}/tests/test_wheel_contents.py +0 -0
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: align-prompt
3
+ description: Align a pasted generation prompt (from Gemini or anywhere) with the current HoodScript release before implementing it. Use whenever the user pastes a prompt that names files, keywords, or features to build.
4
+ ---
5
+
6
+ # align-prompt
7
+
8
+ The user pastes a generation prompt. Do not execute it as written. Do this:
9
+
10
+ 1. Save the pasted prompt verbatim to `$SCRATCHPAD/prompt-in.md`.
11
+ 2. Run the mechanical half:
12
+ ```
13
+ python scripts/align_prompt.py $SCRATCHPAD/prompt-in.md -o $SCRATCHPAD/prompt-aligned.md --report $SCRATCHPAD/prompt-report.md
14
+ ```
15
+ It rewrites the v1.0.0 lexicon to v1.1, swaps stale phrasing/paths/commands,
16
+ prefixes lines that ask for declined/dropped work with `[DECLINED …]` /
17
+ `[DROPPED …]` / `[NEVER …]`, and prepends the PROMPTS.md §0 brief.
18
+ 3. Do the judgment half — diff the aligned scope against the tree:
19
+ - `git ls-files src tests examples docs`, CHANGELOG `[Unreleased]` and the
20
+ latest release section, `CLAUDE.md` "Current status".
21
+ - For each item in the aligned prompt, classify: **shipped** (on `main`,
22
+ cite the path), **declined/dropped** (cite ADR or CHANGELOG), **gap**
23
+ (not on disk — the only thing to implement), or **conflict** (asks for
24
+ something the standing brief forbids — say so, don't build it).
25
+ 4. Show the user a short table (item → status → evidence) and the report's
26
+ flagged lines. Then implement only the **gap** rows, one sprint, under the
27
+ gate (`ruff check && pytest`), on the current branch rules (agents.md).
28
+ 5. If the aligned prompt is worth keeping, record the reconciliation as a
29
+ PROMPTS.md §3.x section like §3.5 — mapping table + the short correct prompt.
30
+
31
+ Never: reintroduce `cook`/`serve`/`Facts`/`holler`/`libcst`; add a keyword;
32
+ build a dialect overlay (ADR-0004); create a tag.
@@ -5,6 +5,106 @@ 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.4.0] — 2026-09-12
9
+
10
+ New library module, new CLI command, three new lint rules, and the prompt
11
+ aligner. Keyword set unchanged (v1.1).
12
+
13
+ ### Added
14
+
15
+ - **Sourcing audit sprint** (PROMPTS.md §2.9, worked record §3.7). Every
16
+ keyword, pattern form, and the `*Trip` suffix re-run through
17
+ `lexical-sourcing.md` §5 with the sources open rather than remembered:
18
+ the nine Yale GDP pages fetched and quoted; Green's Dictionary of Slang
19
+ read entry by entry (variety labels and first-quotation years now in the
20
+ table); CORAAL grepped for the first time — 231 interviews, 1.39 M tokens,
21
+ via a mirror because the Oregon host refused connections; the Urban
22
+ Dictionary liability screen run on all Tier B words. Results and
23
+ corrections: `hoodscript/docs/linguistics.md` "Sourcing audit —
24
+ 2026-09-12". Decisions for the owner: GRAMMAR.md "Sourcing audit
25
+ 2026-09-12 — DRAFT" (`cap`/`no cap` liability + 2018 dating; `trip` now
26
+ two-source; `dip` meets the corpus clause; `tryna` re-ranked; Rickford →
27
+ Labov on the `ain't` row). No keyword changed.
28
+ - **`hoodscript migrate <file.hs>`** — rewrites v1.0.0 code to the v1.1
29
+ lexicon from the CHANGELOG migration table: two line-preserving token
30
+ passes (v1.0.0 map → Python → `py2hood`), so `cook`→`bet`, `serve`→`dip`,
31
+ `holler(x)`→`holla x`, `Facts`→`no cap`, `GhostFileTrip`→`MissingFileTrip`,
32
+ the 21 builtin aliases → their Python names. Auto-detects v1.0.0 by its
33
+ exclusive tokens (`dip` meant `break` there — a v1.1 file is left alone
34
+ with a note; `--from 1.0` forces). `-w` writes with a `.bak`; `--diff`.
35
+ Refuses to write if the line count changed. `align_prompt.py` fixed
36
+ prompts; this fixes code.
37
+ - **`hoodscript sourcing <word>`** — Rule Zero as one command
38
+ (`src/hoodscript/sourcing.py`): standing (keyword / cut / candidate, with
39
+ the `linguistics.md` rows when run from a checkout), Green's Dictionary of
40
+ Slang senses with variety labels and first-quotation years, the Urban
41
+ Dictionary liability screen by rank, CORAAL hits + KWIC with `--coraal`,
42
+ and the checklist of steps no script can do. `--offline`, `--json`. The
43
+ cut list now lives in `sourcing.CUT` (one source for the command and
44
+ `tests/test_rule_zero.py`); `scripts/coraal_hits.py` imports the corpus
45
+ code from here.
46
+ - `scripts/coraal_hits.py` — step 3 of the sourcing procedure as a tool:
47
+ counts every keyword in official CORAAL text files or a JSONL mirror,
48
+ `--kwic` prints the lines so senses can be read, not guessed.
49
+ - `tests/test_rule_zero.py` — the release package's constitutional test
50
+ (the prototype's `test_one_to_one_law.py` read the prototype's table and
51
+ never guarded `constants.py`): cut words stay cut (the full v1.0 + v1.1 +
52
+ alias cut list), tiers are disjoint, Tier B ≤ 9, every Tier A/B keyword
53
+ has a row in `linguistics.md`, Urban Dictionary never appears in a
54
+ citation row, the docs mention every keyword, and the screen matches red
55
+ flags on word boundaries.
56
+
57
+ ### Fixed
58
+
59
+ - `hoodscript/scripts/screen_keyword.py` flagged `something` for `meth` and
60
+ `essex` for `sex` (substring matching); now word-boundary regexes. The UD
61
+ API has returned no vote counts since 2024, so the script reports UD's
62
+ own rank instead of a meaningless `score +0`.
63
+ - `hoodscript/docs/lexical-sourcing.md` §5 step 7 sent contributors to the
64
+ frozen prototype's `KEYWORD_TO_AST`; now `src/hoodscript/constants.py` +
65
+ `tests/test_conformance.py` + `docs/keywords.md`. Green's Dictionary of
66
+ Slang added to the Tier 3 table as the first dictionary to check.
67
+ - `docs/adr/0002` listed `tryna` and `chill` as Tier C (they are A and B);
68
+ CLAUDE.md/agents.md said 16 exception names (17); GRAMMAR.md's `*Trip`
69
+ list lacked `BadImportTrip`.
70
+
71
+ - **Creative-DX sprint** (from a pasted "Horizon & Creative DX" prompt, aligned
72
+ via `scripts/align_prompt.py`; reconciliation in PROMPTS.md §3.6). Built:
73
+ - **`hoodlib.chart`** — Braille/block sparklines, bar charts, histograms,
74
+ `show_bag()` for game inventories. Standard library only; every function
75
+ returns a string; empty/non-numeric input is a plain `BadValueTrip` /
76
+ `WrongKindTrip`. Documented with runnable examples in `docs/hoodlib.md`
77
+ (now part of the docs-are-tests set, together with `hoodlib.disk`).
78
+ - **`hoodscript replay <file.hs>`** — run a program under `sys.settrace`,
79
+ then step through it backwards: `f`/`b`, `j N`, `l N` (first step at a
80
+ line), `v` (locals), `w` (caller chain). Snapshots are deep copies;
81
+ uncopyable values are stored as `repr`; `--max-steps` caps memory;
82
+ `--dump` prints the trace for pipes. A crash is reported by the JPE
83
+ engine and the prompt opens *at* the crash so `b` steps back from it.
84
+ The 1:1 line law is what makes this cheap: trace lines are `.hs` lines.
85
+ - **Lint HL004–HL006** — exact `ast` rules, no tool needed: a secret-looking
86
+ name assigned a real-looking string literal (placeholders and
87
+ `os.environ` reads are ignored); nesting deeper than four blocks inside
88
+ a function (one finding per chain); a function that is one big `if`
89
+ (guard-clause opportunity). Plain-English what/why/fix like HL001–HL003.
90
+ Declined from the same prompt, with reasons: a `pass_to` pipe operator
91
+ (a new keyword without a citation, and `g(f(x))` rewriting is not one
92
+ construct per construct — Rule Zero and the 1:1 law), voice-to-code
93
+ (`hoodscript mic`: a speech stack the zero-dep core forbids, and a spoken
94
+ dialect map — ADR-0004 in audio). Dropped: BeatMode audio cues (not a
95
+ language feature, untestable in CI), a `hoodlib.tui` DSL over Rich (Rich is
96
+ the DSL), `hoodlib.collections` query wrappers (comprehensions, `sorted`,
97
+ `itertools.groupby`, `Counter`), and a 0–10 "Vibe Score" (not actionable;
98
+ its real rules became HL004–HL006).
99
+
100
+ - `scripts/align_prompt.py` + `/align-prompt` skill: rewrites a stale
101
+ generation prompt to the current release (v1.1 lexicon, line law wording,
102
+ current paths/commands), marks lines that ask for declined or dropped work
103
+ (`[DECLINED — ADR-0004]`, `[DROPPED …]`, `[NEVER — libcst]`), prepends the
104
+ PROMPTS.md §0 brief, and prints a change report. Prose is left alone —
105
+ English words that are also v1.0.0 tokens (`say`, `cut`, `word`, `mine`…)
106
+ are rewritten only in code context. Idempotent. PROMPTS.md §3.5.
107
+
8
108
  ## [1.3.1] — 2026-09-12
9
109
 
10
110
  Patch release: what the fresh-machine check of 1.3.0 found, plus two audit
@@ -126,6 +126,13 @@ and the end-to-end audit in [`PROMPTS.md`](PROMPTS.md) §2.8, and only then
126
126
  merges. Two batches arrived on 2026-09-11 claiming completion with defects
127
127
  their own tests could not see; that is why.
128
128
 
129
+ Concretely, for an agent (decided 2026-09-12): it works on a `gemini/<topic>`
130
+ branch — never on `main`, never in a checkout another agent is using — pushes
131
+ it, opens a PR with `.github/PULL_REQUEST_TEMPLATE.md` filled in, and stops.
132
+ The `scripts/hooks/pre-commit` hook refuses commits on `main` unless
133
+ `git config hoodscript.owner` matches `user.email` (the owner sets it once;
134
+ `scripts/install-hooks.sh` installs the hooks).
135
+
129
136
  ## Working with an AI agent
130
137
 
131
138
  Paste the standing brief from [`PROMPTS.md`](PROMPTS.md) §0 at the top of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hoodscript
3
- Version: 1.3.1
3
+ Version: 1.4.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
@@ -110,7 +110,10 @@ hoodscript -c hello.hs # print the Python it becomes
110
110
  hoodscript repl --mirror # interactive REPL (hood> prompt; chill to exit; --mirror shows the Python)
111
111
  hoodscript playground # launch in-browser Pyodide WebAssembly playground
112
112
  hoodscript kernel install # Jupyter kernel (pip install "hoodscript[jupyter]"); or %load_ext hoodscript
113
- hoodscript fmt|lint <file.hs> # formatter / linter (pip install "hoodscript[tools]")
113
+ hoodscript fmt|lint <file.hs> # formatter / linter (pip install "hoodscript[tools]"); lint has HL001–HL006
114
+ hoodscript replay <file.hs> # run it, then step through it backwards (b/f/j/l/v/w); --dump prints all
115
+ hoodscript migrate <file.hs> # rewrite v1.0.0 code (cook/serve/holler/Facts…) to the v1.1 lexicon; -w writes, --diff shows
116
+ hoodscript sourcing <word> # Rule Zero check: standing, Green's Dictionary of Slang, UD liability screen, --coraal PATH
114
117
  hoodscript learn [topic] # interactive tutor driving docs/curriculum.md
115
118
  hoodscript # a short tour of all commands
116
119
  hoodscript zen # the Zen of HoodScript (HPEP 20); or `import vibe`
@@ -196,6 +199,7 @@ hoodarcade play cookout 1 # edit the solution file; the stage re-runs on s
196
199
  - [`docs/onboarding.md`](docs/onboarding.md) — from zero to a running program in ten minutes
197
200
  - [`docs/curriculum.md`](docs/curriculum.md) — ten lessons, each with a runnable file
198
201
  - [`MANUAL.md`](MANUAL.md) — the full manual
202
+ - [`docs/hoodlib.md`](docs/hoodlib.md) — the small standard library: `chart` (sparklines, bars, histograms), `disk` (atomic files)
199
203
  - [`agents.md`](agents.md) — architecture and subsystem walkthrough
200
204
  - [`CLAUDE.md`](CLAUDE.md) — the standing brief for anyone (human or agent) changing the code
201
205
  - [`SPRINT_PLAN.md`](SPRINT_PLAN.md) — what's next · [`CHANGELOG.md`](CHANGELOG.md) · [`CONTRIBUTING.md`](CONTRIBUTING.md)
@@ -60,7 +60,10 @@ hoodscript -c hello.hs # print the Python it becomes
60
60
  hoodscript repl --mirror # interactive REPL (hood> prompt; chill to exit; --mirror shows the Python)
61
61
  hoodscript playground # launch in-browser Pyodide WebAssembly playground
62
62
  hoodscript kernel install # Jupyter kernel (pip install "hoodscript[jupyter]"); or %load_ext hoodscript
63
- hoodscript fmt|lint <file.hs> # formatter / linter (pip install "hoodscript[tools]")
63
+ hoodscript fmt|lint <file.hs> # formatter / linter (pip install "hoodscript[tools]"); lint has HL001–HL006
64
+ hoodscript replay <file.hs> # run it, then step through it backwards (b/f/j/l/v/w); --dump prints all
65
+ hoodscript migrate <file.hs> # rewrite v1.0.0 code (cook/serve/holler/Facts…) to the v1.1 lexicon; -w writes, --diff shows
66
+ hoodscript sourcing <word> # Rule Zero check: standing, Green's Dictionary of Slang, UD liability screen, --coraal PATH
64
67
  hoodscript learn [topic] # interactive tutor driving docs/curriculum.md
65
68
  hoodscript # a short tour of all commands
66
69
  hoodscript zen # the Zen of HoodScript (HPEP 20); or `import vibe`
@@ -146,6 +149,7 @@ hoodarcade play cookout 1 # edit the solution file; the stage re-runs on s
146
149
  - [`docs/onboarding.md`](docs/onboarding.md) — from zero to a running program in ten minutes
147
150
  - [`docs/curriculum.md`](docs/curriculum.md) — ten lessons, each with a runnable file
148
151
  - [`MANUAL.md`](MANUAL.md) — the full manual
152
+ - [`docs/hoodlib.md`](docs/hoodlib.md) — the small standard library: `chart` (sparklines, bars, histograms), `disk` (atomic files)
149
153
  - [`agents.md`](agents.md) — architecture and subsystem walkthrough
150
154
  - [`CLAUDE.md`](CLAUDE.md) — the standing brief for anyone (human or agent) changing the code
151
155
  - [`SPRINT_PLAN.md`](SPRINT_PLAN.md) — what's next · [`CHANGELOG.md`](CHANGELOG.md) · [`CONTRIBUTING.md`](CONTRIBUTING.md)
@@ -50,7 +50,7 @@ To protect host environments against runaway workloads, the sandbox enforces thr
50
50
 
51
51
  ## 4. Code Execution Audit
52
52
 
53
- `exec`/`eval` appear in exactly six places, all on code objects produced by
53
+ `exec`/`eval` appear in exactly seven places, all on code objects produced by
54
54
  `compile()` from the transpiler's output of the user's own source — never on
55
55
  text from anywhere else:
56
56
  1. `src/hoodscript/cli.py` — running a file
@@ -59,7 +59,8 @@ text from anywhere else:
59
59
  4. `src/hoodscript/tutor.py` — running a curriculum lesson
60
60
  5. `src/hoodscript/kernel/kernel.py` — executing notebook cells in the HoodScript kernel
61
61
  6. `src/hoodscript/kernel/magics.py` — executing `%%hood` cells in IPython / Google Colab
62
- `tests/security/test_exec_sites.py` fails if a seventh appears.
62
+ 7. `src/hoodscript/replay.py` — running a file under the `sys.settrace` recorder (`hoodscript replay`)
63
+ `tests/security/test_exec_sites.py` fails if an eighth appears.
63
64
 
64
65
  ### Why this is safe and equivalent to `compile()`:
65
66
  - `transpile()` performs strictly lexical substitution over Python's `tokenize.tokenize` stream and deterministic character-preserving pattern transforms (Rules R1–R12).
@@ -1,7 +1,9 @@
1
1
  # ADR-0002: Linguistic Sourcing & Keyword Reconciliation
2
2
 
3
3
  ## Status
4
- Accepted (Sprint 1–2)
4
+ Accepted (Sprint 1–2). Tier lists corrected 2026-09-12 to match GRAMMAR.md v1.1
5
+ (`tryna` is Tier A, `chill` Tier B); re-audited with sources open — see
6
+ `hoodscript/docs/linguistics.md` "Sourcing audit — 2026-09-12".
5
7
 
6
8
  ## Context
7
9
  The Sprint 0 prototype inherited a set of keywords (`cook`, `serve`, `crew`, `Facts`,
@@ -18,10 +20,14 @@ A rigorous audit against published AAVE lexicography revealed that:
18
20
  We adopted the 7-step Lexical Sourcing Policy (`hoodscript/docs/lexical-sourcing.md`)
19
21
  and reconciled the keywords to authentic, documented AAVE grammar and lexicon:
20
22
  * **Tier A Grammatical Features:** Habitual *be* (`be ... in`), intensive continuative
21
- *steady* (`steady:`), immediate future *finna*, completive *done*, and remote past *BIN*.
22
- * **Tier B High-Frequency Vocabulary:** `bet` (agreement/commitment), `dip` (leave/return),
23
- `fam` (class/family), `no cap` (true), `cap` (false), `holla` (print).
24
- * **Tier C Control Constructs:** `tryna`, `catch`, `regardless`, `throw`, `skip`, `chill`.
23
+ *steady* (`steady:`), immediate future *finna*, completive *done*, remote past *BIN*,
24
+ negation *ain't* / *ain't nobody*, existential *it's*, and *tryna* — each with a Yale
25
+ Grammatical Diversity Project page.
26
+ * **Tier B Lexical Keywords:** `bet` (agreement/commitment), `dip` (leave/return),
27
+ `fam` (class/family), `no cap` (true), `cap` (false), `holla` (print), `chill` (pass),
28
+ and the `*Trip` exception suffix — each with dictionary attestation of AAVE use.
29
+ * **Tier C Plain English, kept deliberately:** `catch`, `regardless`, `throw`, `skip`, `ask`,
30
+ `else if` — labelled as plain English, never claimed as AAVE.
25
31
  * **Python Pass-through:** Where no cited AAVE word existed with verifiable pedigree
26
32
  prior to 2005, Python's standard keywords (`if`, `else`, `while`, `import`, `with`)
27
33
  pass through unchanged rather than inventing artificial slang.
@@ -0,0 +1,105 @@
1
+ # hoodlib — the small standard library
2
+
3
+ `hoodlib` ships with HoodScript and holds only what Python's own library
4
+ doesn't give you in one call. Everything in it is standard-library Python,
5
+ importable from `.hs` or `.py`, with no dependencies. Errors are the plain
6
+ Python exceptions the crash reporter already explains.
7
+
8
+ ```
9
+ from hoodlib import chart, disk
10
+ ```
11
+
12
+ ## `hoodlib.chart` — plots that fit in a terminal
13
+
14
+ Every function returns a string, so you decide whether to `holla` it, log it,
15
+ or test it.
16
+
17
+ ### Sparklines
18
+
19
+ Two points per character on a four-dot Braille scale (80 columns show 160
20
+ points), or one ▁▂▃ block per point with `style="blocks"`. The minimum still
21
+ shows one dot, so every point is visible.
22
+
23
+ ```hood
24
+ from hoodlib import chart
25
+ temps = [61, 64, 70, 75, 73, 68, 62]
26
+ holla chart.sparkline(temps, style="blocks")
27
+ holla chart.sparkline(temps)
28
+ ```
29
+
30
+ ```
31
+ ▁▃▅█▇▅▁
32
+ ⣠⣾⣷⡀
33
+ ```
34
+
35
+ `width=N` caps the output at N characters by resampling, never by cutting.
36
+
37
+ ### Bar charts
38
+
39
+ ```hood
40
+ from hoodlib import chart
41
+ holla chart.bar_chart({"corn": 12, "beans": 6, "hay": 0}, title="Bag", width=32)
42
+ ```
43
+
44
+ ```
45
+ Bag
46
+ ────────────────────────────────
47
+ corn │█████████████████████ 12
48
+ beans │██████████ 6
49
+ hay │ 0
50
+ ```
51
+
52
+ Bars scale to the largest value. `sort=True` orders them, `color=True` adds
53
+ plain ANSI colour, and negative values draw an empty bar with the number
54
+ rather than pretending to be zero. `chart.show_bag(bot.bag)` is the same
55
+ thing for a game inventory, and an empty bag is a line saying so.
56
+
57
+ ### Histograms
58
+
59
+ ```hood
60
+ from hoodlib import chart
61
+ scores = [55, 62, 68, 71, 74, 75, 78, 81, 85, 93]
62
+ holla chart.histogram(scores, bins=4, width=32)
63
+ ```
64
+
65
+ ```
66
+ 55–64.50 │██████████ 2
67
+ 64.50–74 │██████████ 2
68
+ 74–83.50 │███████████████████ 4
69
+ 83.50–93 │██████████ 2
70
+ ```
71
+
72
+ ### What goes wrong, in plain English
73
+
74
+ ```hood
75
+ # expect-crash
76
+ from hoodlib import chart
77
+ holla chart.sparkline([])
78
+ ```
79
+
80
+ The report is a `BadValueTrip` saying `sparkline() got an empty series;
81
+ there is nothing to plot`. A non-number names the item: `item 1 is 'x'
82
+ (str)`.
83
+
84
+ ## `hoodlib.disk` — files that can't half-write
85
+
86
+ Atomic writes (temp file + fsync + rename), per-path locks so two threads
87
+ can't tear a file, and timestamped backups.
88
+
89
+ ```hood
90
+ from hoodlib import disk
91
+ disk.write_json("scores.json", {"ali": 3, "bo": 5})
92
+ holla disk.read_json("scores.json")
93
+ disk.append_csv("log.csv", ["ali", 3])
94
+ holla disk.read_csv("log.csv", header=False)
95
+ holla disk.read_json("missing.json", default={})
96
+ ```
97
+
98
+ ```
99
+ {'ali': 3, 'bo': 5}
100
+ [['ali', '3']]
101
+ {}
102
+ ```
103
+
104
+ `disk.backup(path, keep=5)` copies `path` to `path.<timestamp>.bak` and
105
+ prunes older copies. A missing file with no `default` is a `MissingFileTrip`.
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: E501 (rule tables read better on one line each)
3
+ """Align a stale generation prompt with the current release.
4
+
5
+ Prompts written against the v1.0.0 package (the `cook`/`serve` lexicon, "AST
6
+ isomorphism", `libcst`, `hoodscript vibe`, ...) keep arriving. Executing one as
7
+ written regresses the language; aligning one by hand is the procedure in
8
+ PROMPTS.md §3.5. This script does the mechanical half of that procedure and
9
+ flags the half that needs a decision:
10
+
11
+ 1. lexicon v1.0.0 words -> v1.1 words (the CHANGELOG migration table);
12
+ common English words (`say`, `word`, `cut`, ...) only inside
13
+ backticks / code fences / `word (python)` glosses, so prose
14
+ is not mangled
15
+ 2. phrasing "AST isomorphism" -> the line law, "Friendly Coach" -> the
16
+ JPE engine, stale paths and commands -> current ones
17
+ 3. decisions lines that ask for something the owner declined or dropped
18
+ (dialect overlays -> ADR-0004; hoodpkg, hoodlib.vault/net,
19
+ libcst -> CHANGELOG) are kept but marked [DECLINED ...] /
20
+ [DROPPED ...] so the agent does not do them and the reader
21
+ sees why
22
+ 4. framing the PROMPTS.md §0 standing brief goes on top; a footer tells
23
+ the agent to diff the scope against the tree before writing
24
+
25
+ What it cannot do: know what is already on disk. That is the "diff against
26
+ disk" step of §3.5 and stays a judgment call.
27
+
28
+ Usage:
29
+ python scripts/align_prompt.py prompt.md # aligned prompt on stdout, report on stderr
30
+ python scripts/align_prompt.py - < prompt.md
31
+ python scripts/align_prompt.py prompt.md -o aligned.md --report report.md
32
+ python scripts/align_prompt.py prompt.md --check # exit 1 if anything would change
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import re
39
+ import sys
40
+ from dataclasses import dataclass, field
41
+ from pathlib import Path
42
+
43
+ ROOT = Path(__file__).resolve().parents[1]
44
+
45
+ # --------------------------------------------------------------------------- 1. lexicon
46
+ # Source of truth: CHANGELOG.md "keyword migration table" (v1.0.0 -> v1.1) and
47
+ # src/hoodscript/constants.py. Unambiguous tokens are rewritten anywhere;
48
+ # AMBIGUOUS ones (real English words) only in code context.
49
+
50
+ LEXICON: dict[str, str] = {
51
+ # keywords
52
+ "cook": "bet", "serve": "dip", "crew": "fam", "holler": "holla", "Facts": "no cap", "Cap": "cap",
53
+ "Ghost": "None", "mine": "self", "say": "if", "or_say": "else if", "otherwise": "else",
54
+ "run_thru": "be", "up_in": "in", "keep_goin_if": "while", "keep_pushin": "skip", "attempt": "tryna",
55
+ "aint": "ain't", "on_the_side": "finna", "hold_up": "done", "crash_out": "throw", "catch_heat": "catch",
56
+ "stand_on_it": "assert", "rock_with": "with", "cut": "del", "whole_city": "global",
57
+ "outside_scope": "nonlocal", "inspect": "match", "scenario": "case", "bring_in": "import", "outta": "from",
58
+ "quick_cook": "lambda", "pass_it": "yield", "and_also": "and", "or_else": "or", "straight_is": "is",
59
+ "whatever": "_",
60
+ # builtins
61
+ "check_mic": "assert", "count_up": "len", "span": "range", "what_kind": "type", "built_like": "isinstance",
62
+ "word": "str", "whole_num": "int", "point_num": "float", "lineup": "list", "ledger": "dict", "circle": "set",
63
+ "locked_lineup": "tuple", "unlock_file": "open", "tally": "sum", "index_run": "enumerate", "pair_up": "zip",
64
+ "every_single": "all", "any_at_all": "any", "weed_out": "filter", "apply_all": "map", "ancestor": "super",
65
+ # exceptions whose roots were cut
66
+ "GhostFileTrip": "MissingFileTrip", "StandOnItTrip": "AssertTrip",
67
+ }
68
+
69
+ # English words that are also v1.0.0 tokens: rewrite only in code context.
70
+ AMBIGUOUS = frozenset({
71
+ "say", "otherwise", "mine", "attempt", "cut", "inspect", "scenario", "word", "circle", "span",
72
+ "tally", "ledger", "lineup", "whatever", "serve", "crew", "cook", "ancestor",
73
+ })
74
+
75
+ # --------------------------------------------------------------------------- 2. phrasing
76
+ # (pattern, replacement, note). Applied in order, case-insensitive where flagged.
77
+ PHRASES: list[tuple[str, str, str]] = [
78
+ (r"(?:a )?strict 1:1 AST isomorphism to CPython(?: 3\.1\d(?:[–-]3\.1\d)?)?",
79
+ "the 1:1 line law (token substitution; line N of the .py is line N of the .hs; no AST rewriting) on Python 3.10–3.13",
80
+ "AST isomorphism -> line law"),
81
+ # v1.0.0 `dip` meant break; v1.1 `dip` is return (CHANGELOG). Rewrite the glossed form before the lexicon pass.
82
+ (r"\bdip \(break\)", "break (break)", "dip (break) -> break: v1.1 dip is return"),
83
+ (r"`dip`\s*(?:->|→|=>)\s*`break`", "`break` (Python's word; v1.1 `dip` is `return`)", "dip -> break remap removed"),
84
+ (r"1:1 AST (?:isomorphism|parity|mapping)", "1:1 line mapping (token substitution, no AST rewriting)", "AST isomorphism -> line law"),
85
+ (r"100% CPython PEG grammar parity", "CPython compiles the output unchanged (the 1:1 line law)", "grammar parity -> line law"),
86
+ (r"lexical AST rewriting", "token-stream rewriting (`tokenize` -> NAME substitution -> `untokenize`)", "AST rewriting -> token rewriting"),
87
+ (r"with AST validation", "via `hoodscript.transpile` -> `compile` -> `exec`", "AST validation -> transpile/compile"),
88
+ (r"Friendly Coach(?: error hook(?: integrations)?)?", "the JPE error engine (`hoodscript.errors`: `diagnose` -> `render_plain`; register the source in `linecache` first)",
89
+ "Friendly Coach -> JPE engine"),
90
+ (r"`?hoodscript vibe`?(?![-\w])", "`hoodscript zen`", "hoodscript vibe -> hoodscript zen"),
91
+ (r"`?bring_in vibe`?", "`import vibe`", "bring_in vibe -> import vibe"),
92
+ (r"examples/arcade_solutions/?", "examples/solutions/<game>/", "arcade_solutions path"),
93
+ (r"tests/test_hoodlib\.py", "tests/test_hoodlib_disk.py", "test_hoodlib path"),
94
+ (r"`hoodarcade levels <game>`", "`hoodarcade list` / `hoodarcade levels [game]`", "levels command"),
95
+ (r"`--watch` live solution reloading", "hot-reload on save (the default of `play`; `--once` disables it)", "--watch is the default"),
96
+ (r"`hood` CLI entry point|`hood` command", "`hoodscript` CLI (there is no `hood` entry point in the release package)", "hood -> hoodscript"),
97
+ (r"Python 3\.11\+", "Python 3.10–3.13", "Python floor is 3.10"),
98
+ (r"prompt-toolkit, pygments, libcst, jedi, pygls", "zero-dependency core; extras `repl lsp tools pretty farm arcade jupyter all dev`", "dependency list"),
99
+ (r"beyond `rich`, `cryptography`, and `prompt-toolkit`", "beyond what pyproject's extras already declare (`rich` for the games; the core has none)", "deps: no cryptography"),
100
+ ]
101
+
102
+ # --------------------------------------------------------------------------- 3. decisions
103
+ # (pattern, marker). A line matching a pattern is prefixed with the marker.
104
+ DECISIONS: list[tuple[str, str]] = [
105
+ (r"dialect|Bay Area|Atlanta \(|DMV \(|UK Drill|\bMLE\b|kill_moe|on_mama|WastemanTrip",
106
+ "[DECLINED — ADR-0004 (docs/adr/0004-no-regional-dialect-overlays.md): no dialect overlays under Rule Zero]"),
107
+ (r"hoodpkg|hood\.toml|hood\.lock|\bpkg\.py\b|hood (?:new|add|install|lock|publish)\b",
108
+ "[DROPPED — duplicates pyproject/pip; .hs files are already importable modules (CHANGELOG 1.3.0)]"),
109
+ (r"vault\.py|hoodlib\.vault|AES-GCM|PBKDF2|payload signing",
110
+ "[DROPPED — hoodlib ships only what Python lacks; `cryptography` is one import away (CHANGELOG 1.3.0)]"),
111
+ (r"\bnet\.py\b|hoodlib\.net|async(?:hronous)? HTTP client|connection pooling",
112
+ "[DROPPED — same; `aiohttp`/`httpx` are one import away (CHANGELOG 1.3.0)]"),
113
+ (r"libcst", "[NEVER — libcst was cut with the v1.0.0 stack; the transpiler is tokenize-based]"),
114
+ (r"^\s*<lexicon_taxonomy>",
115
+ "[NOTE — the keyword set is `src/hoodscript/constants.py` + GRAMMAR.md R1–R12; this table was rewritten to v1.1 and is informational only]"),
116
+ ]
117
+
118
+ FOOTER = """
119
+ ---
120
+ ALIGNMENT FOOTER (added by scripts/align_prompt.py — keep it)
121
+
122
+ Before writing a line: diff this scope against the tree. `git ls-files src tests
123
+ examples docs` and the CHANGELOG say what already exists; PROMPTS.md §3.5 is the
124
+ worked example of this procedure. Items marked [DECLINED]/[DROPPED]/[NEVER]
125
+ above are out of scope by owner decision — do not re-propose them inside the
126
+ batch. Work on a `gemini/<topic>` branch and open a PR (agents.md). No tags.
127
+ Gate: `ruff check && pytest`. Docs are tests. Complete files.
128
+ """
129
+
130
+ _WORD = r"(?<![\w'])({})(?![\w'])"
131
+
132
+
133
+ @dataclass
134
+ class Report:
135
+ lexicon: dict[str, int] = field(default_factory=dict)
136
+ phrases: dict[str, int] = field(default_factory=dict)
137
+ flagged: list[tuple[int, str]] = field(default_factory=list)
138
+
139
+ @property
140
+ def changed(self) -> bool:
141
+ return bool(self.lexicon or self.phrases or self.flagged)
142
+
143
+ def render(self) -> str:
144
+ out = ["# align_prompt report", ""]
145
+ if self.lexicon:
146
+ out.append("## Lexicon rewrites (v1.0.0 -> v1.1)")
147
+ for k, n in sorted(self.lexicon.items(), key=lambda kv: -kv[1]):
148
+ out.append(f"- `{k}` -> `{LEXICON[k]}` ×{n}")
149
+ out.append("")
150
+ if self.phrases:
151
+ out.append("## Phrasing / paths / commands")
152
+ for k, n in self.phrases.items():
153
+ out.append(f"- {k} ×{n}")
154
+ out.append("")
155
+ if self.flagged:
156
+ out.append("## Lines flagged by owner decision (kept, marked, out of scope)")
157
+ for ln, marker in self.flagged:
158
+ out.append(f"- line {ln}: {marker.split(' — ')[0].strip('[')}")
159
+ out.append("")
160
+ if not self.changed:
161
+ out.append("Nothing to change: the prompt is already aligned.")
162
+ out.append("Still to do by hand: diff the scope against the tree (PROMPTS.md §3.5).")
163
+ return "\n".join(out) + "\n"
164
+
165
+
166
+ def standing_brief() -> str:
167
+ """PROMPTS.md §0, the block that goes at the top of every prompt."""
168
+ text = (ROOT / "PROMPTS.md").read_text(encoding="utf-8")
169
+ m = re.search(r"## 0\. The standing brief.*?\n```\n(.*?)\n```", text, re.S)
170
+ return m.group(1) if m else ""
171
+
172
+
173
+ def _code_spans(text: str) -> list[tuple[int, int]]:
174
+ """Character ranges that count as code: fenced blocks, backtick spans, and
175
+ `word (python)` glosses like the lexicon tables these prompts carry."""
176
+ spans = []
177
+ for m in re.finditer(r"```.*?```", text, re.S):
178
+ spans.append(m.span())
179
+ for m in re.finditer(r"`[^`\n]+`", text):
180
+ spans.append(m.span())
181
+ for m in re.finditer(r"\b\w+ \((?:def|return|yield|lambda|class|self|if|elif|else|for|in|while|break|continue|pass|import|from|as|try|except|finally|raise|assert|with|async|await|del|global|nonlocal|match|case|True|False|None|_|and|or|not|is|print|len|range|type|str|int|float|list|dict|set|tuple|open|sum|enumerate|zip|all|any|filter|map|isinstance|super|input)\)", text):
182
+ spans.append(m.span())
183
+ return spans
184
+
185
+
186
+ def _in_code(spans: list[tuple[int, int]], pos: int) -> bool:
187
+ return any(a <= pos < b for a, b in spans)
188
+
189
+
190
+ def rewrite_lexicon(text: str, report: Report) -> str:
191
+ spans = _code_spans(text)
192
+ # longest first so `locked_lineup` wins over `lineup`, `or_say` over `say`
193
+ words = sorted(LEXICON, key=len, reverse=True)
194
+ pattern = re.compile(_WORD.format("|".join(re.escape(w) for w in words)))
195
+
196
+ def sub(m: re.Match) -> str:
197
+ w = m.group(1)
198
+ if w in AMBIGUOUS and not _in_code(spans, m.start()):
199
+ return w
200
+ report.lexicon[w] = report.lexicon.get(w, 0) + 1
201
+ return LEXICON[w]
202
+
203
+ return pattern.sub(sub, text)
204
+
205
+
206
+ def rewrite_phrases(text: str, report: Report) -> str:
207
+ for pat, repl, note in PHRASES:
208
+ text, n = re.subn(pat, repl, text)
209
+ if n:
210
+ report.phrases[note] = report.phrases.get(note, 0) + n
211
+ return text
212
+
213
+
214
+ def collapse_glosses(text: str, report: Report) -> str:
215
+ """After the lexicon pass a table row like `bring_in (import)` reads
216
+ `import (import)`; say what that means."""
217
+ text, n = re.subn(r"\b(\w+) \(\1\)", r"\1 (Python's word)", text)
218
+ if n:
219
+ report.phrases["X (X) gloss -> Python's word"] = n
220
+ return text
221
+
222
+
223
+ def mark_decisions(text: str, report: Report) -> str:
224
+ out = []
225
+ for i, line in enumerate(text.split("\n"), 1):
226
+ for pat, marker in DECISIONS:
227
+ if re.search(pat, line, re.I) and not line.lstrip().startswith("["):
228
+ indent = line[: len(line) - len(line.lstrip())]
229
+ line = f"{indent}{marker} {line.lstrip()}"
230
+ report.flagged.append((i, marker))
231
+ break
232
+ out.append(line)
233
+ return "\n".join(out)
234
+
235
+
236
+ HEAD_MARK = "<!-- PROMPTS.md §0 standing brief, prepended by scripts/align_prompt.py -->"
237
+ HEAD_END = "\n\n---\n\n"
238
+
239
+
240
+ def align(text: str, *, brief: bool = True) -> tuple[str, Report]:
241
+ """Idempotent: a brief this script prepended and a footer it appended are
242
+ recognised and left alone (the brief names the cut words on purpose)."""
243
+ report = Report()
244
+ head = ""
245
+ if text.startswith(HEAD_MARK) and HEAD_END in text:
246
+ head, text = text.split(HEAD_END, 1)
247
+ head += HEAD_END
248
+ if FOOTER.strip() in text:
249
+ text = text.replace(FOOTER.strip(), "").rstrip("\n") + "\n"
250
+
251
+ body = rewrite_phrases(text, report) # phrase rules first: `dip (break)` must go before the lexicon pass
252
+ body = rewrite_lexicon(body, report)
253
+ body = collapse_glosses(body, report)
254
+ body = mark_decisions(body, report)
255
+ if not head and brief and "You are working on HoodScript" not in text:
256
+ b = standing_brief()
257
+ head = f"{HEAD_MARK}\n{b}{HEAD_END}" if b else ""
258
+ return head + body.rstrip("\n") + "\n" + FOOTER, report
259
+
260
+
261
+ def main(argv: list[str] | None = None) -> int:
262
+ ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
263
+ ap.add_argument("prompt", help="prompt file, or - for stdin")
264
+ ap.add_argument("-o", "--out", help="write the aligned prompt here (default: stdout)")
265
+ ap.add_argument("--report", help="write the change report here (default: stderr)")
266
+ ap.add_argument("--no-brief", action="store_true", help="do not prepend the PROMPTS.md §0 brief")
267
+ ap.add_argument("--check", action="store_true", help="exit 1 if the prompt would change (no output)")
268
+ args = ap.parse_args(argv)
269
+
270
+ text = sys.stdin.read() if args.prompt == "-" else Path(args.prompt).read_text(encoding="utf-8")
271
+ aligned, report = align(text, brief=not args.no_brief)
272
+
273
+ if args.check:
274
+ sys.stderr.write(report.render())
275
+ return 1 if report.changed else 0
276
+ if args.out:
277
+ Path(args.out).write_text(aligned, encoding="utf-8")
278
+ else:
279
+ sys.stdout.write(aligned)
280
+ if args.report:
281
+ Path(args.report).write_text(report.render(), encoding="utf-8")
282
+ else:
283
+ sys.stderr.write(report.render())
284
+ return 0
285
+
286
+
287
+ if __name__ == "__main__":
288
+ sys.exit(main())