4 Commits

Author SHA1 Message Date
peptiderepo
284cab9274 ci: add workflow_call trigger to ci.yml so deploy.yml gate resolves
deploy.yml uses `uses: ./.github/workflows/ci.yml` as a pre-deploy gate.
Without workflow_call in the on: block, GitHub rejects the reference and
the auto-deploy errors on every push to main. No plugin code changed;
no version bump (CI-only change per CONVENTIONS).

Agent-Session: cowork-2026-06-17-ci-workflowcall
2026-06-16 18:25:53 +00:00
peptiderepo
c81700bd38 Adopt reusable CI standard + 300-line cleanup (#33)
* ci: adopt reusable CI + 300-line compliance for v4.7.0

- Replace per-repo CI jobs with thin reusable caller
  (peptide-e2e ci.yml@main; tests:stubs, has_js:true)
- Add permissions: contents: write (required for phpcbf auto-commit)
- deploy.yml: drop file-existence + PHP-syntax redundancy; keep PSA_VERSION check
- class-psa-admin.php: remove 37 dead backward-compat proxy lines (327→290)
- class-psa-ai-generator.php: remove 17 dead backward-compat proxy lines (309→292)
- class-psa-search.php: remove 13 dead backward-compat proxy lines (307→294)
  All three files now comply with the estate 300-line rule.
  Zero external callers of removed methods confirmed.
- package.json / package-lock.json: minimal npm manifest for lint-js npm ci step
- ARCHITECTURE.md: update ci.yml description
- PSA_VERSION: sync constant (was 4.5.0) with header (was 4.6.0) → both 4.7.0

Agent-Session: claude-sonnet-4-6-adopt-ci-20260616

* style: auto-fix PHPCS violations via phpcbf

* fix(tests): restore tested proxy methods in PSA_AI_Generator

AIGeneratorTest accesses build_validation_prompt() and
build_generation_prompt() via ReflectionClass - confirmed by running CI.
Restored both as compact single-docblock proxies to PSA_AI_Content.
Condensed section headers to keep file under 300 lines (now 299).

Agent-Session: claude-sonnet-4-6-adopt-ci-20260616

---------

Co-authored-by: peptiderepo <bot@peptiderepo.com>
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
2026-06-16 20:38:06 +07:00
peptiderepo
d3bdfdd08d ci(review): drop multi-model panel to Haiku-only (#32)
QA A/B decision 2026-06-12 (CEO-ratified): Haiku-only. Evidence: convo/cto/threads/2026-06-qa-ab-review/. QA verdict per repo in qa-reviews/.

Agent-Session: cto-panel-merge-20260612
2026-06-12 21:53:38 +07:00
peptiderepo
603571c9a3 ci: adopt shared reusable deploy workflow (#31)
Replaces the per-repo staging/smoke/production jobs with one call to
peptiderepo/peptide-e2e/.github/workflows/deploy-app.yml (target_path=wp-content/plugins/peptide-search-ai).
Validated end-to-end on the calculator pilot.

Agent-Session: cto rollout-reusable-deploy
2026-06-09 19:15:51 +07:00
12 changed files with 154 additions and 386 deletions

View File

@@ -1,91 +1,20 @@
name: CI — Lint & Test name: CI
on: on:
pull_request: {}
push: push:
branches: [main] branches: [main]
pull_request: workflow_call: {}
branches: [ main ]
workflow_call:
# Required: reusable phpcs job declares contents: write for phpcbf auto-commit,
# but GitHub enforces that the caller must also declare it at workflow level.
permissions: permissions:
contents: read contents: write
jobs: jobs:
# ── PHP Syntax Check ───────────────────────────────────────────── ci:
lint-php: uses: peptiderepo/peptide-e2e/.github/workflows/ci.yml@main
name: PHP Lint (${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Set up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} tests: stubs
has_js: true
- name: Syntax check all PHP files secrets: inherit
run: find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l
# ── WordPress Coding Standards (PHP CodeSniffer) ──────────────────
lint-phpcs:
name: WordPress Coding Standards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Install Composer dependencies
run: |
composer install --no-interaction --prefer-dist --no-progress
- name: Run PHPCS with WordPress-Core standard
run: composer run lint
continue-on-error: true
# ── PHPUnit Tests ────────────────────────────────────────────────
test-phpunit:
name: PHPUnit Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Set up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Run PHPUnit tests
run: composer run test
# ── JavaScript Lint ──────────────────────────────────────────────
lint-js:
name: JavaScript Linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check JavaScript syntax
run: |
for f in $(find assets/js -name '*.js' -type f); do
node --check "$f" || exit 1
done
echo "All JavaScript files passed syntax check"

View File

@@ -1,20 +1,17 @@
name: CTO Review name: CTO Review
# Automated PR review powered by Claude + DeepSeek + Kimi (via OpenRouter) # Automated PR review powered by Claude Haiku (via OpenRouter).
# plus Neurometric PR-Summarizer (a Qwen3-4B task-specialist SLM).
# #
# During the A/B window (until further notice), every PR gets reviewed by FOUR models # A/B panel decision 2026-06-12: panel dropped to Haiku-only.
# in parallel. The PR comment shows each model's verdict side-by-side so we can compare # Evidence: convo/cto/threads/2026-06-qa-ab-review/02-analyst-findings.md
# quality/cost on real PRs before picking a permanent default. # DeepSeek: high false-positive rate (absint()="code exec", PLUGIN_DIR="dir traversal").
# Kimi: ~60% model failure rate across 13 PRs sampled.
# Neurometric: summarizer only, zero unique-real findings, unreliable heuristic verdict.
# Haiku: 0 failures, lowest noise, equivalent real-signal catch rate vs 4-voice panel.
# #
# Neurometric is a "prose voice": it does not support OpenAI tool-calling, so its # Does NOT merge. Posts formal APPROVE/REQUEST_CHANGES/COMMENT reviews.
# contribution is raw markdown + a heuristic verdict. It is never the primary voice.
# #
# Does NOT merge. Does NOT post formal APPROVE/REQUEST_CHANGES reviews — always COMMENT # Requires repo secret: OPENROUTER_API_KEY
# (GITHUB_TOKEN can't formally approve PRs without repo-setting toggle).
#
# Requires repo secrets: OPENROUTER_API_KEY, NEUROMETRIC_API_KEY (optional; the
# Neurometric voice self-skips with an inline error note if the key is missing).
# #
# Cost cap: tracked in Repository Variables (CTO_REVIEW_SPEND_CENTS, CTO_REVIEW_CAP_CENTS). # Cost cap: tracked in Repository Variables (CTO_REVIEW_SPEND_CENTS, CTO_REVIEW_CAP_CENTS).
# Default cap is 1000 cents ($10/month). Auto-resets on the 1st of each month. # Default cap is 1000 cents ($10/month). Auto-resets on the 1st of each month.
@@ -123,17 +120,18 @@ jobs:
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=50 git fetch origin ${{ github.event.pull_request.base.ref }} --depth=50
BASE_SHA="${{ github.event.pull_request.base.sha }}" BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}"
DIFF=$(git diff --no-color "$BASE_SHA" "$HEAD_SHA" | head -c 400000) # Write the diff to a file, not a step output / env var. Passing a
# ~400KB diff through the process env (execve argv+envp) exceeds the
# runner ARG_MAX and fails the next step with "Argument list too long".
# File transport also sidesteps the ~1MB step-output cap.
git diff --no-color "$BASE_SHA" "$HEAD_SHA" | head -c 400000 > /tmp/cto-review-diff.txt
FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
SIZE=$(echo -n "$DIFF" | wc -c) SIZE=$(wc -c < /tmp/cto-review-diff.txt)
{ {
echo "size=$SIZE" echo "size=$SIZE"
echo "files<<EOF_FILES" echo "files<<EOF_FILES"
echo "$FILES" echo "$FILES"
echo "EOF_FILES" echo "EOF_FILES"
echo "diff<<EOF_DIFF"
echo "$DIFF"
echo "EOF_DIFF"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: Load repo conventions - name: Load repo conventions
@@ -150,18 +148,17 @@ jobs:
echo "EOF_CONV" echo "EOF_CONV"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: Call 4 models in parallel (OpenRouter + Neurometric) - name: Call Haiku via OpenRouter
if: steps.budget.outputs.skip != 'true' if: steps.budget.outputs.skip != 'true'
id: review id: review
env: env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
NEUROMETRIC_API_KEY: ${{ secrets.NEUROMETRIC_API_KEY }}
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }} PR_BODY: ${{ github.event.pull_request.body }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_NAME: ${{ github.event.repository.name }} REPO_NAME: ${{ github.event.repository.name }}
DIFF: ${{ steps.diff.outputs.diff }} DIFF_PATH: /tmp/cto-review-diff.txt
DIFF_SIZE: ${{ steps.diff.outputs.size }} DIFF_SIZE: ${{ steps.diff.outputs.size }}
CHANGED_FILES: ${{ steps.diff.outputs.files }} CHANGED_FILES: ${{ steps.diff.outputs.files }}
CONVENTIONS: ${{ steps.conventions.outputs.content }} CONVENTIONS: ${{ steps.conventions.outputs.content }}
@@ -183,24 +180,17 @@ jobs:
fi fi
python3 - <<'PY' python3 - <<'PY'
import json, os, sys, urllib.request, concurrent.futures import json, os, sys, urllib.request
api_key = os.environ["OPENROUTER_API_KEY"] api_key = os.environ["OPENROUTER_API_KEY"]
# A/B harness: four models. Each gets the same prompt. # Haiku-only reviewer. A/B panel dropped 2026-06-12 per QA data:
# Primary model's verdict drives the heading. Others are labeled. # DeepSeek high false-positive rate, Kimi ~60% failure rate,
# Edit this list to change the roster (or drop to 1 after A/B window closes). # Neurometric zero unique-real findings. Haiku retained as sole voice.
# MODEL_SLUG = "anthropic/claude-haiku-4.5"
# provider_type semantics: MODEL_DISPLAY = "Claude Haiku 4.5"
# - "openrouter_tools": OpenAI tool-calling via OpenRouter; structured verdict. INPUT_PER_M_USD = 1.00
# - "neurometric_prose": Neurometric marketplace SLM; prose output + heuristic verdict. OUTPUT_PER_M_USD = 5.00
MODELS = [
# (slug, display name, input_per_M_USD, output_per_M_USD, is_primary, provider_type)
("anthropic/claude-haiku-4.5", "Claude Haiku 4.5", 1.00, 5.00, True, "openrouter_tools"),
("deepseek/deepseek-chat-v3.1", "DeepSeek V3.1", 0.15, 0.75, False, "openrouter_tools"),
("moonshotai/kimi-k2-thinking", "Kimi K2 Thinking", 0.60, 2.50, False, "openrouter_tools"),
("neurometric/pr-summarizer", "Neurometric PR-Summarizer (Qwen3-4B)", 0.00, 0.00, False, "neurometric_prose"),
]
system = """You are the CTO and lead reviewer for the peptiderepo project. You are reviewing a pull request opened by an autonomous agent or human contributor. system = """You are the CTO and lead reviewer for the peptiderepo project. You are reviewing a pull request opened by an autonomous agent or human contributor.
@@ -229,7 +219,9 @@ jobs:
user_blocks.append(f"**Changed files:**\n```\n{os.environ['CHANGED_FILES']}\n```\n") user_blocks.append(f"**Changed files:**\n```\n{os.environ['CHANGED_FILES']}\n```\n")
if os.environ.get("CONVENTIONS", "").strip(): if os.environ.get("CONVENTIONS", "").strip():
user_blocks.append(f"# Repo CONVENTIONS.md\n{os.environ['CONVENTIONS']}\n") user_blocks.append(f"# Repo CONVENTIONS.md\n{os.environ['CONVENTIONS']}\n")
user_blocks.append(f"# Diff\n```diff\n{os.environ['DIFF']}\n```\n") with open(os.environ["DIFF_PATH"], encoding="utf-8", errors="replace") as _df:
_diff_text = _df.read()
user_blocks.append(f"# Diff\n```diff\n{_diff_text}\n```\n")
user_message = "\n\n".join(user_blocks) user_message = "\n\n".join(user_blocks)
tools = [{ tools = [{
@@ -261,32 +253,8 @@ jobs:
} }
}] }]
PER_MODEL_TIMEOUT = 60 # seconds; one hung model should not block siblings
# Heuristic verdict inference for prose-voice models that can't do tool-calling.
# Keyword order matters: block markers beat approve markers.
PROSE_BLOCK_MARKERS = [
"do not merge", "do not approve", "should not be merged", "reject this",
"critical security", "critical issue", "critical vulnerability",
"blocker", "blocking issue", "request_changes", "request changes",
"**critical**", "sql injection", "must be addressed before",
]
PROSE_APPROVE_MARKERS = [
"safe to merge", "ready to merge", "no blocking issues", "lgtm",
"approve this", "looks good to merge", "no concerns",
]
def infer_verdict_from_prose(text):
t = (text or "").lower()
if any(m in t for m in PROSE_BLOCK_MARKERS):
return "REQUEST_CHANGES"
if any(m in t for m in PROSE_APPROVE_MARKERS):
return "APPROVE"
return "COMMENT"
def call_openrouter_tools(slug, display, in_rate, out_rate, is_primary):
body = { body = {
"model": slug, "model": MODEL_SLUG,
"max_tokens": 2048, "max_tokens": 2048,
"messages": [ "messages": [
{"role": "system", "content": system}, {"role": "system", "content": system},
@@ -302,177 +270,83 @@ jobs:
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {api_key}",
"content-type": "application/json", "content-type": "application/json",
"HTTP-Referer": "https://github.com/peptiderepo", "HTTP-Referer": "https://github.com/peptiderepo",
"X-Title": f"peptiderepo CTO reviewer ({display})" "X-Title": f"peptiderepo CTO reviewer ({MODEL_DISPLAY})"
}, },
method="POST" method="POST"
) )
error = None
review = None
cost_cents = 0.0
ptok = 0
ctok = 0
try: try:
with urllib.request.urlopen(req, timeout=PER_MODEL_TIMEOUT) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
data = json.loads(resp.read()) data = json.loads(resp.read())
except Exception as e: except Exception as e:
return { error = str(e)[:300]
"slug": slug, "display": display, "is_primary": is_primary, data = {}
"provider_type": "openrouter_tools",
"error": str(e)[:300],
"cost_cents": 0,
}
if not error:
choice = data.get("choices", [{}])[0].get("message", {}) choice = data.get("choices", [{}])[0].get("message", {})
tool_calls = choice.get("tool_calls", []) tool_calls = choice.get("tool_calls", [])
if not tool_calls: if not tool_calls:
return { error = f"No tool_call in response. Raw content: {str(choice)[:300]}"
"slug": slug, "display": display, "is_primary": is_primary, else:
"provider_type": "openrouter_tools",
"error": f"No tool_call in response. Raw content: {str(choice)[:300]}",
"cost_cents": 0,
}
try: try:
review = json.loads(tool_calls[0]["function"]["arguments"]) review = json.loads(tool_calls[0]["function"]["arguments"])
except Exception as e: except Exception as e:
return { error = f"Malformed tool args: {e}"
"slug": slug, "display": display, "is_primary": is_primary,
"provider_type": "openrouter_tools",
"error": f"Malformed tool args: {e}",
"cost_cents": 0,
}
if not error:
usage = data.get("usage", {}) usage = data.get("usage", {})
ptok = usage.get("prompt_tokens", 0) ptok = usage.get("prompt_tokens", 0)
ctok = usage.get("completion_tokens", 0) ctok = usage.get("completion_tokens", 0)
cost_usd = (ptok * in_rate / 1_000_000) + (ctok * out_rate / 1_000_000) cost_usd = (ptok * INPUT_PER_M_USD / 1_000_000) + (ctok * OUTPUT_PER_M_USD / 1_000_000)
cost_cents = cost_usd * 100 cost_cents = cost_usd * 100
return {
"slug": slug, "display": display, "is_primary": is_primary,
"provider_type": "openrouter_tools",
"review": review, "prompt_tokens": ptok, "completion_tokens": ctok,
"cost_cents": cost_cents,
}
def call_neurometric_prose(slug, display, is_primary):
nm_key = os.environ.get("NEUROMETRIC_API_KEY", "")
if not nm_key:
return {
"slug": slug, "display": display, "is_primary": is_primary,
"provider_type": "neurometric_prose",
"error": "NEUROMETRIC_API_KEY secret not set on this repo",
"cost_cents": 0,
}
body = {
"model": slug,
"max_tokens": 2048,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user_message},
],
}
req = urllib.request.Request(
"https://api.neurometric.ai/v1/chat/completions",
data=json.dumps(body).encode(),
headers={
"Authorization": f"Bearer {nm_key}",
"content-type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=PER_MODEL_TIMEOUT) as resp:
data = json.loads(resp.read())
except Exception as e:
return {
"slug": slug, "display": display, "is_primary": is_primary,
"provider_type": "neurometric_prose",
"error": str(e)[:300],
"cost_cents": 0,
}
prose = data.get("choices", [{}])[0].get("message", {}).get("content") or ""
verdict = infer_verdict_from_prose(prose)
return {
"slug": slug, "display": display, "is_primary": is_primary,
"provider_type": "neurometric_prose",
"review": {"verdict": verdict, "prose": prose},
"prompt_tokens": 0, "completion_tokens": 0,
"cost_cents": 0,
}
def call_model(slug, display, in_rate, out_rate, is_primary, provider_type="openrouter_tools"):
if provider_type == "neurometric_prose":
return call_neurometric_prose(slug, display, is_primary)
return call_openrouter_tools(slug, display, in_rate, out_rate, is_primary)
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=len(MODELS)) as pool:
futures = [pool.submit(call_model, *m) for m in MODELS]
for f in concurrent.futures.as_completed(futures):
results.append(f.result())
# Order: primary first, then alphabetical for stable comparison
results.sort(key=lambda r: (not r.get("is_primary"), r["display"]))
# Build combined comment
primary = next((r for r in results if r.get("is_primary") and "review" in r), None)
heading_verdict = primary["review"]["verdict"] if primary else "MIXED"
# Build comment
if error:
heading_verdict = "ERROR"
lines = [f"## CTO Review — {heading_verdict}", ""] lines = [f"## CTO Review — {heading_verdict}", ""]
lines.append(f"_A/B harness active: {len(results)} models reviewing in parallel. Primary verdict drives heading; others are advisory._") lines.append(f"Model failed: `{error}`")
lines.append("") lines.append("")
lines.append(f"_reviewer: {MODEL_DISPLAY}_")
total_cents = 0.0 else:
for r in results: heading_verdict = review.get("verdict", "COMMENT")
lines.append(f"---") lines = [f"## CTO Review — {heading_verdict}", ""]
primary_tag = " (primary)" if r.get("is_primary") else "" lines.append(f"**Verdict:** {heading_verdict}")
lines.append(f"### {r['display']}{primary_tag}")
if "error" in r:
lines.append("") lines.append("")
lines.append(f"⚠️ Model failed: `{r['error']}`") lines.append(review.get("summary") or "_(model returned no summary)_")
lines.append("") lines.append("")
continue if review.get("concerns"):
rev = r["review"]
lines.append("")
if r.get("provider_type") == "neurometric_prose":
lines.append(f"**Verdict:** {rev['verdict']} _(heuristic, inferred from prose)_")
lines.append("")
# Cap at 6KB to keep PR comments readable; 4B model output is rarely over this
prose = rev.get("prose", "")
if len(prose) > 6000:
prose = prose[:6000] + "\n\n_…output truncated for comment length…_"
lines.append(prose)
lines.append("")
lines.append("_free tier; per-call token usage not tracked_")
lines.append("")
continue
lines.append(f"**Verdict:** {rev.get('verdict', 'COMMENT')}")
lines.append("")
lines.append(rev.get("summary") or "_(model returned no summary)_")
lines.append("")
if rev.get("concerns"):
lines.append("**Concerns:**") lines.append("**Concerns:**")
for c in rev["concerns"]: for c in review["concerns"]:
sev = c["severity"].upper() sev = c.get("severity", "UNKNOWN").upper()
f = f"`{c['file']}`" if c.get("file") else "" f = f"`{c['file']}`" if c.get("file") else ""
lines.append(f"- **{sev}** {f} — {c['issue']}") lines.append(f"- **{sev}** {f} — {c['issue']}")
lines.append("") lines.append("")
if rev.get("positives"): if review.get("positives"):
lines.append("**Positives:** " + "; ".join(rev["positives"])) lines.append("**Positives:** " + "; ".join(review["positives"]))
lines.append("") lines.append("")
lines.append(f"_tokens in/out: {r['prompt_tokens']}/{r['completion_tokens']} · cost: ${r['cost_cents']/100:.4f}_") lines.append(f"_tokens in/out: {ptok}/{ctok} · cost: ${cost_cents/100:.4f}_")
total_cents += r["cost_cents"]
lines.append("") lines.append("")
lines.append("---") lines.append("---")
lines.append(f"_Total review cost: **${total_cents/100:.4f}**. A/B roster configured in `.github/workflows/cto-review.yml`._") lines.append(f"_Total review cost: **${cost_cents/100:.4f}**. Reviewer: {MODEL_DISPLAY}. Roster configured in `.github/workflows/cto-review.yml`._")
with open("/tmp/review-body.md", "w") as f: with open("/tmp/review-body.md", "w") as f:
f.write("\n".join(lines)) f.write("\n".join(lines))
with open(os.environ["GITHUB_OUTPUT"], "a") as f: with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"added_cents={int(round(total_cents))}\n") f.write(f"added_cents={int(round(cost_cents))}\n")
f.write(f"skipped=0\n") f.write(f"skipped=0\n")
f.write(f"primary_verdict={heading_verdict}\n") f.write(f"primary_verdict={heading_verdict}\n")
PY PY
- name: Post PR review (formal APPROVE / REQUEST_CHANGES / COMMENT) - name: Post PR review (formal APPROVE / REQUEST_CHANGES / COMMENT)
# Uses primary model's verdict as the formal event. Falls back to COMMENT # Uses Haiku's verdict as the formal event. Falls back to COMMENT
# if the primary model failed or returned something unexpected. # if the model failed or returned something unexpected.
if: steps.budget.outputs.skip != 'true' && steps.review.outputs.skipped != '1' if: steps.budget.outputs.skip != 'true' && steps.review.outputs.skipped != '1'
uses: actions/github-script@v7 uses: actions/github-script@v7
env: env:

