Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
57844f6
Restore fresh composer install with explicit vimeo/psalm constraint
koriym Jul 17, 2026
d38332b
Run tests weekly and on manual dispatch
koriym Jul 17, 2026
f2ff752
Ignore generated semantic-dev logs
koriym Jul 17, 2026
71a1523
Fix stale test-running instructions in CLAUDE.md
koriym Jul 17, 2026
f54408a
Align documented PHP requirement with composer constraint
koriym Jul 17, 2026
03d5763
Complete composer.json metadata for all demos
koriym Jul 17, 2026
9544894
Test the Semantic classes the framework actually resolves in blog-pub…
koriym Jul 17, 2026
8d00206
Test the idempotency guard of order-processing Potentials
koriym Jul 17, 2026
c38901f
Cover hello-world's Semantic layer and rejection path
koriym Jul 17, 2026
61da8c0
Order constructor parameters per the Input-then-Inject invariant
koriym Jul 17, 2026
abedcdd
Remove dead false-check in PasswordHasher and fix blog-publishing des…
koriym Jul 17, 2026
78b62d7
State the actual test coverage shape in both READMEs
koriym Jul 17, 2026
a252c67
Sharpen the Reason interface rule to match what the demos do
koriym Jul 17, 2026
fd61c2c
Fix CardExpiry month-boundary bugs and cover it with tests
koriym Jul 17, 2026
76d1ad1
Replace the fictitious #[Moment] attribute with the real #[Inject]
koriym Jul 17, 2026
95517c6
Drop the false $being claim from the insurance-claim catalog entry
koriym Jul 17, 2026
64a294c
Name amount Semantics after the parameters they validate; fix DTI fal…
koriym Jul 17, 2026
f4bef40
Note the qualifier-attribute convention in MomentTemplate
koriym Jul 17, 2026
9f47a6e
Document the pure-data Moment variant in invariant 5
koriym Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [1.x]
pull_request:
branches: [1.x]
schedule:
- cron: '0 3 * * 1'
workflow_dispatch:

jobs:
tests:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
vendor/
.phpunit.cache/
composer.lock
demos/*/var/log/semantic-dev-*.json
28 changes: 19 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,26 @@ will not run under Ray.Di or will break framework expectations.
5. **Moments**: implement `MomentInterface`; create the Potential object in
the constructor; commit it in `be()`. Do NOT call `be()` from the
constructor. The owning Final calls `be()` on its Moments during self-
completion.
completion. Exception: a **pure-data Moment** (a part of the whole that
carries no Potential and performs no side effect) MAY omit
`MomentInterface` and `be()` entirely — state this explicitly in the
class docblock (see `demos/insurance-claim/src/Moment/FraudCleared.php`).
6. **Finals that converge Moments**: take each Moment via `#[Inject]` and call
`$this->foo->be()` in the constructor body, then derive the actualized
fields. See `demos/order-processing/src/Final/OrderConfirmed.php` for the
canonical shape.
7. **Semantic validators**: one class per concept, one `#[Validate]` method,
throw a domain exception from `src/Exception/`. Link to schema.org in the
docblock when a standard term exists (`@link https://schema.org/…`).
8. **Reason services**: Reasons sitting at an **external I/O boundary**
(HTTP, DB, payment gateway, third-party API, filesystem, clock, randomness)
8. **Reason services**: Reasons that model an **external system**
(HTTP, DB, payment gateway, credit bureau, third-party API, filesystem)
MUST define an `…Interface`; consumers depend on the interface and Ray.Di
binds the implementation — so tests can swap in a Fake. Reasons that are
**pure in-process policies or calculators** (rule-book classes with no
I/O) MAY be injected as concrete classes; no interface is required.
Examples: `PaymentGatewayInterface` and `CreditBureauInterface` (boundary);
`IncomePolicy` and `JTASProtocol` (pure policy).
**in-process policies, calculators, or generators** (rule-book classes,
ID/token/timestamp generators with no out-of-process dependency) MAY be
injected as concrete classes; no interface is required.
Examples: `PaymentGatewayInterface` and `CreditBureauInterface` (external
system); `IncomePolicy`, `JTASProtocol`, and `UserIdGenerator` (in-process).
9. **Namespaces**: follow the existing per-demo pattern
`Be\Pattern\<Name>\<Layer>\…`. Never invent a new root namespace.
10. **No side effects in Beings.** A Being transforms data; external I/O
Expand Down Expand Up @@ -133,9 +137,15 @@ a real demo (replace `Template` with the actual demo name).

## 6. Running tests

Each demo is a standalone Composer project — there is no root `composer.json`.
Install dependencies and run PHPUnit inside the demo directory:

```bash
composer test # all demos
./demos/vendor/bin/phpunit demos/medical-triage/tests/ # one demo
# one demo
cd demos/medical-triage && composer install && ./vendor/bin/phpunit

# all demos
for d in demos/*/; do (cd "$d" && composer install -q && ./vendor/bin/phpunit); done
```

If you modify a demo, always run its tests before concluding the task.
6 changes: 3 additions & 3 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ flowchart LR
M3([Moment]) -.-> F1 & F2
```

保険請求処理デモ。`ClaimInput` と `PolicyInput` はどちらも `#[Be([ClaimSettled, ClaimEscalated])]` を宣言しており、`$being` の型マッチングによって各 Input がちょうど1つの Final に解決されます。`DamageValued`、`AdjustmentReviewed`、`FraudCleared` などの Moment は両方の Final に注入されるため、どちらの分岐を辿っても同じ自己完結ロジックが共有されます。
保険請求処理デモ。`ClaimInput` と `PolicyInput` はどちらも `#[Be([ClaimSettled, ClaimEscalated])]` を宣言しており、2つの Final を可能な帰結として名指しします。`DamageValued`、`AdjustmentReviewed`、`FraudCleared` などの Moment は両方の Final に注入されるため、どちらの分岐を辿っても同じ自己完結ロジックが共有されます。

> 具体: `ClaimInput` + `PolicyInput` → `ClaimSettled` または `ClaimEscalated`(各 Final に共有 Moment を注入)

Expand All @@ -179,11 +179,11 @@ flowchart LR
cd demos/hello-world && composer install && vendor/bin/phpunit
```

各デモには正常系統合テスト、Semantic検証単体テスト、Reasonレイヤーロジックテスト、そして該当する場合はPotential冪等性テストが含まれます。
各デモには正常系テストとSemantic検証単体テストが含まれます。上級デモにはさらにReasonレイヤーロジックテスト、該当する場合はPotential冪等性テストが含まれます。

## 要件

- PHP 8.2+
- PHP 8.3+
- [Ray.Di](https://ray-di.github.io/)(依存性注入)

## 背景
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ flowchart LR
M3([Moment]) -.-> F1 & F2
```

Insurance claim processing. `ClaimInput` and `PolicyInput` both declare `#[Be([ClaimSettled, ClaimEscalated])]`, so each Input resolves to exactly one of the two Finals by `$being` type matching. Moments such as `DamageValued`, `AdjustmentReviewed` and `FraudCleared` are injected into both Finals, so the same self-completion logic is shared regardless of which branch is taken.
Insurance claim processing. `ClaimInput` and `PolicyInput` both declare `#[Be([ClaimSettled, ClaimEscalated])]`, naming the two Finals as possible outcomes. Moments such as `DamageValued`, `AdjustmentReviewed` and `FraudCleared` are injected into both Finals, so the same self-completion logic is shared regardless of which branch is taken.

> Concrete: `ClaimInput` + `PolicyInput` → `ClaimSettled` | `ClaimEscalated`, with shared Moments injected into each Final.

Expand All @@ -179,11 +179,11 @@ See [Semantic Logging](https://be-framework.github.io/manuals/1.0/en/10-semantic
cd demos/hello-world && composer install && vendor/bin/phpunit
```

Every demo ships with happy-path integration tests, Semantic validation unit tests, Reason layer logic tests, and (where applicable) Potential idempotency tests.
Every demo ships with happy-path tests and Semantic validation unit tests; the advanced demos add Reason layer logic tests and (where applicable) Potential idempotency tests.

## Requirements

- PHP 8.2+
- PHP 8.3+
- [Ray.Di](https://ray-di.github.io/) (dependency injection)

## Background
Expand Down
6 changes: 4 additions & 2 deletions demos/blog-publishing/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "be-framework/demo-blog-publishing",
"description": "Be Framework Blog Publishing Demo - Moment without Potential (pure data Moment) and mini-diamond merge",
"description": "Be Framework Blog Publishing Demo - Multi-Reason Being orchestrating several Reason services",
"license": "MIT",
"type": "project",
"autoload": {
"psr-4": {
Expand All @@ -19,7 +20,8 @@
},
"require-dev": {
"be-framework/psalm-plugin": "1.x-dev",
"phpunit/phpunit": "^12"
"phpunit/phpunit": "^12",
"vimeo/psalm": "^7.0@beta"
},
"scripts": {
"psalm": "psalm --show-info=false",
Expand Down
2 changes: 1 addition & 1 deletion demos/blog-publishing/src/Moment/MetadataResolved.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
public string $slug;

public function __construct(
#[Inject] SlugGenerated $slugGenerated,
#[Input] public string $authorId,
#[Input] public array $tags,
#[Inject] SlugGenerated $slugGenerated,
#[Inject] AuthorResolverInterface $resolver,
) {
$this->slug = $slugGenerated->slug;
Expand Down
30 changes: 0 additions & 30 deletions demos/blog-publishing/src/Semantic/ArticleTitle.php

This file was deleted.

28 changes: 0 additions & 28 deletions demos/blog-publishing/src/Semantic/Tag.php

This file was deleted.

33 changes: 20 additions & 13 deletions demos/blog-publishing/tests/BlogPublishingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
use Be\Pattern\BlogPublishing\Final\ArticlePublished;
use Be\Pattern\BlogPublishing\Input\ArticleInput;
use Be\Pattern\BlogPublishing\Module\AppModule;
use Be\Pattern\BlogPublishing\Semantic\ArticleTitle;
use Be\Pattern\BlogPublishing\Semantic\AuthorId;
use Be\Pattern\BlogPublishing\Semantic\MarkdownBody;
use Be\Pattern\BlogPublishing\Semantic\Tag;
use Be\Pattern\BlogPublishing\Semantic\Tags;
use Be\Pattern\BlogPublishing\Semantic\Title;
use Be\Framework\Becoming;
use PHPUnit\Framework\TestCase;
use Ray\Di\Injector;
Expand Down Expand Up @@ -102,24 +102,24 @@ public function testExcerptIsTruncated(): void
// Semantic Validation Tests
// ──────────────────────────────────────────────

public function testValidArticleTitle(): void
public function testValidTitle(): void
{
$semantic = new ArticleTitle();
$semantic = new Title();
$semantic->validate('Understanding the BE Framework');
$this->addToAssertionCount(1);
}

public function testEmptyTitleThrowsException(): void
{
$this->expectException(InvalidTitleException::class);
$semantic = new ArticleTitle();
$semantic = new Title();
$semantic->validate('');
}

public function testTitleTooLongThrowsException(): void
{
$this->expectException(InvalidTitleException::class);
$semantic = new ArticleTitle();
$semantic = new Title();
$semantic->validate(str_repeat('a', 201));
}

Expand Down Expand Up @@ -158,24 +158,31 @@ public function testInvalidAuthorIdThrowsException(): void
$semantic->validate('not-a-valid-uuid');
}

public function testValidTag(): void
public function testValidTags(): void
{
$semantic = new Tag();
$semantic->validate('philosophy');
$semantic = new Tags();
$semantic->validate(['philosophy', 'framework', 'php']);
$this->addToAssertionCount(1);
}

public function testEmptyTagsThrowsException(): void
{
$this->expectException(InvalidTagException::class);
$semantic = new Tags();
$semantic->validate([]);
}

public function testInvalidTagFormatThrowsException(): void
{
$this->expectException(InvalidTagException::class);
$semantic = new Tag();
$semantic->validate('INVALID_TAG');
$semantic = new Tags();
$semantic->validate(['INVALID_TAG']);
}

public function testTagTooLongThrowsException(): void
{
$this->expectException(InvalidTagException::class);
$semantic = new Tag();
$semantic->validate(str_repeat('a', 31));
$semantic = new Tags();
$semantic->validate([str_repeat('a', 31)]);
}
}
4 changes: 3 additions & 1 deletion demos/contact-form/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "be-framework/demo-contact-form",
"description": "Be Framework Contact Form Demo - Semantic validation and Being transformation",
"license": "MIT",
"type": "project",
"autoload": {
"psr-4": {
Expand All @@ -19,7 +20,8 @@
},
"require-dev": {
"be-framework/psalm-plugin": "1.x-dev",
"phpunit/phpunit": "^12"
"phpunit/phpunit": "^12",
"vimeo/psalm": "^7.0@beta"
},
"scripts": {
"psalm": "psalm --show-info=false",
Expand Down
4 changes: 3 additions & 1 deletion demos/hello-world/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "be-framework/demo-hello-world",
"description": "Be Framework Hello World Demo - The simplest transformation",
"license": "MIT",
"type": "project",
"autoload": {
"psr-4": {
Expand All @@ -19,7 +20,8 @@
},
"require-dev": {
"be-framework/psalm-plugin": "1.x-dev",
"phpunit/phpunit": "^12"
"phpunit/phpunit": "^12",
"vimeo/psalm": "^7.0@beta"
},
"scripts": {
"dev": "php bin/be.php 'hello?name=World'",
Expand Down
24 changes: 24 additions & 0 deletions demos/hello-world/tests/HelloTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace Be\Pattern\Hello\Tests;

use Be\Pattern\Hello\Exception\InvalidNameException;
use Be\Pattern\Hello\Final\Hello;
use Be\Pattern\Hello\Input\HelloInput;
use Be\Pattern\Hello\Module\AppModule;
use Be\Pattern\Hello\Semantic\Name;
use Be\Framework\Becoming;
use Be\Framework\Exception\SemanticVariableException;
use PHPUnit\Framework\TestCase;
use Ray\Di\Injector;

Expand Down Expand Up @@ -41,4 +44,25 @@ public function testHelloWithDifferentName(): void

$this->assertSame('Hello Be Framework', $final->greeting);
}

public function testBecomingFlowRejectsEmptyName(): void
{
$this->expectException(SemanticVariableException::class);

($this->becoming)(new HelloInput(name: ' '));
}

public function testValidName(): void
{
$semantic = new Name();
$semantic->validate('World');
$this->addToAssertionCount(1);
}

public function testEmptyNameThrowsException(): void
{
$this->expectException(InvalidNameException::class);
$semantic = new Name();
$semantic->validate('');
}
}
2 changes: 1 addition & 1 deletion demos/insurance-claim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ the order-processing demo.
| **Moment** | `DamageValued`, `AdjustmentReviewed`, `FraudCleared`, `PaymentDispatched`, `EscalationQueued` |
| **Potential** | `DamageValuation`, `PaymentExecution` |
| **Final** | `ClaimSettled`, `ClaimEscalated` |
| **Semantic** | `PolicyNumber`, `IncidentDate`, `IncidentType`, `ClaimAmount`, `CoverageType`, `ClaimantId` |
| **Semantic** | `PolicyNumber`, `IncidentDate`, `IncidentType`, `EstimatedAmount`, `CoverageType`, `ClaimantId` |
| **Reason** | `ClaimRegistrar`, `PolicyRegistry`, `CoverageValidator`, `DamageAppraiser`, `AdjusterAllocator`, `FraudDetector`, `PaymentProcessor`, `SettlementPolicy` |
| **Exception** | `InvalidPolicyNumberException`, `InvalidIncidentDateException`, `InvalidIncidentTypeException`, `InvalidClaimAmountException`, `InvalidCoverageTypeException`, `InvalidClaimantIdException` |

Expand Down
5 changes: 4 additions & 1 deletion demos/insurance-claim/composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"name": "be-framework/demo-insurance-claim",
"description": "Be Framework Insurance Claim Demo - Complex convergence with shared Moments across branching Finals",
"license": "MIT",
"type": "project",
"autoload": {
"psr-4": {
Expand All @@ -18,7 +20,8 @@
},
"require-dev": {
"be-framework/psalm-plugin": "1.x-dev",
"phpunit/phpunit": "^12"
"phpunit/phpunit": "^12",
"vimeo/psalm": "^7.0@beta"
},
"scripts": {
"psalm": "psalm --show-info=false",
Expand Down
Loading
Loading