Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 67 additions & 0 deletions .claude/skills/create-or-update-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: create-or-update-pr
description: Creates or updates the GitHub Pull Request for the current branch, generating title and body from git log and diff.
---

Your task is to create or update the GitHub Pull Request for the current branch.

## Steps

1. Run these commands in parallel to gather context:
- `git log --oneline $(git merge-base HEAD master)..HEAD` — commits on this branch
- `git diff $(git merge-base HEAD master)..HEAD --stat` — changed files summary
- `gh pr view --json number,title,body,state 2>/dev/null` — existing PR, if any

2. Analyze all commits and changed files to understand the intent of the branch.

3. Build the PR title:
- Follow conventional commits: `type(scope): short description`
- Use the most significant type across all commits (`feat` > `fix` > `refactor` > `chore`)
- If there is only one commit and its message is already a good title, use it directly
- Keep it under 72 characters
- Do not add a period at the end

4. Build the PR body using this template:

```
## Summary
- <bullet 1>
- <bullet 2>

## Changes
- <file or area>: <what changed and why>

## Test plan
- [ ] <step to verify>
```

- Summary: 2–4 bullets on _what_ and _why_, not _how_
- Changes: one line per meaningful file group or area changed
- Test plan: concrete steps a reviewer can follow to verify the PR

5. If no PR exists for this branch, create one targeting `master`:

```bash
gh pr create --title "..." --body "$(cat <<'EOF'
...
EOF
)"
```

6. If a PR already exists, update its title and body:

```bash
gh pr edit --title "..." --body "$(cat <<'EOF'
...
EOF
)"
```

7. Output the PR URL at the end.

## Rules

- Never push commits — only create or edit the PR metadata
- If the branch has no commits ahead of master, tell the user and stop
- Use the `gh` CLI for all GitHub operations
- Always use a HEREDOC (`<<'EOF'`) to pass the body so newlines are preserved
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ["8.1", "8.5"]

steps:
- uses: actions/checkout@v6

- name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: xdebug

- name: Validate composer.json and composer.lock
run: composer validate

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Check code style
run: composer run-script check-format

- name: Run test suite
run: composer run-script test

- name: Upload coverage to Codecov
if: matrix.php-version == '8.5'
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
30 changes: 0 additions & 30 deletions .github/workflows/php.yml

This file was deleted.

43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release

on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [master]

permissions:
contents: write
issues: write
pull-requests: write

jobs:
release:
name: Semantic Release
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Semantic Release
id: release
uses: cycjimmy/semantic-release-action@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
extra_plugins: |
@semantic-release/changelog@6
@semantic-release/git@10
conventional-changelog-conventionalcommits@7

- name: Notify Packagist
if: steps.release.outputs.new_release_published == 'true'
run: |
curl -s -XPOST \
-H 'content-type: application/json' \
"https://packagist.org/api/update-package?username=${{ secrets.PACKAGIST_USERNAME }}&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
-d '{"repository":{"url":"https://github.com/${{ github.repository }}"}}'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.idea
/vendor
coverage.xml
.claude/settings.local.json
.php-cs-fixer.cache
10 changes: 10 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

$finder = PhpCsFixer\Finder::create()
->in([__DIR__ . '/src', __DIR__ . '/tests']);

return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
])
->setFinder($finder);
42 changes: 42 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"branches": ["master"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "revert", "release": "patch" },
{ "type": "docs", "release": "patch" },
{ "type": "chore", "release": "patch" },
{ "type": "refactor", "release": "minor" },
{ "type": "test", "release": "patch" },
{ "type": "ci", "release": "patch" }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```sh
composer install # install dev deps (phpunit 5, phpcs)
composer test # run phpunit with coverage-text
composer check-format # phpcs PSR2 lint over src/
composer format # phpcbf PSR2 autofix over src/ and tests/

vendor/bin/phpunit tests/TreeWalkerTest.php # run a single test file
vendor/bin/phpunit --filter testSimpleStructs # run a single test method
```

CI (`.github/workflows/php.yml`) runs `composer test` on push and uploads `coverage.xml` to Codecov. PHP >= 5.5 is the supported floor.

## Architecture

Single-class library: `src/TreeWalker.php` (no namespace; PSR-4 autoload maps `""` → `src/`). Tests live in `tests/`; an interactive demo is in `example/index.php`.

The public API operates on three interchangeable structure shapes — JSON string, `\stdClass` object, or associative array — configured per-instance via the constructor's `returntype` option (`"jsonstring" | "object" | "array"`) plus a `debug` flag that appends a `time` key to outputs.

Two pieces of internal machinery are load-bearing and worth understanding before editing any public method:

1. **`studyType(&$struct, &$problem)`** is called at the top of every public method. It normalizes the input in place to an associative array (decoding JSON strings, casting objects), or returns `false` with an error message. Public methods always work in array-space internally.

2. **`returnTypeConvert($struct)`** is the matching exit converter, encoding the result back to whatever `returntype` was configured. New public methods must funnel through this — never return raw arrays.

The diff/merge implementation uses a **path-flattening** strategy rather than recursive structural comparison:

- `structPathArray()` walks a nested structure and produces a flat associative array keyed by slash-delimited paths (e.g. `"cafeina/ss/ff" => 21`).
- `getdiff()` and `structMerge()` both flatten both inputs, then operate on the flat maps (`structPathArrayDiff`, `array_merge`).
- `pathSlashToStruct()` is the inverse — it re-nests slash-keys back into a tree, and is gated by the `$slashtoobject` parameter on `getdiff` / `structMerge`. Note the README examples pass `true` to mean "no slashes / nested output" — the boolean naming is counter-intuitive.
- `pathSlashToStruct()` calls `createDynamicallyObjects()` + `setDynamicallyValue()` internally; those helpers depend on `returntype` being `"array"`, so it temporarily flips the config via `switchType()` and flips it back at the end. Anything that calls these helpers from a non-array return-type context must do the same dance.

`walker()` is the only method that does true recursive in-place mutation (callback receives `&$struct, $key, &$value`); it does not go through the path-flattening pipeline.
Loading
Loading