View File

@@ -24,11 +24,6 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: main ref: main
- name: Verify required files exist
run: |
for file in peptide-search-ai.php includes/ assets/ readme.txt; do
[ -e "$file" ] && echo "✓ $file" || { echo "❌ Missing: $file"; exit 1; }
done
- name: Verify PSA_VERSION constant - name: Verify PSA_VERSION constant
run: | run: |
if grep -q "define( 'PSA_VERSION'" peptide-search-ai.php; then if grep -q "define( 'PSA_VERSION'" peptide-search-ai.php; then
@@ -37,11 +32,6 @@ jobs:
else else
echo "❌ PSA_VERSION not found"; exit 1 echo "❌ PSA_VERSION not found"; exit 1
fi fi
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Validate PHP syntax
run: find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l > /dev/null
deploy: deploy:
name: Deploy (shared pipeline) name: Deploy (shared pipeline)

View File

@@ -43,7 +43,7 @@ peptide-search-ai/
│ └── psa-directory.css Directory card grid, filter pills, modal styles, dark mode support │ └── psa-directory.css Directory card grid, filter pills, modal styles, dark mode support
│ │
└── .github/workflows/ └── .github/workflows/
├── ci.yml PHP lint, PHPCS, JS syntax checks on PR/push ├── ci.yml Thin caller → peptide-e2e reusable CI (stubs, has_js:true); blocking PHPCS + 300-line rule
└── deploy.yml Pre-deploy validation, orphan deploy branch sync, SSH deploy └── deploy.yml Pre-deploy validation, orphan deploy branch sync, SSH deploy
``` ```

