Skip to content

Commit fae23d9

Browse files
authored
Release/1.0.0 (#1)
1 parent ab516d3 commit fae23d9

84 files changed

Lines changed: 7672 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/hooks/php-ordering-conformance.py‎

Lines changed: 607 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#!/usr/bin/env python3
2+
"""Prose punctuation conformance hook for tiny-blocks PHP libraries.
3+
4+
Self-contained PostToolUse hook on Edit|Write|MultiEdit. Verifies prose punctuation:
5+
no em-dash, en-dash, or ` -- ` as a clause separator in Markdown prose or
6+
PHP comments, plus no `;` separator in Markdown. The checks read raw text only, so
7+
this script carries no PHP lexer. Markdown files route to the prose check, PHP
8+
sources to the comment check.
9+
10+
Control flow uses guard clauses only and nesting never exceeds two levels. Reports
11+
violations to stderr and exits 2 to prompt Claude with feedback; exits 0 silently if
12+
no violations or the file is out of scope.
13+
"""
14+
15+
import json
16+
import re
17+
import sys
18+
from dataclasses import dataclass
19+
from pathlib import Path
20+
from typing import Final
21+
22+
# --- Configuration ----------------------------------------------------------
23+
24+
# In-scope files: PHP sources under src/ or tests/, plus any Markdown file.
25+
SCOPE_PATTERN: Final = re.compile(r"(^|/)(src|tests)/.+\.php$")
26+
MARKDOWN_PATTERN: Final = re.compile(r"\.md$")
27+
28+
# Prohibited prose punctuation as clause separators. Em-dash U+2014, en-dash U+2013,
29+
# spaced double hyphen, and (Markdown only) the semicolon.
30+
PROSE_PUNCTUATION: Final = re.compile(r"[\u2014\u2013]| -- |;")
31+
PROSE_DASHES: Final = re.compile(r"[\u2014\u2013]| -- ")
32+
FENCE: Final = re.compile(r"^\s*```")
33+
PHP_COMMENT: Final = re.compile(r"/\*.*?\*/|//[^\n]*|#(?!\[)[^\n]*", re.DOTALL)
34+
INLINE_CODE: Final = re.compile(r"`[^`]*`")
35+
36+
MAX_ERRORS_REPORTED = 30
37+
38+
39+
# --- Types --------------------------------------------------------------------
40+
41+
42+
@dataclass(frozen=True)
43+
class Violation:
44+
"""One style violation at a source position."""
45+
46+
line: int
47+
path: str
48+
message: str
49+
50+
def __str__(self) -> str:
51+
return f"{self.path}:{self.line}: {self.message}"
52+
53+
54+
@dataclass(frozen=True)
55+
class FileUnit:
56+
"""One file under analysis: its path and raw text. No lexing needed here."""
57+
58+
path: str
59+
text: str
60+
61+
62+
# --- Checks -------------------------------------------------------------------
63+
64+
65+
def is_markdown(path: str) -> bool:
66+
"""Whether the path is a Markdown file, routed to the Markdown prose check."""
67+
return bool(MARKDOWN_PATTERN.search(path))
68+
69+
70+
def markdown_violations(unit: FileUnit) -> tuple[Violation, ...]:
71+
"""No `;`, em-dash, en-dash, or ` -- ` as a clause
72+
separator in Markdown prose. Fenced code and table rows are exempt."""
73+
violations = []
74+
in_fence = False
75+
for number, line in enumerate(unit.text.split("\n"), start=1):
76+
if FENCE.match(line):
77+
in_fence = not in_fence
78+
continue
79+
80+
if in_fence or "|" in line:
81+
continue
82+
83+
if PROSE_PUNCTUATION.search(INLINE_CODE.sub("", line)):
84+
violations.append(Violation(
85+
line=number,
86+
path=unit.path,
87+
message=(
88+
"prohibited prose punctuation (`;`, em-dash, en-dash, or ` -- `), "
89+
"split the sentence or use a comma, colon, or parentheses"
90+
),
91+
))
92+
return tuple(violations)
93+
94+
95+
def comment_violations(unit: FileUnit) -> tuple[Violation, ...]:
96+
"""In PHPDoc and comments: no em-dash, en-dash,
97+
or ` -- ` as a separator. The `;` is not checked in PHP comments (it terminates
98+
statements in commented code)."""
99+
violations = []
100+
for match in PHP_COMMENT.finditer(unit.text):
101+
if PROSE_DASHES.search(INLINE_CODE.sub("", match.group(0))):
102+
violations.append(Violation(
103+
line=unit.text.count("\n", 0, match.start()) + 1,
104+
path=unit.path,
105+
message=(
106+
"prohibited prose punctuation (em-dash, en-dash, or ` -- `) in a "
107+
"comment"
108+
),
109+
))
110+
return tuple(violations)
111+
112+
113+
def punctuation_violations(unit: FileUnit) -> tuple[Violation, ...]:
114+
"""Punctuation violations for one file: Markdown prose for `.md`, comments otherwise."""
115+
if is_markdown(unit.path):
116+
return markdown_violations(unit)
117+
return comment_violations(unit)
118+
119+
120+
# --- Shell --------------------------------------------------------------------
121+
122+
123+
def requested_paths() -> list[Path]:
124+
"""The paths to verify, from argv or from the hook's stdin payload."""
125+
if len(sys.argv) > 1:
126+
return [Path(argument) for argument in sys.argv[1:]]
127+
try:
128+
payload = json.load(sys.stdin)
129+
except ValueError:
130+
return []
131+
132+
file_path = (payload.get("tool_input") or {}).get("file_path")
133+
134+
if isinstance(file_path, str):
135+
return [Path(file_path)]
136+
return []
137+
138+
139+
def in_scope(path: Path) -> bool:
140+
"""Whether the path is a PHP source or any Markdown this hook covers."""
141+
posix = path.as_posix()
142+
matched = SCOPE_PATTERN.search(posix) or MARKDOWN_PATTERN.search(posix)
143+
return bool(matched) and path.is_file()
144+
145+
146+
def file_violations(path: Path) -> tuple[Violation, ...]:
147+
"""The punctuation violations for one file."""
148+
unit = FileUnit(
149+
path=path.as_posix(),
150+
text=path.read_text(errors="replace", encoding="utf-8"),
151+
)
152+
return punctuation_violations(unit)
153+
154+
155+
def main() -> int:
156+
violations = [
157+
violation
158+
for path in requested_paths()
159+
if in_scope(path)
160+
for violation in file_violations(path)
161+
]
162+
163+
if not violations:
164+
return 0
165+
166+
for violation in violations[:MAX_ERRORS_REPORTED]:
167+
print(violation, file=sys.stderr)
168+
overflow = len(violations) - MAX_ERRORS_REPORTED
169+
170+
if overflow > 0:
171+
print(f"... and {overflow} more violations", file=sys.stderr)
172+
return 2
173+
174+
175+
if __name__ == "__main__":
176+
sys.exit(main())
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
description: Folder structure, public API boundary, and Internal/ semantics for PHP libraries.
3+
paths:
4+
- "src/**/*.php"
5+
---
6+
7+
# Architecture
8+
9+
Covers the physical layout of the library. Folder structure, the boundary between public API and
10+
implementation detail, and where each type of class lives. Semantic rules (value objects,
11+
exceptions, enums, complexity, nomenclature) live in `php-library-modeling.md`. Code style lives
12+
in `php-library-code-style.md`.
13+
14+
## Pre-output checklist
15+
16+
Verify every item before producing or relocating any file. If any item fails, revise before
17+
outputting.
18+
19+
1. None of the following folder names exist in `src/`: `Models/`, `Entities/`, `ValueObjects/`,
20+
`Enums/`, `Domain/`. They carry no semantic content and conflate technical role with domain
21+
meaning.
22+
2. The `src/` root contains only interfaces, extension points, public enums, thin orchestration
23+
classes, and primary implementations or façades. Substantial logic (algorithms, state machines,
24+
I/O) lives in `src/Internal/`, never at the root.
25+
3. `src/Internal/` is implementation detail and not part of the public API. Breaking changes
26+
inside `src/Internal/` are not semver-breaking.
27+
4. Consumers must not reference, extend, or depend on any type inside `src/Internal/`. The
28+
namespace itself is the boundary.
29+
5. Public exception classes live in `src/Exceptions/`.
30+
6. Internal exception classes live in `src/Internal/Exceptions/`.
31+
7. Public enums live at the `src/` root or inside a public `<ConceptGroup>/` folder. Enums used
32+
only by internals live in `src/Internal/`.
33+
8. Public interfaces live at the `src/` root or inside a public `<ConceptGroup>/` folder.
34+
9. A `<ConceptGroup>/` folder at the `src/` root groups related public types under a shared
35+
concept. Each group has its own namespace and is part of the public API.
36+
10. `<ConceptGroup>/` is optional. Use it only when the library exposes several coherent groups of
37+
types (for example, aggregates and events) rather than a flat set of types around a single
38+
concept.
39+
11. Test fixtures representing domain concepts live in `tests/Models/`. Test doubles for system
40+
boundaries live at the root of `tests/Unit/` or `tests/Integration/`. No dedicated `Mocks/`
41+
or `Doubles/` subdirectory exists. Vendor compatibility (driver) tests, verifying the
42+
library against specific external libraries/frameworks, are optional and have no `src/`
43+
counterpart. They exist only as tests, under `tests/Integration/Drivers/<Vendor>/`,
44+
grouped by vendor. Never a top-level `Drivers/` under `tests/`.
45+
12. The `tests/Integration/` folder exists only when the library interacts with external
46+
infrastructure (filesystem, database, network). Otherwise, the folder is absent.
47+
48+
## Folder structure
49+
50+
Canonical layout for a PHP library in the tiny-blocks ecosystem.
51+
52+
```
53+
src/
54+
├── <PublicInterface>.php # public contract at root
55+
├── <Implementation>.php # main implementation or extension point at root
56+
├── <PublicEnum>.php # public enum at root
57+
├── <ConceptGroup>/ # public folder grouping related public types under a shared concept
58+
│ ├── <PublicType>.php
59+
│ └── ...
60+
├── Internal/ # implementation details, not part of the public API
61+
│ ├── <Collaborator>.php
62+
│ └── Exceptions/ # internal exception classes
63+
└── Exceptions/ # public exception classes
64+
65+
tests/
66+
├── Models/ # domain fixtures reused across tests
67+
├── Unit/ # unit tests targeting the public API
68+
│ ├── <SomeMock>.php # test doubles at root of Unit/
69+
│ └── <SomeSpy>.php
70+
└── Integration/ # only present when the library interacts with infrastructure
71+
├── Drivers/ # only present when the library exposes vendor-specific drivers
72+
│ └── <Vendor>/ # tests against one specific third-party implementation
73+
└── <SomeMock>.php # test doubles at root of Integration/ when needed
74+
```
75+
76+
Never use `Models/`, `Entities/`, `ValueObjects/`, `Enums/`, or `Domain/` as folder names. They
77+
carry no semantic content and describe technical role instead of domain meaning.
78+
79+
## Public API boundary
80+
81+
The `src/` root is the contract. Everything at the root, plus everything inside public
82+
`<ConceptGroup>/` folders and the public `Exceptions/` folder, is what consumers depend on. Changes
83+
to these types follow semver rules.
84+
85+
`src/Internal/` is implementation detail. The namespace itself signals the boundary. Consumers
86+
must not depend on any type inside `src/Internal/`. Breaking changes inside `src/Internal/` are
87+
not semver-breaking for the library.
88+
89+
### What lives at the public boundary
90+
91+
- Interfaces that define contracts for consumers.
92+
- Extension points designed to be subclassed or composed by consumers.
93+
- Public enums and value objects consumers manipulate directly.
94+
- Thin orchestration classes that wire collaborators together without containing substantial logic.
95+
- Public exception classes consumers may catch.
96+
97+
### What lives in `src/Internal/`
98+
99+
- Algorithms, state machines, and complex transformations.
100+
- Adapters for I/O (filesystem, network, database).
101+
- Collaborators that exist purely to break a public class into testable units.
102+
- Implementation details that may change between minor or patch releases.
103+
- Internal exception classes raised by collaborators.
104+
105+
## Reference examples
106+
107+
### Small library with flat root
108+
109+
```
110+
src/
111+
├── Timezone.php # public value object
112+
├── Timezones.php # public collection
113+
├── Clock.php # public interface
114+
└── Internal/
115+
├── SystemClock.php # default Clock implementation
116+
└── Exceptions/
117+
└── InvalidTimezone.php
118+
```
119+
120+
Everything lives at the root or inside `Internal/`. No `<ConceptGroup>/` folders. Suitable when
121+
the library exposes a small, cohesive set of types around a single concept.
122+
123+
### Library with public concept groups
124+
125+
```
126+
src/
127+
├── ValueObject.php # public extension point at root
128+
├── Aggregate/ # public namespace grouping aggregate types
129+
│ ├── AggregateRoot.php
130+
│ ├── EventualAggregateRoot.php
131+
│ └── ModelVersion.php
132+
├── Event/ # public namespace grouping event types
133+
│ ├── EventRecord.php
134+
│ ├── EventRecords.php
135+
│ └── SequenceNumber.php
136+
├── Internal/
137+
│ ├── DefaultModelVersionResolver.php
138+
│ └── Exceptions/
139+
│ └── InvalidSequenceNumber.php
140+
└── Exceptions/
141+
└── EventRecordingFailure.php
142+
```
143+
144+
`Aggregate/` and `Event/` are public folders at the root, each grouping a coherent set of public
145+
types under one shared concept. Consumers import directly, for example
146+
`TinyBlocks\<LibName>\Aggregate\AggregateRoot`. Suitable when the library exposes several distinct
147+
concept areas, each with its own set of related types.

0 commit comments

Comments
 (0)