Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b97099
adding enforcement related files
Mar 22, 2026
f5458a4
fix testing - sonar error
Mar 22, 2026
095553d
fix - tests does not use real decision folder
Mar 22, 2026
d2ce621
fix related .decision folder in files
Mar 22, 2026
2d74f85
fix test
Mar 22, 2026
6b9ecfa
fix - test
Mar 22, 2026
79ee7c2
fix test - removing reflection
Mar 23, 2026
3ef103a
fix the fallback JSON path
Mar 23, 2026
be55d68
fix - stop overescaping annotation
Mar 23, 2026
9df3eaa
add fix for test and yaml example file and related docs
Mar 29, 2026
8ef458d
add fix - in decodeFile checking
Mar 30, 2026
187247e
fix - replacing tab with space
Mar 30, 2026
32a1355
fix - optimization in PhpStanFindingLoader and EnforceCommand
Mar 30, 2026
b0c5043
fix - in AnalyzerFindingLoader
Mar 30, 2026
a89623b
fix - removing space + adding removeUtf8Bom
Mar 30, 2026
0a73d14
fix - test unchecked UTF-16 file_put_content calls
Mar 30, 2026
87c8c42
fix - removing trailing whitespaces
Mar 30, 2026
80b1e02
fix updating uml diagram
Mar 30, 2026
540d16f
fix enforcement token normalization and summary field naming
Apr 3, 2026
b76887e
add regression test for PR comment summary count field
Apr 3, 2026
b725f3e
fix in decision rules connected to token at normalizeTokenList
Apr 3, 2026
aed74c1
removing trailing white spaces
Apr 3, 2026
d00bc58
add deterministic enforcement ordering and cache opt-out
Apr 3, 2026
91cb87f
adding path normalization
Apr 3, 2026
fbc9b75
fix: prevent duplicate decision lint validation messages
Apr 3, 2026
7556e8f
fix: formatting style
Apr 3, 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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ env.bash.sh
docs/env.bash.sh
.env
.env.*

.decisions
.vscode
build

# Ignore Markdown files except the anatomy guide
docs/*.md
docs/*.pem
!docs/decision-file-anatomy.md
50 changes: 46 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@ This project aims to follow [Keep a Changelog](https://keepachangelog.com/en/1.1

## [Unreleased]

## [1.3.0] - 2026-03-23

### Added
- CLI command `enforce` for the first `v1.3.0` enforcement-ready workflow.
- Consumes a generic JSON analyzer report from `--report`.
- Consumes native Semgrep JSON from `--semgrep-report`.
- Consumes native PHPStan JSON from `--phpstan-report`.
- Supports `--format json` for machine-readable CI/bot integration output.
- Matches findings to active decisions by scope and `rules.forbid` token.
- Prints violations grouped by decision ID and exits non-zero when decision-linked violations are found.
- Checked-in Semgrep example assets for the `DEC-0003` flow:
- `semgrep/rules/no-orm-in-order-domain.yaml`
- `examples/github-actions/phpdecide-semgrep-enforce.yaml`
- Added a second active sample decision/rule pair for template boundaries:
- `examples/decisions/DEC-0004.no-business-logic-in-templates.yaml`
- `semgrep/rules/no-business-logic-in-templates.yaml`
- Added example Twig fixtures so the template-boundary rule has a concrete allowed/violating path pair:
- `examples/fixtures/templates/order/show.html.twig`
- `examples/fixtures/templates/order/calculate_total.html.twig`
- Added checked-in PHPStan example assets for the native identifier mapping path:
- `examples/decisions/DEC-0005.no-orm-in-order-domain-via-phpstan.yaml`
- `examples/phpstan/no-orm-in-order-domain-report.json`
- `examples/phpstan/README.md`
- `examples/fixtures/phpstan/src/Order/OrderService.php`
- `examples/fixtures/phpstan/src/Infrastructure/Persistence/Doctrine/OrderRecord.php`
Comment on lines +22 to +33

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog references example decision files under .decisions/…, but this PR adds them under examples/decisions/… (e.g. examples/decisions/DEC-0004.no-business-logic-in-templates.yaml and examples/decisions/DEC-0005.no-orm-in-order-domain-via-phpstan.yaml). Updating these paths will avoid confusing users who try to locate the assets described here.

Copilot uses AI. Check for mistakes.
- includes a minimal custom-rule sketch showing how to emit `phpstan.doctrine.orm`
- Added a parallel GitHub Actions example for the PHPStan mapping path:
- `examples/github-actions/phpdecide-phpstan-enforce.yaml`
- The checked-in GitHub Actions example now consumes `enforce --format json`, writes a job summary from the JSON payload, uploads the JSON artifact, and fails the workflow based on the recorded enforcement exit code.
- Added a reusable example markdown renderer for enforcement JSON output:
- `examples/github-actions/render-phpdecide-pr-comment.php`
- Added a second reusable example consumer for enforcement JSON output:
- `examples/github-actions/render-phpdecide-annotations.php`

### Fixed
- Enforcement JSON report loading now normalizes UTF-8 BOM and UTF-16/UTF-32 encoded files, improving compatibility with shell-generated reports on Windows and CI.

### Changed
- Decision `rules.forbid` tokens now have an operational contract for enforcement mapping: analyzer `rule_id` values can be mapped back to decisions through the `enforce` command.

## [1.2.0] - 2026-03-19

### Added
Expand All @@ -30,6 +70,7 @@ This project aims to follow [Keep a Changelog](https://keepachangelog.com/en/1.1
- `PHPDECIDE_AI_OMIT_MODEL` to omit the JSON `model` field when the gateway encodes the model/deployment in the URL.
- `PHPDECIDE_AI_AUTH_HEADER_NAME` and `PHPDECIDE_AI_AUTH_PREFIX` to support non-Bearer authentication (e.g. `Api-Key: <key>`).
- Decision loading cache for `.decisions/*.yaml` to speed up repeated runs when files are unchanged.
This cache applies to decision files in your project's `.decisions/` directory.
- New CLI flag: `explain --no-cache`.
- New env var: `PHPDECIDE_DECISIONS_CACHE=0`.

Expand All @@ -53,17 +94,17 @@ This project aims to follow [Keep a Changelog](https://keepachangelog.com/en/1.1

### Highlights
- First stable release of PHPDecide: decision files as structured, version-controlled project knowledge.
- CI-friendly linting for `.decisions/*.yaml` (syntax + schema checks).
- CI-friendly linting for decision files in your project's `.decisions/` directory (syntax + schema checks).
- “Explain” workflow with optional AI summarization (presentation only; decisions remain the source of truth).

### Added
- CLI command `decisions:lint`
- Validates `.yaml` decision files in a directory (default: `.decisions/`).
- Validates `.yaml` decision files in a directory (default: your project's `.decisions/`).
- `--require-any` option to fail if no `.yaml` files exist.
- Detects unsupported `.yml` extension and reports it explicitly.
- Detects duplicate decision IDs across multiple files.
- CLI command `explain <question>`
- Loads recorded decisions from `.decisions/`.
- Loads recorded decisions from your project's `.decisions/` directory.
- `--path <path>` option to only consider decisions applicable to a specific file path.
- Optional `--ai` mode to summarize recorded decisions.
- `--ai-strict` option to fail if AI is enabled but unavailable/errors (default: falls back to plain output).
Expand All @@ -89,4 +130,5 @@ This project aims to follow [Keep a Changelog](https://keepachangelog.com/en/1.1
[1.0.0]: https://github.com/distvan/phpdecide/releases/tag/v1.0.0
[1.1.0]: https://github.com/distvan/phpdecide/releases/tag/v1.1.0
[1.2.0]: https://github.com/distvan/phpdecide/releases/tag/v1.2.0
[Unreleased]: https://github.com/distvan/phpdecide/compare/v1.2.0...HEAD
[1.3.0]: https://github.com/distvan/phpdecide/releases/tag/v1.3.0
[Unreleased]: https://github.com/distvan/phpdecide/compare/v1.3.0...HEAD
170 changes: 168 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,143 @@ When enforced:

This turns architecture from "guideline" into living constraints.

### First enforcement-ready workflow (`v1.3.0`)

The first enforcement slice does not build a new analyzer.
Instead, PHPDecide maps external analyzer findings back to decision IDs.

Run it with a generic JSON report:

`php ./bin/phpdecide enforce --report build/phpdecide-findings.json`

Emit a machine-readable result for CI or bots:

`php ./bin/phpdecide enforce --report build/phpdecide-findings.json --format json`

Or feed Semgrep output directly:

`semgrep scan --config semgrep/rules --json --output build/semgrep.json`

`php ./bin/phpdecide enforce --semgrep-report build/semgrep.json`

Or feed PHPStan output directly:

`phpstan analyse --error-format=json > build/phpstan.json`

`php ./bin/phpdecide enforce --phpstan-report build/phpstan.json`

Checked-in example assets:

- Example decision: [examples/decisions/DEC-0003.no-orm-in-order-domain.yaml](examples/decisions/DEC-0003.no-orm-in-order-domain.yaml)
- Example decision: [examples/decisions/DEC-0004.no-business-logic-in-templates.yaml](examples/decisions/DEC-0004.no-business-logic-in-templates.yaml)
- Example decision: [examples/decisions/DEC-0005.no-orm-in-order-domain-via-phpstan.yaml](examples/decisions/DEC-0005.no-orm-in-order-domain-via-phpstan.yaml)
- Semgrep rule example: [semgrep/rules/no-orm-in-order-domain.yaml](semgrep/rules/no-orm-in-order-domain.yaml)
- Semgrep rule example: [semgrep/rules/no-business-logic-in-templates.yaml](semgrep/rules/no-business-logic-in-templates.yaml)
- PHPStan report example: [examples/phpstan/no-orm-in-order-domain-report.json](examples/phpstan/no-orm-in-order-domain-report.json)
- PHPStan example notes: [examples/phpstan/README.md](examples/phpstan/README.md)
- GitHub Actions example: [examples/github-actions/phpdecide-semgrep-enforce.yaml](examples/github-actions/phpdecide-semgrep-enforce.yaml)
- GitHub Actions example: [examples/github-actions/phpdecide-phpstan-enforce.yaml](examples/github-actions/phpdecide-phpstan-enforce.yaml)
- PR comment renderer: [examples/github-actions/render-phpdecide-pr-comment.php](examples/github-actions/render-phpdecide-pr-comment.php)
- Annotation renderer: [examples/github-actions/render-phpdecide-annotations.php](examples/github-actions/render-phpdecide-annotations.php)

Matching decision tokens for your own project decisions:

- `doctrine/orm`
- `twig/business-logic`
- `phpstan.doctrine.orm`

Record those tokens in decision files under your own project's `.decisions/` directory.

Sample fixture files used by the second rule:

- Allowed fixture: [examples/fixtures/templates/order/show.html.twig](examples/fixtures/templates/order/show.html.twig)
- Violating fixture: [examples/fixtures/templates/order/calculate_total.html.twig](examples/fixtures/templates/order/calculate_total.html.twig)

Sample fixture files used by the PHPStan example:

- Allowed fixture: [examples/fixtures/phpstan/src/Infrastructure/Persistence/Doctrine/OrderRecord.php](examples/fixtures/phpstan/src/Infrastructure/Persistence/Doctrine/OrderRecord.php)
- Violating fixture: [examples/fixtures/phpstan/src/Order/OrderService.php](examples/fixtures/phpstan/src/Order/OrderService.php)

Supported JSON shapes:

```json
{
"findings": [
{
"tool": "semgrep",
"rule_id": "doctrine/orm",
"path": "src/Order/OrderService.php",
"line": 12,
"severity": "error",
"message": "Doctrine ORM import detected."
}
]
}
```

Or a top-level JSON array of the same finding objects.

Initial mapping behavior:

- active decisions only
- scope must match the finding path
- the finding `rule_id` must equal a token listed in `rules.forbid`

Analyzer report paths are normalized for scope matching, so Windows-style separators and leading `./` do not prevent a finding from matching path-based decision scopes.

This gives CI a stable way to print violations grouped by decision ID without forcing PHPDecide to own static analysis itself.
For Semgrep, PHPDecide reads native `results[*].check_id`, `path`, `start.line`, and `extra.message` / `extra.severity` fields and converts them internally.
For PHPStan, PHPDecide reads native `files[*].messages[*]` entries and maps the message `identifier` field to the decision token in `rules.forbid`.
The repository includes a checked-in native example report at [examples/phpstan/no-orm-in-order-domain-report.json](examples/phpstan/no-orm-in-order-domain-report.json) so teams can copy the JSON shape and token contract directly.
There is also a short authoring note in [examples/phpstan/README.md](examples/phpstan/README.md) showing how a real PHPStan custom rule should emit the same identifier token.

When `--format json` is used, the command emits a structured payload with:

- `ok`
- `summary`
- `violations_by_decision`
- `unmapped_findings`

The `summary` object contains:

- `violating_decision_count`: number of decision IDs that have at least one matched violation
- `violation_count`: total number of matched violations across all decisions
- `unmapped_finding_count`: number of analyzer findings that did not map to an active scoped decision rule

The exit code behavior does not change: matched decision violations still produce a non-zero exit code.

The checked-in GitHub Actions example now demonstrates the full structured path:

- run Semgrep
- call `phpdecide enforce --format json`
- render PR-comment markdown from the JSON payload
- render GitHub annotation commands from the same JSON payload
- write a human-readable step summary from that markdown
- upload the JSON artifact
- upload the markdown artifact
- upload the annotation command artifact
- fail the job if the enforcement exit code was non-zero

There is also a parallel PHPStan-flavored workflow example:

- stage a native PHPStan JSON report
- call `phpdecide enforce --phpstan-report ... --format json`
- reuse the same markdown and annotation renderers
- upload the JSON, markdown, and annotation artifacts
- fail the job if the enforcement exit code was non-zero

See [examples/github-actions/phpdecide-phpstan-enforce.yaml](examples/github-actions/phpdecide-phpstan-enforce.yaml).

The renderer can also be used locally:

`php examples/github-actions/render-phpdecide-pr-comment.php build/phpdecide-enforce.json`

The annotation renderer can also be used locally:

`php examples/github-actions/render-phpdecide-annotations.php build/phpdecide-enforce.json`

Use the PR-comment renderer when you want one grouped markdown summary. Use the annotation renderer when you want file-level warnings/errors that show up inline in GitHub Actions logs and checks.

### Philosophy of AI Usage in PHPDecide

- AI is an assistant, not an Authority
Expand All @@ -123,7 +260,7 @@ PHPDecide is ideal for:

### Quickstart (Phase 1: explain adoption)

1) Create a `.decisions/` folder in your repo root and add your first decision file.
1) Create a `.decisions/` folder in your project root and add your first decision file.

2) Validate decisions locally (fast feedback):

Expand All @@ -138,13 +275,42 @@ PHPDecide is ideal for:

Tip: use [docs/decision-file-anatomy.md](docs/decision-file-anatomy.md) as the schema guide.

### Quickstart (Phase 2: enforcement-ready mapping)

1) Add stable machine-oriented tokens in decision `rules.forbid`.

2) Configure your analyzer to emit matching `rule_id` values.

3) Feed the analyzer report into PHPDecide:

`php ./bin/phpdecide enforce --report build/phpdecide-findings.json`

Or use Semgrep directly:

`php ./bin/phpdecide enforce --semgrep-report build/semgrep.json`

Or use PHPStan directly:

`php ./bin/phpdecide enforce --phpstan-report build/phpstan.json`

For CI integration, prefer:

`php ./bin/phpdecide enforce --semgrep-report build/semgrep.json --format json`

If you want a starting point, this repository includes visible example decision files under [examples/decisions](examples/decisions), plus Semgrep and PHPStan example assets that use the same stable tokens. The GitHub Actions examples stage those decision files into a temporary directory before running `decisions:lint --require-any` and `enforce`, so the workflow stays self-contained. The second Semgrep sample intentionally targets example fixture files under `examples/fixtures/templates/` rather than a real application template directory.

The command fails when it finds decision-linked violations, which makes it suitable for CI.

By default, `enforce` uses the same decision cache as `explain` and may create or refresh `.decisions/.phpdecide-decisions.cache` during a run. Disable cache writes for read-only CI jobs or debugging with either `--no-cache` or `PHPDECIDE_DECISIONS_CACHE=0`.

### Decision loading cache (optional)

By default, `explain` caches parsed decisions in `.decisions/.phpdecide-decisions.cache` to speed up repeated runs.
By default, `explain` and `enforce` cache parsed decisions in `.decisions/.phpdecide-decisions.cache` to speed up repeated runs.

Disable cache if you are debugging loader behavior:

- CLI: `php ./bin/phpdecide explain "Why no ORMs?" --no-cache`
- CLI: `php ./bin/phpdecide enforce --report build/phpdecide-findings.json --no-cache`
- Env: set `PHPDECIDE_DECISIONS_CACHE=0`

### AI configuration (optional)
Expand Down
2 changes: 2 additions & 0 deletions bin/phpdecide
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Console\Application;
use PhpDecide\CLI\ExplainCommand;
use PhpDecide\CLI\DecisionsLintCommand;
use PhpDecide\CLI\EnforceCommand;

$version = 'dev';
try {
Expand All @@ -24,5 +25,6 @@ $application = new Application('PHP Decide', $version);

$application->addCommand(new ExplainCommand());
$application->addCommand(new DecisionsLintCommand());
$application->addCommand(new EnforceCommand());

$application->run();
2 changes: 2 additions & 0 deletions docs/DEC-0003.no-orm-in-order-domain.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ scope:
type: path
paths:
- 'src/Order/*'
- 'src/Order/**/*'

decision:
summary: >
Expand All @@ -28,6 +29,7 @@ examples:
- 'src/Infrastructure/Persistence/Doctrine/*'
forbidden:
- 'src/Order/*'
- 'src/Order/**/*'

rules:
forbid:
Expand Down
Loading
Loading