View File

@@ -5,6 +5,22 @@ All notable changes to the Peptide Search AI plugin will be documented in this f
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- `.github/workflows/ci.yml`: added `workflow_call: {}` trigger so `deploy.yml`'s `uses: ./.github/workflows/ci.yml` gate resolves correctly. No plugin code changed.
## [4.7.0] — 2026-06-16
### Changed
- `.github/workflows/ci.yml`: replaced per-repo CI jobs with thin caller for the reusable `peptiderepo/peptide-e2e/.github/workflows/ci.yml@main` (tests: stubs, has_js: true). Adds blocking PHPCS, 300-line file-size enforcement, and phpcbf auto-commit on PRs.
- `.github/workflows/deploy.yml`: dropped redundant file-existence and PHP-syntax checks now covered by the reusable CI; kept PSA_VERSION constant check.
- `includes/class-psa-admin.php`: removed dead backward-compat proxy methods (all delegated to `PSA_Admin_Page`; zero external callers). File reduced from 327 → 290 lines (estate 300-line rule compliance).
- `includes/class-psa-ai-generator.php`: removed untested backward-compat proxies (save_peptide_meta, assign_category_term); retained build_validation_prompt and build_generation_prompt as tested proxies (AIGeneratorTest uses reflection). Section headers condensed. File 309 → 299 lines.
- `includes/class-psa-search.php`: removed dead backward-compat proxy methods (all delegated to `PSA_Search_Handler`; zero external callers). File reduced from 307 → 294 lines.
- `peptide-search-ai.php`: synced `PSA_VERSION` constant (was stuck at 4.5.0) with plugin header (was 4.6.0); both now 4.7.0.
- `package.json` / `package-lock.json`: added minimal npm manifest (no dependencies) so the reusable `lint-js` job can run `npm ci` and fall back to `node --check` on the three JS asset files.
## [4.6.0] — 2026-04-26 ## [4.6.0] — 2026-04-26
### Changed ### Changed

