Skip to content

1.9.79: redirector create, update, delete and whole-rule-set writes in the MCP connector - #265

Merged
tracking202 merged 15 commits into
1.9.79from
1.9.79-mcp-rotator-crud
Oct 28, 2026
Merged

tracking202 merged 15 commits into
1.9.79from
1.9.79-mcp-rotator-crud

Conversation

@tracking202

Copy link
Copy Markdown
Owner

Backlog: BDY6

What changed and why

An agent helping someone set up a redirector could read one through MCP (rotator_list, rotator_get, rotator_stats) but could not make or change one. The owner asked for whatever is easiest and most powerful for an agent to use. This PR builds the coordinator's reading of that: one declarative call for the whole rule set, plus create, update and delete for the redirector. There are no per-rule MCP tools.

  • PUT /api/v3/rotators/{id}/rules (RotatorsController::setRules()). The body is {"rules": [...]}, each rule in the shape GET /rotators/{id}/rules answers, listed in the order the rules are to be tried.

    • Atomic. The whole set is written in one Connection::transaction() or not at all. The transaction first locks the redirector's row (SELECT … FOR UPDATE), then plans the set from the rules as they stand, then writes. POST/PUT /rules/{ruleId}, the rule delete and the redirector delete take the same lock.
    • Ids are kept. A rule sent with its id is that rule, changed, and keeps the id, so its clicks and report rows stay its own. The same holds for criteria and destinations sent with their ids. Anything sent without an id is added.
    • Leaving a rule out removes it. That needs remove_rotator_rule, as on Setup › Redirectors.
    • Read, edit, send back. A set sent back exactly as GET answered it changes nothing (changes.unchanged covers every rule).
    • Refused as the page refuses (Upgrade to PHP 8 #5): a rule with no name, no criterion or no destination; two destinations on a rule that is not a split test; a split test with a destination that has no weight, or whose weights are all 0.
    • Refused by the API as well:
      • A campaign or landing page that is not the account's ([Update] Fix CPC warnings and input length #27, named by its place, e.g. rules.1.redirects.0.redirect_campaign).
      • Another redirector's rule, criterion or destination id.
      • A rule or entry listed twice.
      • Unknown keys at every level (PayloadKeys::refuseUnknown/listErrors, subid missing in pixel url #4).
      • An empty set: the page cannot save one either, and the message says to set each rule's status to 0 instead.
      • More than 200 rules, or more than 100 entries on a rule.
    • Errors. Every problem in the set comes back in one 422, and nothing is written.
    • ?dry_run=1 answers the set as it would be stored. Each rule's change is listed (add, update with what changed, keep, remove) and nothing is written. It is in DryRunRoutesTest's list ([CI] Fix PHP lint action #14) and in the write-preview router.
    • Staged. The route is stageable (?staged=1, p202 … --staged).
    • PATCH is refused with 405. The router lets a PATCH reach a PUT route, and a PATCH naming one rule would remove the others.
    • [Setup] Remove undefined variable from template calls #25. A destination writes NULL in the parts it does not use, as the page does. This is checked live with a click through rtr.php.
    • [Core] Update PHP version check for PHP 8 #13. The read-back after commit is wrapped in WriteCommittedException, so a failed read is never answered as a failed write.
  • Rule order now exists. rtr.php and offrtr.php tried rules in table order, which is id order. "Put this rule ahead of the catch-all" therefore could not be expressed, and a rule added after a match-everyone rule never matched.

    • New column. 202_rotator_rules.position (unsigned int, default 0) is added in the 1.9.78 rung through the reconciler (_upgrade_measurement_tables). It is self-contained.
    • Readers. Every reader orders by position, id (Prosper202\Rotator\RuleOrder): both click scripts, Setup › Redirectors, the API's GET, Overview › Rotator Breakdown and GET /rotators/{id}/stats.
    • Writers. Upgraded rows hold 0, so the existing order is kept. The page's save numbers rules in the order it lists them (ajax/rotator.php), and a rule added on its own goes last.
    • Upgrade window. Until the upgrade runs, the click path catches errno 1054 and reads in id order, so no click fails. The API answers 503 "upgrade pending" in that window (1.9.79: the review findings of the re-click restore and the ledger lock #258's ServerFailureAnswersAreListedTest path).
  • Criterion values are now stored as the page stores them, on POST/PUT of one rule as well as on the set:

    • comma-split and trimmed, with empty values dropped;
    • all stored as ALL, the only spelling offrtr.php reads as everyone;
    • an IP normalized;
    • a device with a capital (Mobile, which the redirects never match) refused, naming it.

    Before this, "United States(US), Canada(CA)" kept the space and never matched Canada.

  • CLI: p202 rotator set-rules <id>.

    • Input. The set comes from --rules '<json>' or --file <path>, with - meaning stdin. It may be an array or {"rules": [...]}.
    • --dry-run previews and writes nothing.
    • What it refuses before any request (validation, exit 1, with a hint): a pasted {"data": …} answer (the hint gives the jq path .data.rules), duplicate JSON keys, trailing JSON, a body over 1 MB, a terminal on stdin, and a missing file.
    • Hints for API errors. 404 points to p202 rotator list. A bare 404 means the server predates 1.9.79: upgrade it or use the per-rule commands. A 422 says that nothing was written.
    • Also. rotator update now validates its id.
  • MCP: rotator_create, rotator_update (name and default destination), rotator_delete (dry run first, with the cascade counted) and rotator_set_rules, declared as the other write tools are.

    • A JSON argument kind. rotator_set_rules takes rules as a JSON argument, so the catalogue gained a JSON kind (mcpToolSpec.JSON). The schema is nested (rules → criteria and redirects, with the type and statement enums), compacted, and checked for duplicate keys.
    • The schema stays honest. TestRotatorRulesSchemaIsTheControllers holds it to the controller's field lists.
    • Instructions. The server instructions gained one line: read with rotator_get, edit the list, send it all back, dry_run: true first.
    • Hosted connector. AiConnect::WRITE_AREAS gains rotators, so propose access mints rotators:stage and write access rotators:write. The approval page names redirectors.
  • Search: a p202 rotator set-rules task with the words people use ("redirector rules", "rule order", "weighted split", …). setup-link-003 is no longer a known miss.

  • Docs: documentation/api/12-rotators.md (the endpoint, rule order, the value rules), docs/openapi.yaml (setRotatorRules), docs/cli.md, documentation/cli/10-go-cli.md, documentation/cli/12-ui-parity.md, go-cli/cmd/COMMAND_CONTRACTS.md. The changelog lines are under 1.9.79 in changelogs.txt and 202-config/changelogs.json.

Tests

  • tests/Api/V3/RotatorRuleSetIntegrationTest.php (@group integration, scratch database, 24 tests). It covers:

    • replacing a set and reading it back identical;
    • a removed rule and its entries gone;
    • ids kept;
    • order applied and read by RuleOrder;
    • a foreign campaign, landing page, rule or entry refused with nothing written;
    • a dry run writing nothing;
    • a failure mid-set (a forced error on a later statement) leaving the old set whole, row for row;
    • the removal permission;
    • every page refusal;
    • duplicates;
    • criterion normalization;
    • the pre-upgrade fallback, with the column dropped;
    • a non-1054 error passed on.
  • tests/Redirect/RotatorRuleSetInstanceTest.php (@group instance). It clicks rtr.php on a running instance to prove the new order decides which rule a visitor gets, and that PATCH answers 405.

  • Go: rotator_set_rules_test.go (what the command sends, dry run and staged, file and stdin, every refusal, the hints, the exact argv an MCP call builds, the schema against the PHP constants) and TestJSONDuplicateKeyNamesTheKeyGivenTwice.

  • Updated tests: TestHostedMCPToolsStayInsideTheAIKeyAreas inputs, DryRunRoutesTest, and ReportsControllerTest's statement key. PathIdsAreReadStrictlyTest now skips $writePreviewRouter, an allow-list whose handlers never read a path.

  • Agent evals (tests/fixtures/agent-eval/cases/rotator-rules.json):

    • rotator-rules-001: put a split rule ahead of the catch-all; checks the order, the weights and that the catch-all is unchanged.
    • rotator-rules-002: preview a change without making it; checks that nothing changed.
    • Reference-agent branches for both. setup-link-003 now accepts rotator set-rules.
  • Plants. Every check was planted against and caught by a named test.

    • PHP, 14 plants:
      • the click path ignoring position;
      • no transaction;
      • kept rules keeping their old position;
      • a rule with no criterion accepted;
      • a removal not asking for the permission;
      • a dry run that writes;
      • destinations rewritten with new ids;
      • another redirector's rule id taken as new;
      • a split test with every weight 0;
      • no pre-upgrade fallback;
      • values stored untrimmed;
      • a rule listed twice;
      • an entry listed twice;
      • PATCH replacing the set.
    • Go, 7 plants:
      • a JSON argument accepting a string;
      • a duplicate key passing;
      • --dry-run writing;
      • an API answer sent as the set;
      • a 422 without a hint;
      • the schema naming a key the controller refuses;
      • hosted keys unable to write rotators.

    Each plant used a mktemp backup checked with cmp, and the tree was restored and re-run green.

MCP catalogue size (#260)

TestMCPCatalogStaysInBudget measures compact JSON:

Build Characters Tools
1.9.79 base 79,533 53
#260 (text ads) 82,902 56
this branch 84,564 57
both together (sum of the two deltas) ~87,933 60

The budget here is 90,000. #260 raises it to 84,000, which this branch alone already exceeds.

Live pass

Local instance on MariaDB 10.11 and PHP 8.3.6, port 8107, installed with install-instance.sh and seeded with seed.sh. The CLI was built from this branch, and every write went through p202 mcp call <tool> --input -. Final tree: 34 passed, 0 failed.

  1. rotator_create, then a tracker on it whose link goes through rtr.php.
  2. rotator_set_rules with dry_run: true: it answers dry_run: true and two adds, and the database is unchanged.
  3. Before the set, an office click goes to the default.
  4. The set has two rules:
    • "Office split": an IP criterion, split test 70/30 between EVAL Campaign A and a URL;
    • "Partner to B": another IP, to EVAL Campaign B.
  5. rotator_get reads the set in order.
  6. Clicks:
    • 60 clicks from the office address split 42/18;
    • the partner lands on Campaign B, and its click row records the rule it matched;
    • anyone else goes to the default.
  7. A second set changes three things and the dry run names each one: it adds "Lab to A" first, changes the office rule (splittest, position and destinations, keeping its id and its b destination's id), and removes the partner rule.
    • Applied, it is read back in the new order.
    • The office now goes to b only, the lab lands on A, and the partner's address falls to the default.
  8. A GET body sent back unchanged reports 0 added, 0 updated, 2 unchanged, 0 removed.
  9. Refusals, with nothing written:
    • a campaign that is not the account's (exit 1, named by its place);
    • a rule with no criterion.
  10. rotator_update replaced the default destination, and a click no rule matches follows it.
  11. --staged:
    • it returns a chg_ proposal and writes nothing;
    • change apply writes the set;
    • clicks follow it.
  12. rotator_delete: the dry run counts the cascade, then the delete removes the redirector.

Also run on the final tree:

  • Agent evals, reference agent: rotator-rules-001, rotator-rules-002 and setup-link-003 each needs_judge, with their deterministic halves passing (there is no judge here). Earlier, a naive agent that does not read the rules first FAILed both new cases.
  • Setup pages: 328 passed, 0 failed. This pass drives every Setup page through a browser-less session, Redirectors included.
  • Page and API share one order: 6 of 6.
    1. The page saves two rules, numbered as listed.
    2. The API's set reorders them.
    3. The page then lists and saves them in that order, with the same ids.
  • tests/live/upgrade-equals-install.sh (1.9.55 on PHP 7.4, upgraded by this tree): 25 passed, 0 failed.
    • One earlier run on the same tree stopped at the 1.9.64 rung: failed to create 202_customers and 14 more LTV tables, a rung this branch does not touch.
    • The run straight after passed 25/0. No cause was recorded (see "Found outside").

Verification

.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed. It ran on the final tree, which is merged with origin/1.9.79 at c42982b (#258), on PHP 8.3.6 with the full vendor/. The schema tier ran against a scratch MariaDB 10.11 database. The unit tier ran 6073 tests, 5 skipped.

Scope report
------------
  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.24)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.24)
  openapi    PASS
  patterns   PASS
  phpcs      PASS
  phpstan    PASS
  schema     PASS
  syntax     PASS
  unit       PASS

Not covered by this script:
  live      stand up an instance, drive the path a user would take
  agent     add and run a case under tests/fixtures/agent-eval/cases/
  ci        a workflow still in_progress has not passed; read the run

Report the lines above verbatim. Do not write "tests pass".

Also run by hand on the final tree:

  • the five rotator integration suites on a fresh scratch database (24, 34, 3, 6 and 4 tests);
  • the instance test (2 tests, 29 assertions);
  • the plants listed above;
  • everything under "Live pass".

CI status: not yet observed.

What could not run here

  • CI's database. CI runs MySQL 8.0; this sandbox has MariaDB 10.11. The PR's workflows will run the integration and eval suites there.
  • No judge. No rubric judge is available here, so the eval cases report needs_judge, as in CI without judge_cmd.
  • The hosted relay (another repository). The install side has unit coverage (TestHostedMCPToolsStayInsideTheAIKeyAreas, WRITE_AREAS). Whether the relay passes the nested JSON schema of rotator_set_rules through unchanged was not observed.
  • One skip beyond the four documented ones. CronEntryPointsFailLoudlyTest::testWithoutAnInstallEveryCronJobSaysSoAndExitsNonZero skipped because the live instance's 202-config.php was in the checkout. That file is not committed.

For the reviewer to check

  1. The position column. Order could not be expressed without it, and the coordinator's reading ("replace the whole ordered rule set") needs it.
    • The column is in the 1.9.78 rung, through the reconciler.
    • The click path's 1054 fallback is detected by errno only, because the message is redacted by p202FailedQueryMessage.
    • The API answers 503 until the upgrade runs.
  2. Locking. The API's rule writes lock the redirector row, so a set and a single-rule write never interleave. Setup › Redirectors' save takes no lock and runs no transaction, so it can still interleave with them (see "Found outside").
  3. An empty set is refused, for parity with the page, which cannot save one. The way to send everyone to the default is status 0 on each rule.
  4. Omitted fields on a kept rule. An omitted splittest/status keeps the held value. A held weight or name that equals what is stored is accepted as is, so a GET body always goes back unchanged.
  5. Criterion normalization and the device case refusal apply to POST/PUT of one rule too. A client that sent Mobile now gets a 422 where it used to get a 201 for a rule that never matched.
  6. PATCH answers 405 on the set. It is refused in the dispatcher, because the router maps PATCH to PUT routes.
  7. Hosted connections made before 1.9.79 keep the scope they were approved with and get 403 on the four new tools until the person connects again. Nothing widens a stored key; this is documented.
  8. Budget 90,000. This branch alone is 84,564. With 1.9.79: text_ad_create, text_ad_update and text_ad_delete in the MCP connector #260 it comes to about 87,933.
  9. PathIdsAreReadStrictlyTest skips $writePreviewRouter: it registers preview handlers that take no path id.
  10. phpcs. The phpcs tier compares each file with its copy at HEAD. Measured against origin/1.9.79 instead, this branch adds no PSR12 errors except one. The new route line in api/v3/index.php keeps the column alignment every other line of that route table has, which PSR12 counts as an error. It also adds line-length warnings in the new controller code and the two new test files; CI runs no phpcs.

Found outside this item (one line each)

  • POST /rotators/{id}/rules accepts a rule with no criterion (201). Such a rule matches every visitor; the page refuses it, and the new set refuses it.
  • offrtr.php reads only ALL as everyone, while rtr.php also reads all. A rule stored as all through the old API matches everyone on one path and no one on the other.
  • Values stored untrimmed stay broken. Criterion values stored untrimmed by the API before this change (", Canada(CA)") keep failing to match until their rule is saved again.
  • Setup › Redirectors' save (tracking202/ajax/rotator.php) runs no transaction. A failure part way through leaves part of the old set and part of the new.
  • The page's weight input has no maximum, while the API caps a weight at 100.
  • ServerStateStore replays old idempotency records. A database reinstalled under the same host and name replays the old install's records (the directory is keyed by host and database name). Here, a seed answered 201 and created nothing until the directory was removed.
  • _upgrade_query() logs no error for a non-transient failure. It answers false with no errno or message logged, so Prosper202 upgrade: failed to create 202_customers cannot say why (seen once here, and not reproduced).
  • p202 eval run --only keeps only the last value when repeated. It is a String flag, so --only a --only b runs only b with no warning. It takes a comma list.
  • The verify ladder's phpcs tier compares a file with its copy at HEAD, not with the --since base. A new file, once committed, is its own baseline, and its findings read as pre-existing.

Generated by Claude Code

…or tools

PUT /rotators/{id}/rules replaces a redirector's rule set the way Setup ›
Redirector saves it, in one transaction: every rule the redirector is to
have, in the order the redirects try them. A rule or entry sent with its id
keeps it (its clicks and report rows stay its own), one without is added,
one left out is removed (remove_rotator_rule, as the page asks). What the
page refuses is refused (a rule with no criterion or destination, two
destinations without a split test, a split test without weights), every
problem named by its place in one 422 with nothing written; another
account's campaign or another redirector's rule is refused by name.
?dry_run=1 answers the set as it would be stored and each rule's change;
the route is stageable; PATCH is a 405. A GET body sent back changes
nothing.

The rules had no order but their ids, so a rule could only be added after
the others: 202_rotator_rules gains position (the 1.9.79 rung), read by
rtr.php and offrtr.php through Prosper202\Rotator\RuleOrder (in id order on
a database the upgrade has not reached, rather than failing the click), the
Setup page, the API and both rotator reports; the page's save and POST
.../rules write it. The API's rule writers lock the redirector's row.

A criterion's values are stored as the page stores them (split, trimmed,
`all` as ALL), and a device in capitals, which never matches, is refused.

p202 rotator set-rules <id> (--rules or --file, --dry-run) sends a set; the
MCP connector gains rotator_create, rotator_update, rotator_delete and
rotator_set_rules, whose rules argument is the set as JSON with a schema
(a tool argument can now be a JSON document). Hosted AI keys may write
redirectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
With the rotator tools the list is 84,564 characters; with #260's text-ad
tools beside them, about 87,900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… them

The merged tree's unit tier read three of the new statements as hidden
from it: the criteria and destinations delete named its table through a
variable, the click path's rule read was built in a class the cached-read
scan does not follow, and two rule reads put a method call after their
WHERE. The delete names each table literally, RuleOrder makes the click
path's cached read itself, and the ORDER BY goes through a variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Copilot AI balanced review requested due to automatic review settings October 23, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T12:31:51.491659Z 8fd7969 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Code review: CLEAN

For 3560ac5: 0 issue(s). The review is encrypted for the maintainers in the review-findings artifact of this run.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3560ac5fb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/v3/Controllers/RotatorsController.php
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Adversarial review: NON-BLOCKING

For 8fd7969: 7 finding(s). The report is encrypted for the maintainers in the review-findings artifact of this run.

Rewritten on every push; the check fails on BLOCKING.

… back

PUT /rotators/{id}/rules?dry_run=1 built each rule's criteria and
redirects in the order the body listed them, while the write updates kept
entries in place and GET reads them back by id, so a body that reordered a
rule's entries (keeping their ids) previewed one order and wrote another.
The nested order decides nothing on the click path: every criterion has to
match, and a destination is drawn by weight. So the plan now holds each
rule's entries as the write will read them back (the kept ones by id, the
new ones after them as sent), and the preview and the write answer the
same data. The rules keep the order sent. The API reference and the
OpenAPI description say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da294b8396

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

$step_ok = _upgrade_measurement_tables([
\Prosper202\Database\Tables\ClickTables::clickLatestVisits(),
\Prosper202\Database\Tables\ClickTables::reClickBackfill(),
\Prosper202\Database\Tables\RotatorTables::rotatorRules(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the column in a migration current installs will run

Existing installations whose database version is already 1.9.79—including installations upgraded from the parent release—skip this if ($prosper202_version == '1.9.78') block, so they never receive the new position column. The newly changed API, setup, and reporting queries then fail with an unknown-column error (only the click path has a fallback). Reconcile this schema change in a new version step or another path that also runs for databases already at 1.9.79.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I'm declining this one. Here is the reasoning for the owner to weigh; I checked each point.

  • 1.9.79 is not tagged. git ls-remote --tags origin 'v1.9.7*' lists v1.9.76, v1.9.77 and v1.9.78, and nothing for 1.9.79. 202-config/version.php says 1.9.79.
  • The 1.9.78 → 1.9.79 rung is still open. CLAUDE.md Fix PHP 8+ compatibility issues throughout Prosper202 #33 freezes a step only once its version is tagged, so the == '1.9.78' rung that writes 1.9.79 is the open one. Its own comment says "A schema change for 1.9.79 belongs in this block, reconciled before the version is written". 1.9.79: capture gbraid, wbraid and ttclid on clicks #263 (202_google), 1.9.79: new accounts start with privacy at Enabled for European Traffic #264 (202_users_pref) and 1.9.79: PayKickstart's product IPN, verified with each campaign's key #268 (202_users_pref) add their tables to the same _upgrade_measurement_tables() list in that rung.
  • Only branch code has written 1.9.79. The only databases storing 1.9.79 were installed or upgraded by unreleased branch code. Every released install stores 1.9.78 or earlier, so it runs this rung.
  • The released path is covered.
    • tests/live/upgrade-equals-install.sh passed 25/0. It installs 1.9.55 on PHP 7.4 and upgrades it with this branch.
    • I also installed v1.9.78 from its tag, served this branch on that database and ran the upgrade page. The page said Success!, 202_version reads 1.9.79, and position was added as int unsigned NOT NULL DEFAULT 0, as the installer defines it. Nothing was logged. PUT /rotators/{id}/rules then stored and read the rules in order. That run was 12/0.

I have not added a second migration. I'm leaving this thread unresolved: whether branch deployments already at 1.9.79 need anything more is the owner's call.


Generated by Claude Code

Comment thread api/v3/Controllers/RotatorsController.php
…rule

PUT /rotators/{id}/rules/{ruleId} read and checked the rule, then took the
redirector's lock, so a rule set that removed the rule could commit in
between: the update then matched no row and its criteria and redirects
were inserted for a rule that no longer existed (these tables have no
foreign keys), answered as a success. DELETE /rotators/{id}/rules/{ruleId}
checked the rule the same way and answered a removed rule's delete as done.

Both now take the redirector's lock, then the rule's own (a locking read,
which also holds off Setup › Redirectors' save, which takes neither), before
the first read of the transaction, and read the rule under them. A rule gone
by then is a 404, with nothing written; an update that matches no row is
checked by a current read and refused the same way. The redirector delete
no longer reads the redirector before its lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1.9.79 gained #262. The only conflict was 202-config/changelogs.json, where
both sides added 1.9.79 lines: 1.9.79's lines are kept and this branch's
follow them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 595bbc06a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/v3/Controllers/RotatorsController.php Outdated
Comment thread api/v3/Controllers/RotatorsController.php
…a rule at once

A rule write read its answer back after its transaction committed, when
the redirector's lock was already let go: a writer waiting on that lock
could replace the set in between, and PUT /rotators/{id}/rules answered the
second writer's rules beside its own changes (POST and PUT of one rule
answered the second writer's set too). The read-back now runs inside the
transaction, under the locks, and is what the write answers.

A rule set's 422 promised every problem at once, and gave one per rule:
the name, the flags and each criterion and redirect threw at the first
problem. Every check of a rule now runs and each problem is answered by
its place (rules.0.criteria.1.value), several in one entry included; the
single-rule writes report every entry's problems the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
The catalogue keeps #280's traffic source, account and category updates and
deletes beside the rotator writes, and rotator_list takes #279's hosted
omission of --all. Both changelogs keep the version branch's lines and add
this branch's. The tool list is 89,469 characters with both; its budget is
90,000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…or_get answers

#281 holds a hosted call's arguments to 1,000 JSON values, because each
item of a list argument becomes a flag of its own. rotator_set_rules takes
its rules as one JSON document, written to its command as one flag, and has
no per-rule tool to fall back on: at 1,000 values a hosted call could send
back about 30 rules of one criterion and two destinations, where rotator_get
answers about 100 under the result bound. A tool that takes a JSON document
is held to 5,000 values instead; at that bound the call allocated 5.5 MB, the
list arguments at theirs 5.1. Past it the refusal says what to leave out and
which command sends a larger set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
"set the api url" ranked p202 system integrations level with config
set-url once the rotator writes and #280's tasks were both in the index:
the two had scored within half a percent on the version branch, and the new
commands' words moved the weights enough to tie them. config set-url now
has a search task with the words the docs use for what it sets, the
install's base URL or instance URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e9661e20e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go-cli/cmd/rotator.go Outdated
…B limit

--file read the whole file with os.ReadFile before checking it against
the API's 1 MB body limit, so a path naming something huge, or endless
like /dev/zero, was read into memory before the size refusal could run.
A path is now read through the same rotatorMaxRuleSetBytes+1 limit
stdin already was, and anything past it is refused by the size check.

TestRotatorSetRulesReadsAFileOnlyUpToTheLimit sends --file /dev/zero and
expects the 1 MB refusal with no request; with os.ReadFile put back it
dies with "fatal error: out of memory" under a 3 GB address-space limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, 202-config/functions-upgrade.php, changelogs.txt, where both sides added lines: the incoming lines are kept as they are and this branch's follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
@tracking202
tracking202 merged commit 377f957 into 1.9.79 Oct 28, 2026
26 checks passed
tracking202 added a commit that referenced this pull request Oct 29, 2026
… split them, not by splittest (#327)

* 1.9.79: a rule's destinations are checked by weight, as the redirects split them, not by splittest

The rule check (RotatorsController::planRule(), used by PUT
/rotators/{id}/rules, the MCP tool rotator_set_rules, and POST and PUT of one
rule) refused a rule with two or more destinations and splittest 0, saying
"splittest is 0, so the rule sends every visitor it matches to one
destination". Neither redirect reads splittest: rtr.php and offrtr.php send
a rule's one destination every visitor it matches, and split them between
more by weight with getSplitTestValue(), the first taking them all when every
weight is 0. Measured on a live instance, 24 clicks each through rtr.php and
through offrtr.php: splittest 0 at 50/50 split 13/11 and 11/13, as splittest
1 did; 100/0 and 0/0 sent every click to the first destination under either
value. So the refusal named behaviour that does not happen, a GET body
holding such a rule could not be sent back to the set, such a rule could not
be renamed through PUT of one rule, and a POST the 1.9.78 API took was
refused.

The check now asks every rule with more than one destination for a weight on
each and at least one above 0, whatever splittest says, and says why in its
message; a rule with one destination is taken whatever its weight, since
the redirects send it every visitor. splittest is stored as sent: it is
Setup › Redirectors' Split test switch.

Setup › Redirectors showed a rule with more than one destination and
splittest 0 with the switch off, which shows and posts only the first
destination, so saving the redirector removed the others with nothing said
(measured in a browser, on 1.9.79 and before). Such a rule now opens as a
split test, each destination with its weight, and saves as one; the list
names it a split test and shows the weights.

RotatorRuleSplitIntegrationTest holds the check to the click path: it runs
the real getSplitTestValue() from connect2.php over each shape of weights,
expects a rule taken exactly when the redirects send its visitors where its
weights say, the same with splittest 0 and 1 and through all three writes,
and fails if rtr.php, offrtr.php, connect2.php or RuleOrder.php comes to read
splittest or to choose a destination another way.
RedirectorPageShowsSplitsIntegrationTest renders the page for such a rule.
SyncRotatorRulesIntegrationTest syncs one, as a set and one rule at a time;
the check made that sync fail. RotatorRuleSetInstanceTest clicks through
rtr.php for a rule sent with splittest 0. Follows up #265, which merged
without this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr

* 1.9.79: read the three request values 1.9.79's own merges left cast

1.9.79's PHPStan job has been red since the merges at 15:46Z. #308 extended
ForbidNumericCastOfRequestParamRule to the pages and baselined the casts
already there. Three PRs merged beside it each added a cast that its baseline
does not count, and each PR passed on its own:

- Personal settings (#283) cast the same-IP fallback after checking it is a
  key of its choices. It now stores 1 for '1' and 0 for '0', the two keys the
  check admits.
- Setup > Offline Conversions (#293) cast a feed id after a regex test. It now
  reads it with PostedId::read().
- Setup > Traffic Sources (#271) cast the edited account's id to read its
  stored postbacks. The page's own ownership check compares the raw value in
  SQL, where MySQL reads '12abc' as 12. So a save from
  ?edit_ppc_account_id=12abc changed account 12 (one of the account's own, not
  the one named) and held that account's postbacks. Measured on a live
  instance: the old page saved the tampered edit onto account 1. The id is now
  read exactly, and anything else is refused in the page's words before
  anything is read or written.

The baseline counts are unchanged: each file now holds only the casts they
counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tracking202 <306382+tracking202@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants