refdef 0.1.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 (167) hide show
  1. refdef-0.1.0/.gitattributes +1 -0
  2. refdef-0.1.0/.github/workflows/ci.yml +31 -0
  3. refdef-0.1.0/.github/workflows/docs.yml +35 -0
  4. refdef-0.1.0/.github/workflows/publish.yml +39 -0
  5. refdef-0.1.0/.gitignore +11 -0
  6. refdef-0.1.0/.python-version +1 -0
  7. refdef-0.1.0/AGENTS.md +12 -0
  8. refdef-0.1.0/CLAUDE.md +48 -0
  9. refdef-0.1.0/DESIGN.md +69 -0
  10. refdef-0.1.0/LICENSE +21 -0
  11. refdef-0.1.0/PKG-INFO +34 -0
  12. refdef-0.1.0/README.md +23 -0
  13. refdef-0.1.0/RULES.md +119 -0
  14. refdef-0.1.0/docs/CHANGELOG.md +17 -0
  15. refdef-0.1.0/docs/api-snapshot.txt +98 -0
  16. refdef-0.1.0/docs/guide/build.md +37 -0
  17. refdef-0.1.0/docs/guide/check.md +29 -0
  18. refdef-0.1.0/docs/guide/class-table.md +27 -0
  19. refdef-0.1.0/docs/guide/index.md +14 -0
  20. refdef-0.1.0/docs/guide/number.md +18 -0
  21. refdef-0.1.0/docs/guide/plc.md +49 -0
  22. refdef-0.1.0/docs/guide/print.md +16 -0
  23. refdef-0.1.0/docs/guide/read.md +48 -0
  24. refdef-0.1.0/docs/guide/sort.md +16 -0
  25. refdef-0.1.0/docs/guide/terminals.md +27 -0
  26. refdef-0.1.0/docs/guide/tree.md +75 -0
  27. refdef-0.1.0/docs/index.md +6 -0
  28. refdef-0.1.0/docs/llms.txt +63 -0
  29. refdef-0.1.0/justfile +61 -0
  30. refdef-0.1.0/mkdocs.yml +35 -0
  31. refdef-0.1.0/pyproject.toml +166 -0
  32. refdef-0.1.0/scripts/build_site.py +142 -0
  33. refdef-0.1.0/scripts/design_guard.py +91 -0
  34. refdef-0.1.0/scripts/family_compare.py +55 -0
  35. refdef-0.1.0/scripts/family_files.txt +18 -0
  36. refdef-0.1.0/scripts/gen_render_corpus.py +41 -0
  37. refdef-0.1.0/scripts/gen_table.py +108 -0
  38. refdef-0.1.0/scripts/release_guard.py +93 -0
  39. refdef-0.1.0/src/refdef/__init__.py +33 -0
  40. refdef-0.1.0/src/refdef/__main__.py +18 -0
  41. refdef-0.1.0/src/refdef/_api.py +42 -0
  42. refdef-0.1.0/src/refdef/_check.py +55 -0
  43. refdef-0.1.0/src/refdef/_heads.py +70 -0
  44. refdef-0.1.0/src/refdef/_parse.py +177 -0
  45. refdef-0.1.0/src/refdef/_render.py +64 -0
  46. refdef-0.1.0/src/refdef/designation.py +93 -0
  47. refdef-0.1.0/src/refdef/issues.py +47 -0
  48. refdef-0.1.0/src/refdef/level.py +111 -0
  49. refdef-0.1.0/src/refdef/numbering.py +42 -0
  50. refdef-0.1.0/src/refdef/order.py +44 -0
  51. refdef-0.1.0/src/refdef/plc.py +121 -0
  52. refdef-0.1.0/src/refdef/py.typed +0 -0
  53. refdef-0.1.0/src/refdef/scheme.py +77 -0
  54. refdef-0.1.0/src/refdef/tables/__init__.py +1 -0
  55. refdef-0.1.0/src/refdef/tables/standard.py +192 -0
  56. refdef-0.1.0/src/refdef/terminal.py +76 -0
  57. refdef-0.1.0/tests/conftest.py +14 -0
  58. refdef-0.1.0/tests/corpus/addresses.txt +62 -0
  59. refdef-0.1.0/tests/corpus/render.txt +110 -0
  60. refdef-0.1.0/tests/corpus/strings.txt +110 -0
  61. refdef-0.1.0/tests/family.py +461 -0
  62. refdef-0.1.0/tests/fixtures/issues/check_aspect_off.py +11 -0
  63. refdef-0.1.0/tests/fixtures/issues/check_duplicate.py +11 -0
  64. refdef-0.1.0/tests/fixtures/issues/check_letter_count.py +11 -0
  65. refdef-0.1.0/tests/fixtures/issues/check_never_letter.py +11 -0
  66. refdef-0.1.0/tests/fixtures/issues/check_unknown_class.py +11 -0
  67. refdef-0.1.0/tests/fixtures/issues/parse_bad_label.py +10 -0
  68. refdef-0.1.0/tests/fixtures/issues/parse_dot_level.py +10 -0
  69. refdef-0.1.0/tests/fixtures/issues/parse_empty.py +10 -0
  70. refdef-0.1.0/tests/fixtures/issues/parse_empty_level.py +10 -0
  71. refdef-0.1.0/tests/fixtures/issues/parse_empty_name.py +10 -0
  72. refdef-0.1.0/tests/fixtures/issues/parse_empty_pin.py +10 -0
  73. refdef-0.1.0/tests/fixtures/issues/parse_empty_top.py +10 -0
  74. refdef-0.1.0/tests/fixtures/issues/parse_free_label.py +10 -0
  75. refdef-0.1.0/tests/fixtures/issues/parse_leading_zero.py +10 -0
  76. refdef-0.1.0/tests/fixtures/issues/parse_lower_case.py +10 -0
  77. refdef-0.1.0/tests/fixtures/issues/parse_no_sign.py +10 -0
  78. refdef-0.1.0/tests/fixtures/issues/parse_number_too_large.py +10 -0
  79. refdef-0.1.0/tests/fixtures/issues/parse_unsupported_suffix.py +10 -0
  80. refdef-0.1.0/tests/fixtures/issues/parse_whitespace.py +10 -0
  81. refdef-0.1.0/tests/fixtures/issues/parse_zero_number.py +10 -0
  82. refdef-0.1.0/tests/fixtures/malformed/designation_levels_list.py +8 -0
  83. refdef-0.1.0/tests/fixtures/malformed/designation_levels_not_level.py +8 -0
  84. refdef-0.1.0/tests/fixtures/malformed/designation_top_ampersand.py +8 -0
  85. refdef-0.1.0/tests/fixtures/malformed/designation_top_colon.py +8 -0
  86. refdef-0.1.0/tests/fixtures/malformed/designation_top_gt.py +8 -0
  87. refdef-0.1.0/tests/fixtures/malformed/designation_top_not_str.py +8 -0
  88. refdef-0.1.0/tests/fixtures/malformed/designation_top_outer_space.py +8 -0
  89. refdef-0.1.0/tests/fixtures/malformed/designation_top_semicolon.py +8 -0
  90. refdef-0.1.0/tests/fixtures/malformed/level_aspect_not_aspect.py +8 -0
  91. refdef-0.1.0/tests/fixtures/malformed/level_code_with_digit.py +8 -0
  92. refdef-0.1.0/tests/fixtures/malformed/level_coded_and_label.py +8 -0
  93. refdef-0.1.0/tests/fixtures/malformed/level_empty.py +8 -0
  94. refdef-0.1.0/tests/fixtures/malformed/level_label_angle.py +8 -0
  95. refdef-0.1.0/tests/fixtures/malformed/level_label_coded_shape_k1.py +8 -0
  96. refdef-0.1.0/tests/fixtures/malformed/level_label_coded_shape_lower.py +8 -0
  97. refdef-0.1.0/tests/fixtures/malformed/level_label_colon.py +8 -0
  98. refdef-0.1.0/tests/fixtures/malformed/level_label_digits_only.py +8 -0
  99. refdef-0.1.0/tests/fixtures/malformed/level_label_dot.py +8 -0
  100. refdef-0.1.0/tests/fixtures/malformed/level_label_outer_space.py +8 -0
  101. refdef-0.1.0/tests/fixtures/malformed/level_label_sign_char.py +8 -0
  102. refdef-0.1.0/tests/fixtures/malformed/level_letters_without_number.py +8 -0
  103. refdef-0.1.0/tests/fixtures/malformed/level_lower_case_code.py +8 -0
  104. refdef-0.1.0/tests/fixtures/malformed/level_non_ascii_code.py +8 -0
  105. refdef-0.1.0/tests/fixtures/malformed/level_number_and_label.py +8 -0
  106. refdef-0.1.0/tests/fixtures/malformed/level_number_bool.py +8 -0
  107. refdef-0.1.0/tests/fixtures/malformed/level_number_float.py +8 -0
  108. refdef-0.1.0/tests/fixtures/malformed/level_number_too_large.py +8 -0
  109. refdef-0.1.0/tests/fixtures/malformed/level_number_zero.py +8 -0
  110. refdef-0.1.0/tests/fixtures/malformed/signal_empty_designation.py +8 -0
  111. refdef-0.1.0/tests/fixtures/malformed/signal_empty_name.py +8 -0
  112. refdef-0.1.0/tests/fixtures/malformed/signal_name_not_str.py +8 -0
  113. refdef-0.1.0/tests/fixtures/malformed/signal_name_outer_space.py +8 -0
  114. refdef-0.1.0/tests/fixtures/malformed/signal_of_not_designation.py +8 -0
  115. refdef-0.1.0/tests/fixtures/malformed/terminal_empty_pin.py +8 -0
  116. refdef-0.1.0/tests/fixtures/malformed/terminal_no_pins.py +8 -0
  117. refdef-0.1.0/tests/fixtures/malformed/terminal_of_not_designation.py +8 -0
  118. refdef-0.1.0/tests/fixtures/malformed/terminal_pin_colon.py +8 -0
  119. refdef-0.1.0/tests/fixtures/malformed/terminal_pin_not_str.py +8 -0
  120. refdef-0.1.0/tests/fixtures/malformed/terminal_pin_outer_space.py +8 -0
  121. refdef-0.1.0/tests/fixtures/malformed/terminal_pins_list.py +8 -0
  122. refdef-0.1.0/tests/fixtures/tables/classes.csv +7 -0
  123. refdef-0.1.0/tests/fuzz.py +95 -0
  124. refdef-0.1.0/tests/guide/test_guide.py +85 -0
  125. refdef-0.1.0/tests/issue_hook.py +30 -0
  126. refdef-0.1.0/tests/test_agents.py +112 -0
  127. refdef-0.1.0/tests/test_api.py +76 -0
  128. refdef-0.1.0/tests/test_boundary.py +15 -0
  129. refdef-0.1.0/tests/test_boundary_r.py +119 -0
  130. refdef-0.1.0/tests/test_check.py +227 -0
  131. refdef-0.1.0/tests/test_deps.py +7 -0
  132. refdef-0.1.0/tests/test_design_guard.py +121 -0
  133. refdef-0.1.0/tests/test_designation.py +56 -0
  134. refdef-0.1.0/tests/test_docs.py +104 -0
  135. refdef-0.1.0/tests/test_family_compare.py +58 -0
  136. refdef-0.1.0/tests/test_family_rules.py +36 -0
  137. refdef-0.1.0/tests/test_gen_table.py +131 -0
  138. refdef-0.1.0/tests/test_lean.py +98 -0
  139. refdef-0.1.0/tests/test_level.py +64 -0
  140. refdef-0.1.0/tests/test_licence.py +27 -0
  141. refdef-0.1.0/tests/test_llms.py +47 -0
  142. refdef-0.1.0/tests/test_malformed.py +7 -0
  143. refdef-0.1.0/tests/test_numbering.py +68 -0
  144. refdef-0.1.0/tests/test_order.py +126 -0
  145. refdef-0.1.0/tests/test_parse.py +156 -0
  146. refdef-0.1.0/tests/test_plc.py +138 -0
  147. refdef-0.1.0/tests/test_pure.py +7 -0
  148. refdef-0.1.0/tests/test_pyproject_pins.py +41 -0
  149. refdef-0.1.0/tests/test_release_guard.py +114 -0
  150. refdef-0.1.0/tests/test_render.py +159 -0
  151. refdef-0.1.0/tests/test_roundtrip.py +82 -0
  152. refdef-0.1.0/tests/test_site.py +100 -0
  153. refdef-0.1.0/tests/test_sketch_cases.py +28 -0
  154. refdef-0.1.0/tests/test_sketch_rd-3.py +44 -0
  155. refdef-0.1.0/tests/test_sketch_rd-4.py +36 -0
  156. refdef-0.1.0/tests/test_sketch_rd-5.py +16 -0
  157. refdef-0.1.0/tests/test_sketch_rd-6b.py +51 -0
  158. refdef-0.1.0/tests/test_sketch_rd2.py +119 -0
  159. refdef-0.1.0/tests/test_standalone.py +24 -0
  160. refdef-0.1.0/tests/test_surface.py +42 -0
  161. refdef-0.1.0/tests/test_table.py +109 -0
  162. refdef-0.1.0/tests/test_table_version.py +38 -0
  163. refdef-0.1.0/tests/test_tree.py +109 -0
  164. refdef-0.1.0/tests/test_tree_properties.py +36 -0
  165. refdef-0.1.0/tests/test_vocab.py +7 -0
  166. refdef-0.1.0/tests/test_wording.py +131 -0
  167. refdef-0.1.0/uv.lock +479 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,31 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ ci:
13
+ runs-on: ubuntu-24.04
14
+ steps:
15
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
16
+ with:
17
+ fetch-depth: 0
18
+ fetch-tags: true
19
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
20
+ with:
21
+ version: "0.13.5"
22
+ - run: uv sync
23
+ - run: uv python install 3.14
24
+ - run: uvx --from rust-just==1.59.0 just ci
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
+ if: github.event.repository.private == false
27
+ with:
28
+ repository: OleJBondahl/refdef
29
+ path: sibling
30
+ - run: uv run python scripts/family_compare.py sibling
31
+ if: github.event.repository.private == false
@@ -0,0 +1,35 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*.*.*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-24.04
13
+ steps:
14
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
15
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
16
+ with:
17
+ version: "0.13.5"
18
+ - run: uv run python scripts/build_site.py
19
+ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
20
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
21
+ with:
22
+ path: .site/
23
+
24
+ deploy:
25
+ needs: build
26
+ runs-on: ubuntu-24.04
27
+ environment:
28
+ name: github-pages
29
+ url: ${{ steps.deployment.outputs.page_url }}
30
+ permissions:
31
+ pages: write
32
+ id-token: write
33
+ steps:
34
+ - id: deployment
35
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
@@ -0,0 +1,39 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*.*.*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-24.04
13
+ steps:
14
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
15
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
16
+ with:
17
+ version: "0.13.5"
18
+ - name: The tag matches the package version
19
+ run: test "v$(uv version --short)" = "$GITHUB_REF_NAME"
20
+ - run: uv build
21
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ pypi:
27
+ needs: build
28
+ runs-on: ubuntu-24.04
29
+ environment: pypi
30
+ permissions:
31
+ id-token: write # trusted publishing: no token secret
32
+ steps:
33
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
38
+ with:
39
+ attestations: true
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ .ruff_cache/
4
+ .pytest_cache/
5
+ .coverage
6
+ .superpowers/
7
+ claude-tools/
8
+ dist/
9
+ .site-src/
10
+ .site/
11
+ .refdef/
@@ -0,0 +1 @@
1
+ 3.15.0
refdef-0.1.0/AGENTS.md ADDED
@@ -0,0 +1,12 @@
1
+ # Agent guide
2
+
3
+ - Docs site: https://olejbondahl.github.io/refdef/
4
+ - Surface: `python -m refdef api`, snapshot in `docs/api-snapshot.txt`
5
+ - Guide: `docs/guide/`
6
+ - Design: `DESIGN.md`
7
+ - Rules: `RULES.md`
8
+ - A change follows RULES.md.
9
+ - Design-review paths: `DESIGN.md`, `RULES.md`, `docs/api-snapshot.txt`, `src/refdef/tables/`, `tests/corpus/`.
10
+ A commit touching them needs a `Design-review: <ID>` trailer.
11
+ - A need the surface does not meet is a reviewed design change, never a workaround.
12
+ - Use `uv` only, with exact pins.
refdef-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,48 @@
1
+ # refdef: agent guide
2
+
3
+ refdef is a library of reference designations: strict, typed, immutable Python values for the tags
4
+ that name things in a plant, with no runtime dependency. A sibling library follows the same family rules.
5
+ Docs: <https://olejbondahl.github.io/refdef/>. Surface: `python -m refdef api`.
6
+
7
+ ## Read first
8
+
9
+ Read `DESIGN.md`, then `RULES.md`. They bind: a change follows RULES.md, and code that disagrees
10
+ with them is wrong. Family rules F1-F17 are shared with the sibling library; R1-R12 are ours.
11
+
12
+ ## Gate
13
+
14
+ `just ci` runs locally (no hosted gate beyond one CI job): ruff, ty, vulture, pytest with a 95 %
15
+ coverage floor, the tests on Python 3.14, the two guards, and the strict site build. Its last
16
+ stdout line is `site built: .site`. Each shared test guards one family rule:
17
+
18
+ - `test_deps` (F1): no runtime dependency, stdlib and the package only.
19
+ - `test_pure` (F2), `test_check` and `test_malformed` (F3): frozen values, no I/O, issues as values.
20
+ - `test_vocab` (F4): a closed vocabulary is never a bare `str`.
21
+ - `test_boundary` (F5, F6, F13): no boundary names, no application default, no sibling import.
22
+ - `test_wording` (F7 and the em-dash rule): no standard cited by number, clause or year.
23
+ - `test_surface` (F8, F15): the snapshot equals `python -m refdef api`; no operators on surface classes.
24
+ - `test_docs` (F9) and `test_site`: every name has a docstring and a runnable guide block.
25
+ - `test_lean` (F10): no nested def, 300 code lines, short docstrings, every noqa with a reason.
26
+ - `test_release_guard` (F11) and `test_design_guard` (F12): the guard scripts can fail.
27
+ - `test_licence` (F14), `test_agents` (F16), `test_standalone` (F17), `test_family_rules`, `test_pyproject_pins`.
28
+
29
+ ## Process
30
+
31
+ - F12: a commit that touches a review path (`DESIGN.md`, `RULES.md`, `docs/api-snapshot.txt`,
32
+ `src/refdef/tables/`, `tests/corpus/`) carries a trailer `Design-review: <ID>`. The maintainer approves the
33
+ contract; the design reviewer approves surface and table changes.
34
+ - F13: a missing need is a reviewed extension of the surface, never a workaround or a private import.
35
+ - F17: name no application anywhere, in a tracked or an untracked file, or in a commit message.
36
+ - One source of truth for the surface: `docs/api-snapshot.txt`, regenerated with `just api-write`,
37
+ never edited by hand. A new surface name adds its door to `[tool.family].doors` and `_api.DOORS`
38
+ together, a docstring, a runnable guide block, a CHANGELOG line and its `docs/llms.txt` lines.
39
+ - The identical test set listed in `scripts/family_files.txt` is copied byte for byte between the
40
+ sibling repositories; `scripts/family_compare.py` checks it. Edit it in both or in neither.
41
+
42
+ ## Style
43
+
44
+ - `uv` only; no bare `python` or `pip`. Dependencies are exact pins (`name==X.Y.Z`).
45
+ - No em-dashes in any text file.
46
+ - Lean limits (F10): 15 statements, 12 branches, complexity 8, no nested `def`, 300 code lines per
47
+ module, docstrings of one summary line plus at most two of why, `# noqa: CODE - reason`.
48
+ - Stage named paths with git; never `git add -A`.
refdef-0.1.0/DESIGN.md ADDED
@@ -0,0 +1,69 @@
1
+ # refdef: design
2
+
3
+ refdef is a small Python library for reference designations: the tags that name the things in a plant, a machine or a cabinet (`=M1+ER-K2`, `-X1:3`, `-KE1:DI03`). It is compatible with IEC practice. This file is the current truth of the design; `RULES.md` holds the rules every change must keep, and the two change only through the process in the last section.
4
+
5
+ ## Purpose
6
+
7
+ A designation names an occurrence of a thing by its place in one or more trees: what it does (function, `=`), what it is (product, `-`) and where it sits (location, `+`). Every electrical engineer who scripts, generates or checks drawings, lists and PLC configurations in Python meets these tags, and every tool passes them around as strings and then re-splits, re-sorts and re-checks them in slightly different ways. refdef gives the tag one structured value, one strict way to print it, one lenient way to read it, and the operations a tool needs on top: relative paths, common prefixes, free-number allocation, natural sort, a class-code table and a check that reports issues. It has no runtime dependency, does no I/O and holds no global state.
8
+
9
+ ## The family
10
+
11
+ Three libraries share the work, and each fact has one home:
12
+
13
+ - `symdef` draws: symbols, frames, markers, their geometry and styles.
14
+ - `elecdef` says what a thing is: kinds of devices, their properties, terminal markings and internal links.
15
+ - `refdef` says what a thing is called: designations, their structure, printing, reading, numbering and order.
16
+
17
+ symdef draws a thing, elecdef says what it is, refdef says what it is called; an application connects and lays out. The three are basic building blocks for electrical work in Python, written for every electrical engineer who works there, and each stands alone. A fact never has two homes; what a terminal number means is an `elecdef` fact, how it prints after the colon is a `refdef` fact. None of the three imports another, and none names an application that uses it.
18
+
19
+ ## The core model
20
+
21
+ **Aspect.** An enum whose value is the sign: `FUNCTION "="`, `PRODUCT "-"`, `LOCATION "+"`, `TYPE "%"`, `OTHER "#"`, `ASSIGNMENT "=="`, `SITE "++"`. The first three are on in the shipped scheme; the other four parse always and are off until a project turns them on.
22
+
23
+ **Level.** One step in a tree: an aspect with class letters and a number (`-K2`), a number alone (`+3`), or a verbatim label (`+ER`, `-PE_BAR`). A string has exactly one value: letters need a number, so letters alone are a label; a number is an `int` from 1 to 999,999,999 with no padding and no meaning; a label is stripped, non-empty, holds no sign or separator character (`= - + % # : ; & < > .`) and is never shaped like a code (ASCII letters then digits worth 1 to 999,999,999, so `K1` and `7` are refused as labels while `K0` is one), so what it prints reads back to itself. Code letters are upper case. Whether the letters form a known class is not a construction rule, so a designation from any era can be built.
24
+
25
+ **Designation.** A frozen tuple of levels and an optional top node (printed `<...>`). The empty designation is the root. Level order is kept as given; nothing re-sorts aspects. The builder is the constructor taking levels and `child(aspect, code, number, label)`, and nothing else: two designations are joined through their levels, `Designation(tag.levels + d.levels)`, and no operator builds a value. It is viewed with `only(*aspects)`, `parent(aspect)` and `last`. A top node, when given, is stripped, non-empty and holds none of `> : ; &`.
26
+
27
+ **Scheme.** A frozen value passed to every check: the aspects that are on, a `LetterRule` (letter count, letters that never appear) and a `ClassTable`. `STANDARD` is the shipped scheme. `scheme.extend(*entries, aspects=...)` returns a new scheme with project classes overlaid and further aspects on; a project that uses `J` for its board connectors adds `ClassEntry("J", "board connector")` this way.
28
+
29
+ **Parse and render.** `str(d)` prints strictly: every level with its sign, no padding, no separators. `render(d, pad=, dotted=, bare=)` adds the dialects a caller may want: `pad={"W": 4}` prints `-W0001`; `dotted=(Aspect.PRODUCT,)` prints a number-only sub-level as `-W25.1`; `bare=True` drops the first sign. `parse(text, bare=)` reads leniently and returns `Parsed(value, issues)`: lower-case code letters are raised, leading zeros dropped, a dot read as a sub-level, text before the first sign taken as the aspect the caller names in `bare` (a required keyword: which aspect a signless tag means is the caller's convention), anything that is not letters and a number kept verbatim as a label, each with a named issue. Nothing is coerced to a default letter or number, and nothing raises for content. `parse` knows shape only; `check(designations, scheme)` holds every scheme rule: an aspect that is off, a letter count, a letter that never appears, an unknown class, a duplicate. An `Issue` is a code from a closed enum of 21 (16 from `parse` and `parse_address`, 5 from `check`, listed in R6), a subject and a message; the caller decides what is fatal. A digit run of zero or over nine digits reads as a label with an issue.
30
+
31
+ **Tree arithmetic.** Every operation works per aspect. `d.within(root)` holds when, for each aspect both share, the root's levels are a head of `d`'s; an aspect missing from either side constrains nothing; a root with a top node matches only that top. `d.relative_to(root)` drops the matched heads and keeps the rest in order, so `=U1=P1+ER+C1-K1` relative to the unit root `=U1+ER` is `=P1+C1-K1`, and a cable `=U1-W1` with no location prints `-W1` inside that unit. `Designation.common(*ds)` is the longest shared head per aspect, ordered as the first argument; every argument is within it. Units as atoms is this: a unit's own document prints each tag relative to the unit's root.
32
+
33
+ **Numbering.** `allocate(aspect, code, taken, reserved, first=1)` returns the lowest free number: the first `n` from `first` whose level is in neither collection. Gaps refill unless the caller lists them as reserved. `number_group(aspect, codes, taken, reserved)` numbers a sequence of codes in one go and feeds each result back into `taken`. refdef owns no store of retired numbers; what forms a sibling group and what stays reserved are the caller's facts.
34
+
35
+ **Sorting.** `natural_key(text, case_sensitive=True)` orders digit runs by value and text runs as text, numbers first, with the raw text as the last tie-break (`X01` before `X1`). `sort_key(d, aspects=, case_sensitive=True)` is one pair per level, aspect rank then the natural key of the level text; `aspects` is a required keyword, since the order of aspects is the caller's convention, and an unlisted aspect ranks after the listed ones.
36
+
37
+ **Terminals and signals.** `Terminal(of, pins)` prints `-K1:13`, `-X1:L1:1`, `-A2:J7:3`, `-X1:OUT1-`; a pin is stripped, non-empty text without `:` (so `+`, `-`, `;`, `<` and inner spaces are allowed, and every pin reads back), kept verbatim, and its meaning is an `elecdef` fact. `Signal(of, name)` prints `;NAME` or `-K1;NAME`; the name is stripped, non-empty, free text, and `of` is `None` or a designation with a level or a top (an empty designation is refused). Both have `relative_to`. refdef never reads inside a pin or a name. Wires and cables need no type of their own: `-W25`, `=FIRE-W25`, a core as a sub-level.
38
+
39
+ **PLC.** `refdef.plc` holds the direct-address form common to PLC programming languages: `Address(memory, size, path)` for `%IX0.0`, `%QW4`, `%M10.3` and the incomplete `%I*`, with `as_bit()` normalising `%Q7.5` to `%QX7.5`, and `parse_address(text)`. A channel tag such as `-KE1:DI03` is a `Terminal`; `ChannelTag(terminal, address)` pairs it with an optional address (named so it does not collide with `elecdef`'s channel). Vendor address dialects are separate modules, added when an application needs one.
40
+
41
+ ## The class table
42
+
43
+ `refdef.tables.standard` is a generated Python module holding `ClassTable(name="standard", version="1", depth=2, entries=(...))`: the single letters that name the main classes and the two-letter codes beneath them, as electrical practice uses them, each with a short title in our own words. A title names the typical device of the class (owner 2026-10-22); a device's own facts are `elecdef`'s knowledge. `depth=2` means `check` tests a code by its first two letters, so a three-letter code passes when its two-letter parent exists. The code index is built once per table.
44
+
45
+ The table has its own version line, independent of the library version. A moved, added or removed code bumps `version`, ships as a new entry tuple under the same `STANDARD` name, and is named in the changelog. A project pins the library version and gets the table that version carries; a second table, for another code set or a translation keyed by code, is a further module and a further scheme constant.
46
+
47
+ ## The public surface
48
+
49
+ `refdef.__all__`: `Aspect`, `Level`, `Designation`, `Terminal`, `Signal`, `Scheme`, `ClassEntry`, `STANDARD`, `parse`, `Parsed`, `render`, `check`, `Issue`, `IssueCode`, `allocate`, `number_group`, `natural_key`, `sort_key`. `refdef.plc.__all__`: `Memory`, `Size`, `Address`, `ChannelTag`, `parse_address`. `ClassTable` and `LetterRule` are reached through `Scheme`. An application imports these names and nothing below them. Every surface name has a docstring of one summary line and at most two lines of why, an entry in the generated API reference and a runnable example in the guide (family rule F9). No surface class overloads an operator; one spelling builds, one reads, one prints (F15). `python -m refdef api` prints the two `__all__` tuples with every surface signature; `docs/api-snapshot.txt` holds that output byte for byte, and a change to it is a reviewed design change (family rules F8 and F12).
50
+
51
+ ## Docs and agents
52
+
53
+ The docs site is GitHub Pages, built with Zensical from `docs/`, with the API reference generated from the docstrings. It ships with the first public release and has four parts: a guide (build, print, read, check, number, sort) in which every surface name has a runnable example, the API reference, `DESIGN.md` and `RULES.md` rendered as they are, and a changelog that names every moved class code.
54
+
55
+ Two files are written for agents (family rule F16). `llms.txt` at the docs site root lists, in plain text, what refdef is, the URL of each docs page, the surface names with one line each, and the rules in one line each. `AGENTS.md` in the repository root names the docs site, the surface command (`python -m refdef api`) and snapshot (`docs/api-snapshot.txt`), the guide, `DESIGN.md` and `RULES.md`; it says which files change only with a design review (`DESIGN.md`, `RULES.md`, `docs/api-snapshot.txt`, `src/refdef/tables/`, `tests/corpus/`) and that a need the surface does not meet is a reviewed design change and never a workaround. The docs build is a gate: a surface name without an example, or an example that does not run, fails it (F9).
56
+
57
+ ## Out of scope
58
+
59
+ refdef does not hold: the meaning of a pin or terminal number, a device's recommended class code, a drawing of anything, a store of retired numbers or a numbering file, document designations and their kind codes, signal-name rules, wire-number rules, vendor PLC dialects, the three-letter class level, other code sets, translations. Each is either another library's fact or a later, reviewed addition.
60
+
61
+ ## How it changes
62
+
63
+ `DESIGN.md` and `RULES.md` are the contract. A pull request that edits either, changes `docs/api-snapshot.txt`, changes `src/refdef/tables/` or moves a golden under `tests/corpus/` carries a `Design-review:` trailer naming an approved review; `scripts/design_guard.py` fails the merge otherwise (family rule F12, refdef rule R3). An application whose need the surface does not meet opens a design review rather than a workaround on either side; a change that breaks a rule in `RULES.md` is not merged. Versions follow family rule F11; a changed printed form of an unchanged value moves the render golden (R3) and carries its changelog line, and a table change bumps the table's own version (R10).
64
+
65
+ Who approves is family rule F12: the maintainer approves the contract, and the design reviewer approves surface and table changes.
66
+
67
+ ## Licence
68
+
69
+ refdef is MIT licensed, code and shipped class table alike (family rule F14). Open sources used are credited in one block in `README.md` (F7); no share-alike or non-commercial source is used.
refdef-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ole Johan Bondahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
refdef-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.5
2
+ Name: refdef
3
+ Version: 0.1.0
4
+ Summary: Reference designations as strict, typed, immutable Python values: parse, check, print, number and order
5
+ Project-URL: Repository, https://github.com/OleJBondahl/refdef
6
+ Author: Ole Johan Bondahl
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ # refdef
13
+
14
+ Reference designations (the tags that name things in a plant, a machine or a cabinet) as strict, typed, immutable Python values.
15
+ The library has no runtime dependencies and does no file, network, clock or random access.
16
+ Every value is a frozen dataclass or an enum.
17
+
18
+ Status: pre-release, version 0.1.0. The package is an empty scaffold.
19
+
20
+ ## Install
21
+
22
+ ```text
23
+ uv add refdef
24
+ ```
25
+
26
+ Compatible with IEC conventions.
27
+
28
+ Documentation: <https://olejbondahl.github.io/refdef/>
29
+
30
+ Licence: MIT.
31
+
32
+ <!-- credit:start -->
33
+ Credits: none yet.
34
+ <!-- credit:end -->
refdef-0.1.0/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # refdef
2
+
3
+ Reference designations (the tags that name things in a plant, a machine or a cabinet) as strict, typed, immutable Python values.
4
+ The library has no runtime dependencies and does no file, network, clock or random access.
5
+ Every value is a frozen dataclass or an enum.
6
+
7
+ Status: pre-release, version 0.1.0. The package is an empty scaffold.
8
+
9
+ ## Install
10
+
11
+ ```text
12
+ uv add refdef
13
+ ```
14
+
15
+ Compatible with IEC conventions.
16
+
17
+ Documentation: <https://olejbondahl.github.io/refdef/>
18
+
19
+ Licence: MIT.
20
+
21
+ <!-- credit:start -->
22
+ Credits: none yet.
23
+ <!-- credit:end -->
refdef-0.1.0/RULES.md ADDED
@@ -0,0 +1,119 @@
1
+ # refdef: rules
2
+
3
+ These rules bind every implementation and every later change of refdef. The family rules F1-F17 are shared with elecdef and carried verbatim; the refdef rules R1-R12 add what is refdef's own and repeat no family rule. Each rule is one sentence and its GATE: the mechanical check that fails a breach, or "review" only where no mechanism can work, with the reason. A change to this file, to `DESIGN.md`, to `docs/api-snapshot.txt`, to `src/refdef/tables/` or to `tests/corpus/` needs a `Design-review:` trailer (F12, R3).
4
+
5
+ ## refdef rules
6
+
7
+ - **R1. One value per string.** For every designation, terminal or signal value `v`, `parse(str(v), bare=a).value == v` for any aspect `a`, and for every string `s` that parses without an issue, `str(parse(s, bare=a).value) == s`. GATE: `tests/test_roundtrip.py`, a property test whose stdlib generator draws levels, labels, tops, pins and signal names from the full alphabets R2 allows (pins and names including `; & < > + - .` and inner spaces), plus `tests/corpus/strings.txt` of strings seen in real projects.
8
+ - **R2. The shapes that construct.** A `Level` has upper-case letters with a number, a number alone, or a stripped non-empty label without `= - + % # : ; & < > .`; a number is an `int` from 1 to 999_999_999; a label is never shaped like a code (ASCII letters, then digits worth 1 to 999_999_999, such as `K1`, `k01` or `7`), because `-K1` must read back as a coded level (`K0` and `K1234567890` are labels); a `Designation.top` is empty or stripped, non-empty and without `> : ; &`; a `Terminal` has at least one pin, each stripped, non-empty and without `:`; a `Signal.name` is stripped and non-empty, and a `Signal.of` is `None` or a designation with at least one level or a top (an empty `Signal.of` is refused, because `;N` must read back to the same value); any other shape is malformed and raises `TypeError` at construction (F3). GATE: `tests/test_malformed.py` holds one fixture per clause (the labels `K1`, `k01` and `7`, the numbers 0 and 1_000_000_000, and an empty `Signal.of` among them) and asserts `TypeError`; `tests/test_level.py` asserts every value that constructs prints and parses back to itself (R1).
9
+ - **R3. Strict render.** `str()` prints every level with its sign and never pads, dots or drops a sign; every dialect (`pad`, `dotted`, `bare`) is an explicit `render` option, and a change to any printed form moves the golden. GATE: `tests/test_render.py` asserts `str(v) == render(v)` over the round-trip corpus, compares `render` of `tests/corpus/strings.txt` under each option to `tests/corpus/render.txt` line by line, and asserts each option changes only the documented characters; `scripts/design_guard.py` watches `tests/corpus/` (F12).
10
+ - **R4. Lenient parse with issues.** `parse` and `parse_address` return a value and an issue tuple for any string, every lenient repair (case, leading zero, dot, missing sign, free label, unsupported suffix) is matched by an issue with its code, and no default letter or number is ever substituted; a digit run that is zero or longer than nine digits (the bound of R2) reads as a label with an issue. GATE: `tests/test_parse.py` feeds a fuzz corpus (random text, every sign alone, empty, whitespace) and asserts no exception, and asserts one issue per repair against a table of input, value and codes.
11
+ - **R5. Shape in parse, scheme in check.** `parse` takes no scheme, every rule that needs one (aspect on, letter count, never-letters, unknown class by `code[:depth]`, duplicate) lives in `check`, and no function reads a scheme or table other than through its argument. GATE: `tests/test_parse.py` asserts `parse`'s signature has no `scheme` parameter; `tests/test_check.py` has one case per check-family `IssueCode`, asserts `parse` never emits them, and an AST test in it fails the name `STANDARD` or an import from `refdef.tables` anywhere under `src/refdef` outside `scheme.py` and `__init__.py`.
12
+ - **R6. An issue is a code, a subject and a message.** `Issue` has exactly these three fields, no severity, and `IssueCode` is a closed enum of exactly 21 codes that every emitted issue uses: the parse family (16) `EMPTY`, `NO_SIGN`, `EMPTY_LEVEL`, `EMPTY_TOP`, `WHITESPACE`, `LOWER_CASE`, `LEADING_ZERO`, `ZERO_NUMBER`, `NUMBER_TOO_LARGE`, `FREE_LABEL`, `BAD_LABEL`, `DOT_LEVEL`, `EMPTY_PIN`, `EMPTY_NAME`, `UNSUPPORTED_SUFFIX`, `NOT_ADDRESS` (the last only from `parse_address`), and the check family (5) `ASPECT_OFF`, `LETTER_COUNT`, `NEVER_LETTER`, `UNKNOWN_CLASS`, `DUPLICATE`; the values are the lower-case names. GATE: `tests/test_check.py` asserts the field names of `Issue`, that the `IssueCode` names equal these 21, and that every issue any test produces has a code in `IssueCode`.
13
+ - **R7. Per-aspect tree arithmetic.** `within`, `relative_to` (on `Designation`, `Terminal` and `Signal`) and `common` follow the rule in `DESIGN.md` exactly, and every argument is within `common(*ds)`. GATE: `tests/test_tree.py` holds the unit-root, cable, mixed-order and top-node cases for all three types, and a property test asserting `a.within(common(a, b))`, `len(b.relative_to(common(a, b)).levels) == len(b.levels) - len(common(a, b).levels)` and `Terminal(d, p).relative_to(r).of == d.relative_to(r)` for random values.
14
+ - **R8. Numbering is pure.** `allocate` returns the lowest free number from `first` given `taken` and `reserved`, `number_group` equals `allocate` applied in sequence with each result added to `taken`, and refdef holds no numbering store. GATE: `tests/test_numbering.py` asserts `allocate` against a brute-force search and `number_group` against the sequential fold, for random inputs.
15
+ - **R9. One natural order.** `natural_key` orders digit runs by value and text runs as text, numbers before text, with the raw text as the last tie-break; `sort_key` is one `(aspect rank, natural_key(level.text))` pair per level, with the aspect order a required keyword and an unlisted aspect ranked after the listed ones in enum order. GATE: `tests/test_order.py` asserts the fixed cases (`-W2 < -W10`, `X01 < X1`, numbers before text), a property test that `sorted` by `sort_key` is a total order consistent with `natural_key` on single-level designations, and that `sort_key` has no default for `aspects`.
16
+ - **R10. The class table is versioned.** `ClassTable.version` is refdef's own line, bumps whenever the entry tuple changes, and the changelog names each moved code. GATE: `tests/test_table_version.py` hashes the entry tuple and compares it with the hash stored beside `version`, failing when the entries change and the version does not; the changelog line is review.
17
+ - **R11. One PLC grammar in the core.** `refdef.plc` holds only the direct-address form (`%` + memory area + optional size + dotted path, `*` for incomplete), with `as_bit` the one normalisation, and every vendor dialect is its own module behind its own type. GATE: `tests/test_plc.py` round-trips `tests/corpus/addresses.txt` through `parse_address` and `str`, asserts `as_bit` changes only a size-less dotted address, and `tests/test_boundary.py` fails a vendor name (`siemens`, `rockwell`, `omron`, `mitsubishi`, `beckhoff`) in `plc.py`.
18
+ - **R12. Pins and signal names are opaque.** refdef never reads inside a pin or a signal name: the only operations on them are the `:` split in `parse`, the `:` join in `render` and `natural_key`. GATE: an AST test in `tests/test_boundary.py` fails any attribute call, subscript or comparison on `pins`, a pin element or `Signal.name` under `src/refdef` outside those three sites; the F5 name list below catches a field that would hold their meaning.
19
+
20
+ ## refdef's lists for the family gates
21
+
22
+ - F4 allowlist of `str` fields, by design: `Level.code` (class letters), `Level.label` (verbatim text), `Designation.top`, `Terminal.pins` (`tuple[str, ...]`, opaque by R12), `Signal.name` (opaque by R12), `ClassEntry.code`, `ClassEntry.title`, `ClassTable.name`, `ClassTable.version`, `Scheme.name`, `Issue.subject`, `Issue.message`. Every other closed set is an enum: `Aspect`, `IssueCode`, `plc.Memory`, `plc.Size`.
23
+ - F5 boundary names for refdef: F5's six (`rating`, `voltage`, `svg`, `x`, `y`, `page`) plus `marking`, `role`, `kind`, `potential`.
24
+ - F6 caller conventions refdef takes only as arguments: `bare` (which aspect a signless tag means, required), `aspects` (the sort order of aspects, required), `pad`, `dotted`, `first`, `case_sensitive`.
25
+ - F15 numeric types: none; no refdef surface class defines an arithmetic or bitwise dunder, and `Designation` is built by its constructor and `child()` only.
26
+
27
+ ## Family rules
28
+
29
+ These rules bind elecdef and refdef alike, and symdef where it can (symdef has one runtime dependency, so F1 does not bind it). Both repositories carry this text verbatim in `RULES.md`,
30
+ and `tests/test_family_rules.py` in each compares it to the other's last release.
31
+
32
+ - **F1.** The library declares no runtime dependency and imports only the standard library and
33
+ itself; libraries of the family never import each other.
34
+ GATE: `tests/test_deps.py` asserts `dependencies = []` in `pyproject.toml` and walks every
35
+ module's imports with `ast`, allowing only `sys.stdlib_module_names` and the package.
36
+ - **F2.** Every value is a frozen dataclass or an enum; the library performs no file, network,
37
+ clock or random access.
38
+ GATE: `tests/test_pure.py` fails a `@dataclass` without `frozen=True` and any import or name
39
+ from `os`, `pathlib`, `io`, `socket`, `time`, `datetime`, `random`, `secrets`, `uuid` or a
40
+ call to `open` under `src/`.
41
+ - **F3.** Validation returns issues as values and never raises for content; only a malformed
42
+ call (a wrong type, a wrong kind of argument, or a shape the library's `RULES.md` names as
43
+ malformed) raises `TypeError`.
44
+ GATE: `tests/test_check.py` runs `check` over every malformed fixture in `tests/fixtures/`
45
+ and asserts a non-empty issue tuple and no exception; `tests/test_malformed.py` asserts
46
+ `TypeError` for each malformed-call fixture.
47
+ - **F4.** A closed vocabulary is an enum or a typed constant, never a string.
48
+ GATE: `tests/test_vocab.py` fails any dataclass field annotated `str` that is not in the
49
+ allowlist that the test names and `RULES.md` lists for that library.
50
+ - **F5.** A fact has one home across the family: `symdef` draws, `elecdef` says what a thing is,
51
+ `refdef` says what it is called, a consumer connects and lays out.
52
+ GATE: `tests/test_boundary.py` fails a field or function named for another home, from a list each
53
+ library keeps in the test (for elecdef: `designation`, `tag`, `svg`, `x`, `y`, `page`; for refdef:
54
+ `rating`, `voltage`, `svg`, `x`, `y`, `page`); the rest is review, because a
55
+ duplicated meaning under a new name is invisible to a test.
56
+ - **F6.** A caller's convention (a padding, an ordering, a default, a project class) arrives as an
57
+ argument or a value; no general function carries a caller's default. A fact the library itself
58
+ defines (a block's default terminal markings, the shipped scheme's signs) is not a convention.
59
+ GATE: the tests of every option-taking function pass the option explicitly, and
60
+ `tests/test_boundary.py` fails a module-level default that names a consumer; what counts as a
61
+ convention is review, because a test cannot tell a convention from a fact.
62
+ - **F7.** No paid standard is cited and no standard's wording is used; open sources are used
63
+ only with the credit block in `README.md`.
64
+ GATE: `tests/test_wording.py` fails a standards-body abbreviation followed by digits, an
65
+ edition year beside a table or scheme name, or a clause reference (the pattern list lives in
66
+ the test) anywhere under `src/`, `docs/`, `README.md` or
67
+ `CHANGELOG.md` outside the README credit block, and asserts the credit block names every
68
+ open source a `tables/` module header declares.
69
+ - **F8.** The public surface is the names in `__all__` of the top level and of each door, read
70
+ by `python -m <library> api`, and it changes only with the snapshot.
71
+ GATE: `tests/test_surface.py` compares `python -m <library> api` to `docs/api-snapshot.txt`
72
+ byte for byte, and fails an import of a non-surface name in `docs/` and `tests/guide/`.
73
+ - **F9.** Every surface name has a docstring and a runnable example on the docs site.
74
+ GATE: `tests/test_docs.py` asserts a non-empty docstring for every surface name, one fenced
75
+ Python block naming it under `docs/guide/`, and runs every fenced block; the strict
76
+ Zensical docs build runs in CI.
77
+ - **F10.** Code stays lean: a function has at most 15 statements, complexity 8 and 12 branches,
78
+ no `def` inside a `def`; a module has at most 300 code lines; a docstring is one summary line
79
+ plus at most two lines of why.
80
+ GATE: ruff `PLR0915=15`, `C901=8`, `PLR0912=12` in `pyproject.toml`; `tests/test_lean.py` for
81
+ nested defs, module lines and docstring shape; every `noqa` and `ty: ignore` names its reason
82
+ on its line, checked by the same test.
83
+ - **F11.** Versions are semantic and every surface change carries a changelog line.
84
+ GATE: `scripts/release_guard.py` in CI fails when `docs/api-snapshot.txt` differs from the
85
+ last tag's copy and `CHANGELOG.md` has no line under the unreleased heading, and when a name
86
+ disappeared without a major bump (minor before 1.0).
87
+ - **F12.** A pull request that edits `DESIGN.md` or `RULES.md`, or changes
88
+ `docs/api-snapshot.txt`, or changes a shipped table under `src/<library>/tables/`, carries a
89
+ `Design-review:` trailer naming an approved review. The maintainer approves a change to
90
+ `DESIGN.md` or `RULES.md`; the design reviewer approves a surface or table change, and the
91
+ maintainer sees it at the next review.
92
+ GATE: `scripts/design_guard.py` in CI inspects every commit in the range touching those
93
+ paths and fails one without the trailer; branch protection requires the job.
94
+ - **F13.** A consumer's missing need is met by a reviewed surface extension, never by a
95
+ workaround in the consumer, a private import, a copy of the rule or a consumer-specific
96
+ default in the library.
97
+ GATE: `tests/test_boundary.py` fails an import or a name of another family library under `src/`, and
98
+ `tests/test_standalone.py` (F17) fails an application's name; the consumer side is review, because the library cannot see the
99
+ consumer's tree.
100
+ - **F14.** The library is MIT licensed, and the licence covers its code and its shipped tables.
101
+ GATE: `tests/test_licence.py` asserts `LICENSE` holds the MIT text and `pyproject.toml`
102
+ declares `license = "MIT"`, and fails a `tables/` module header that declares an open source
103
+ whose licence is share-alike or non-commercial.
104
+ - **F15.** One public spelling per task: a second way to build, read or print the same value is
105
+ not added, and no operator is overloaded to build a value; arithmetic belongs only to the
106
+ numeric value types the library's `RULES.md` names.
107
+ GATE: `tests/test_surface.py` fails an arithmetic or bitwise dunder (`__add__`, `__or__`,
108
+ `__truediv__` and their kin) on a surface class that `RULES.md` does not name as numeric;
109
+ a second spelling under a new name is review, because a test cannot see that two names do one
110
+ task.
111
+ - **F16.** An agent finds everything from one place: `AGENTS.md` in the repository and `llms.txt`
112
+ at the docs site root name the docs site, the surface command (`python -m <library> api`),
113
+ the guide, `DESIGN.md` and `RULES.md`, and say that a change follows `RULES.md`.
114
+ GATE: `tests/test_agents.py` asserts both files exist, name each of those five targets, and
115
+ that every link in them resolves in the built site (the strict Zensical docs build).
116
+ - **F17.** The library stands alone: no file in the repository names an application that uses it.
117
+ It is written for every electrical engineer, and its examples are general.
118
+ GATE: `tests/test_standalone.py` lowercases every word in the tracked files and fails one whose
119
+ SHA-256 is in the test's list of application-name hashes, so the test spells no name itself.
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ This file holds one section per release of refdef, newest first.
4
+ Every change to the public surface gets a line under Unreleased until the release is cut.
5
+
6
+ ## Unreleased
7
+
8
+ ## 0.1.0 (2026-10-22)
9
+
10
+ - Add `Aspect`, `Level`, `Designation`, `Terminal`, `Signal`, `Issue`, `IssueCode`, `Parsed` and `render`.
11
+ - Add `parse`.
12
+ - Add `within`, `relative_to` and `common` on `Designation`, and `relative_to` on `Terminal` and `Signal`.
13
+ - Add `allocate`, `number_group`, `natural_key` and `sort_key`.
14
+ - Add the door `refdef.plc`: `Memory`, `Size`, `Address`, `ChannelTag` and `parse_address`.
15
+ - Add `Scheme`, `ClassEntry`, `STANDARD` and `check`.
16
+ - Add class table version 1: 17 main letters and 166 two-letter codes (first version, no moved code).
17
+ - First release: the package scaffold with the family rule gates, standard library only.
@@ -0,0 +1,98 @@
1
+ ## refdef
2
+ class Aspect
3
+ The tree a level belongs to; the value is its sign.
4
+
5
+ The first three are on in the shipped scheme; declaration order is the rank tie-break.
6
+
7
+ class ClassEntry(code: 'str', title: 'str') -> 'None'
8
+ One class code with its title, the typical device of the class.
9
+
10
+ class Designation(levels: 'tuple[Level, ...]' = (), top: 'str' = '') -> 'None'
11
+ A tuple of levels and an optional top node, built by the constructor and `child` only.
12
+
13
+ Join two through their levels; the empty designation is the root.
14
+
15
+ class Issue(code: 'IssueCode', subject: 'str', message: 'str') -> 'None'
16
+ One finding about a string or a designation: a code, the offending text, a short sentence.
17
+
18
+ class IssueCode
19
+ The 21 issue codes: the first 16 come from the readers, the last 5 from check.
20
+
21
+ class Level(aspect: 'Aspect', code: 'str' = '', number: 'int | None' = None, label: 'str' = '') -> 'None'
22
+ One step in a tree: letters and a number (`-K2`), a number alone (`+3`) or a label (`+ER`).
23
+
24
+ Letters are upper case and need a number; a label is never code-shaped. Else TypeError.
25
+
26
+ class Parsed(value: 'T', issues: 'tuple[Issue, ...]') -> 'None'
27
+ A value read from text together with the issues the reader found.
28
+
29
+ STANDARD: Scheme
30
+ What `check` tests against: aspects on, a letter rule and a class table.
31
+
32
+ `STANDARD` is the shipped scheme: `= - +` on, 1-3 letters without I and O, a versioned table.
33
+
34
+ class Scheme(name: 'str', aspects: 'frozenset[Aspect]', letters: 'LetterRule', classes: 'ClassTable') -> 'None'
35
+ What `check` tests against: aspects on, a letter rule and a class table.
36
+
37
+ `STANDARD` is the shipped scheme: `= - +` on, 1-3 letters without I and O, a versioned table.
38
+
39
+ class Signal(of: 'Designation | None', name: 'str') -> 'None'
40
+ A signal name on a designation, printed `of;name`; `of` is None for a bare name.
41
+
42
+ The name is stripped and non-empty and may hold `: ; &`. It is opaque text.
43
+
44
+ class Terminal(of: 'Designation', pins: 'tuple[str, ...]') -> 'None'
45
+ A designation and the pins on it, printed `of:pin:pin`.
46
+
47
+ At least one pin; each is stripped, non-empty and without `:`. Pins are opaque text.
48
+
49
+ allocate(aspect: 'Aspect', code: 'str', taken: 'Collection[Level]', reserved: 'Collection[Level]' = (), *, first: 'int' = 1) -> 'int'
50
+ The smallest number from `first` for which `Level(aspect, code, n)` is in neither collection.
51
+
52
+ `first` below 1 is a TypeError. Nothing is stored: the caller passes what is taken.
53
+
54
+ check(designations: 'Iterable[Designation]', scheme: 'Scheme') -> 'tuple[Issue, ...]'
55
+ Issues of the designations against `scheme`, in order; duplicates first for each one.
56
+
57
+ Terminals and signals are not checked: pass their `of`.
58
+
59
+ natural_key(text: 'str', *, case_sensitive: 'bool' = True) -> 'tuple[Any, ...]'
60
+ A sort key: digit runs by value, text runs as text, numbers first, then the raw text.
61
+
62
+ number_group(aspect: 'Aspect', codes: 'Sequence[str]', taken: 'Collection[Level]' = (), reserved: 'Collection[Level]' = ()) -> 'tuple[Level, ...]'
63
+ One level per code, each numbered after the earlier ones; `taken` is not changed.
64
+
65
+ parse(text: 'str', *, bare: 'Aspect') -> 'Parsed[Designation | Terminal | Signal]'
66
+ Read text as a designation, terminal or signal, with an issue for each repair.
67
+
68
+ `bare` is the aspect of text before the first sign. Never raises for content.
69
+
70
+ render(d: 'Designation', *, pad: 'Mapping[str, int] | None' = None, dotted: 'Collection[Aspect]' = (), bare: 'bool' = False) -> 'str'
71
+ Print a designation; `pad`, `dotted` and `bare` are the explicit dialects, none is `str(d)`.
72
+
73
+ `pad={"W": 4}` gives `-W0001`; `dotted=(P,)` gives `-W25.1`; `bare=True` omits the first sign.
74
+
75
+ sort_key(d: 'Designation', *, aspects: 'Sequence[Aspect]', case_sensitive: 'bool' = True) -> 'tuple[Any, ...]'
76
+ A sort key: per level the rank of its aspect in `aspects` and its natural key.
77
+
78
+ Aspects not listed rank after the listed ones, in enum order. The top is not in the key.
79
+
80
+ ## refdef.plc
81
+ class Address(memory: 'Memory', size: 'Size', path: 'tuple[int, ...]') -> 'None'
82
+ A direct address printed `%` memory size path; an empty path is the incomplete `%I*`.
83
+
84
+ A path index is an int from 0 to 999_999_999; any other shape raises TypeError.
85
+
86
+ class ChannelTag(terminal: 'Terminal', address: 'Address | None' = None) -> 'None'
87
+ A terminal with an optional address, printed as the terminal alone (`-KE1:DI03`).
88
+
89
+ class Memory
90
+ The memory area of an address: input, output or marker; the value is the letter.
91
+
92
+ class Size
93
+ The access size of an address: none, bit, byte, word, double word or long word.
94
+
95
+ parse_address(text: 'str') -> 'Parsed[Address | None]'
96
+ Read a direct address; repairs (outer whitespace, leading zeros) are issues.
97
+
98
+ Text that is not an address gives `None` and a NOT_ADDRESS issue.
@@ -0,0 +1,37 @@
1
+ # Build
2
+
3
+ A designation is a tuple of levels, built by the constructor and `child`, never by an operator.
4
+
5
+ ```python
6
+ from refdef import Aspect, Designation, Level
7
+ F, P, L = Aspect.FUNCTION, Aspect.PRODUCT, Aspect.LOCATION
8
+ u1 = Designation().child(P, "U", 1)
9
+ k1 = u1.child(P, "U", 2).child(P, "K", 3)
10
+ print(k1) # -U1-U2-K3
11
+ print(Designation((Level(L, label="ER"),) + k1.levels)) # +ER-U1-U2-K3
12
+ print(Level(P, "K", 3).text) # K3
13
+ print(k1.parent()) # -U1-U2
14
+ print(Designation((Level(L, label="ER"),) + k1.levels).only(P)) # -U1-U2-K3
15
+ ```
16
+
17
+ A level is upper-case letters with a number, a number alone, or a label. A label is never shaped
18
+ like a code, so `K1` and `7` cannot be labels (`K0` can). A malformed call raises `TypeError`.
19
+
20
+ ```python
21
+ from refdef import Aspect, Level
22
+ try:
23
+ Level(Aspect.PRODUCT, label="K1")
24
+ except TypeError:
25
+ print("refused") # refused
26
+ ```
27
+
28
+ ## Findings
29
+
30
+ A reader or a checker reports an `Issue` with an `IssueCode`, and a reader returns its value in `Parsed`.
31
+
32
+ ```python
33
+ from refdef import Designation, Issue, IssueCode, Parsed
34
+ issue = Issue(IssueCode.EMPTY, "", "the text is empty")
35
+ parsed = Parsed(Designation(), (issue,))
36
+ print(parsed.issues[0].code.value) # empty
37
+ ```