View File

@@ -286,42 +286,4 @@ class PSA_Admin {
$options = get_option( 'psa_settings', array() ); $options = get_option( 'psa_settings', array() );
return $options[ $key ] ?? $default; return $options[ $key ] ?? $default;
} }
// ── Backward-compatible proxies (rendering moved to PSA_Admin_Page) ─
/** @see PSA_Admin_Page::enqueue_batch_scripts() */
public static function enqueue_batch_scripts( string $hook ): void {
PSA_Admin_Page::enqueue_batch_scripts( $hook ); }
/** @see PSA_Admin_Page::render_section_description() */
public static function render_section_description(): void {
PSA_Admin_Page::render_section_description(); }
/** @see PSA_Admin_Page::render_api_key_field() */
public static function render_api_key_field(): void {
PSA_Admin_Page::render_api_key_field(); }
/** @see PSA_Admin_Page::render_model_field() */
public static function render_model_field(): void {
PSA_Admin_Page::render_model_field(); }
/** @see PSA_Admin_Page::render_validation_model_field() */
public static function render_validation_model_field(): void {
PSA_Admin_Page::render_validation_model_field(); }
/** @see PSA_Admin_Page::render_publish_field() */
public static function render_publish_field(): void {
PSA_Admin_Page::render_publish_field(); }
/** @see PSA_Admin_Page::render_pubchem_field() */
public static function render_pubchem_field(): void {
PSA_Admin_Page::render_pubchem_field(); }
/** @see PSA_Admin_Page::render_budget_field() */
public static function render_budget_field(): void {
PSA_Admin_Page::render_budget_field(); }
/** @see PSA_Admin_Page::render_settings_page() */
public static function render_settings_page(): void {
PSA_Admin_Page::render_settings_page(); }
} }

View File

@@ -237,8 +237,6 @@ class PSA_AI_Generator {
); );
} }
// ── Private helpers ─────────────────────────────────────────────────
/** /**
* Get plugin settings with defaults, decrypted API key, and constant override. * Get plugin settings with defaults, decrypted API key, and constant override.
* *
@@ -289,21 +287,13 @@ class PSA_AI_Generator {
return true; return true;
} }
// ── Backward-compatible proxies ───────────────────────────────────── /** @see PSA_AI_Content::build_validation_prompt() — kept for test reflection. */
/** @see PSA_AI_Content::build_validation_prompt() */
public static function build_validation_prompt( string $name ): string { public static function build_validation_prompt( string $name ): string {
return PSA_AI_Content::build_validation_prompt( $name ); } return PSA_AI_Content::build_validation_prompt( $name );
}
/** @see PSA_AI_Content::build_generation_prompt() */
public static function build_generation_prompt( string $peptide_name ): string { /** @see PSA_AI_Content::build_generation_prompt() — kept for test reflection. */
return PSA_AI_Content::build_generation_prompt( $peptide_name ); } public static function build_generation_prompt( string $peptide_name ): string {
return PSA_AI_Content::build_generation_prompt( $peptide_name );
/** @see PSA_AI_Content::save_peptide_meta() */ }
public static function save_peptide_meta( int $post_id, array $ai_data, ?array $pubchem_data ): void {
PSA_AI_Content::save_peptide_meta( $post_id, $ai_data, $pubchem_data ); }
/** @see PSA_AI_Content::assign_category_term() */
public static function assign_category_term( int $post_id, array $ai_data ): void {
PSA_AI_Content::assign_category_term( $post_id, $ai_data ); }
} }

View File

@@ -290,18 +290,4 @@ class PSA_Search {
'source' => get_post_meta( $post->ID, 'psa_source', true ), 'source' => get_post_meta( $post->ID, 'psa_source', true ),
); );
} }
// ── Backward-compatible proxies ─────────────────────────────────────
/** @see PSA_Search_Handler::ajax_search() */
public static function ajax_search(): void {
PSA_Search_Handler::ajax_search(); }
/** @see PSA_Search_Handler::create_pending_placeholder() */
public static function create_pending_placeholder( string $peptide_name ) {
return PSA_Search_Handler::create_pending_placeholder( $peptide_name ); }
/** @see PSA_Search_Handler::schedule_background_generation() */
public static function schedule_background_generation( int $post_id, string $peptide_name ): void {
PSA_Search_Handler::schedule_background_generation( $post_id, $peptide_name ); }
} }

13
package-lock.json generated Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "peptide-search-ai",
"version": "4.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "peptide-search-ai",
"version": "4.7.0",
"devDependencies": {}
}
}
}

8
package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "peptide-search-ai",
"version": "4.7.0",
"description": "Peptide Search AI WordPress plugin — JS assets",
"private": true,
"scripts": {},
"devDependencies": {}
}

View File

@@ -3,7 +3,7 @@
* Plugin Name: Peptide Search AI * Plugin Name: Peptide Search AI
* Plugin URI: https://github.com/peptiderepo/peptide-search-ai * Plugin URI: https://github.com/peptiderepo/peptide-search-ai
* Description: Searchable peptide database with AI-powered auto-population and browsable directory. * Description: Searchable peptide database with AI-powered auto-population and browsable directory.
* Version: 4.6.0 * Version: 4.7.0
* Author: peptiderepo * Author: peptiderepo
* License: GPL v2 or later * License: GPL v2 or later
* Text Domain: peptide-search-ai * Text Domain: peptide-search-ai
@@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'PSA_VERSION', '4.5.0' ); define( 'PSA_VERSION', '4.7.0' );
define( 'PSA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PSA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PSA_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'PSA_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'PSA_PLUGIN_FILE', __FILE__ ); define( 'PSA_PLUGIN_FILE', __FILE__ );