You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
One diagram renderer, scripts/render-diagram.sh, shared with every sibling that has docs/img/ sheets. With no argument it renders every sheet, sizing the window to the measured page; docs/img/render.sh, which guessed an 1800px window, is removed, and autocrop.py moves beside the renderer. Two PNGs that had drifted from their sheets are re-rendered.
The /command rate limit claims atomically. A user still gets RATE_LIMIT_BURST (30) POSTs per second, now one slot each through Rate_Limit's atomic add(), where two concurrent requests could both read a transient count under the cap. The one-second window is no finer than before: memcached frees a slot within the second it was claimed in, so the budget is close to one bucket per cache tick. With neither memcached nor APCu, or a slot read that fails, the door admits and logs one rate-limited warning, as it ran unmetered before. The newspack_nodes/command_rate_limit filter is clamped to Rate_Limit::MAX_BURST (256). HTTP_In_Node::$clock_now_seam and $rate_limit_disabled are gone; a test meters against a fake cache instead.
The spawn endpoint's admin-path limit claims atomically too. One external spawn per user per Spawn_Controller::RATE_LIMIT_S (2 seconds) still answers 429 rate_limited past it, but the claim is Rate_Limit's atomic add() where it was a transient read and write two concurrent requests could both pass. With no shared cache the spawn is admitted, with one rate-limited warning, because the fleet revives itself through this endpoint. The transient key newspack_nodes_spawn_rate:<user> is no longer written.
The Overview tab's Topics charts lay out two to a row, as the Jobs and Tables tabs' do. TopicsPanels renders its own .nodes-topics-panels grid wrapper, styled once in event-dashboards/styles/topics-chart.scss, which TopicsChart.js imports; the per-tab .nodes-overview__panels and .nodes-probe-tab__panels wrappers and rules are gone. The debug overlay renders its two charts through TopicsPanels too, narrowed to one column by .nodes-debug__panel .nodes-topics-panels.
A Topics chart draws at most 500 slots, down from 1000. The cap tracks the panel's width, and a panel is now half a row, about 900px; buildAlignedSeries() widens each bucket to fit, keeping its max, so a spike still survives.
One pre-push hook serves all six plugins.scripts/pre-push is shared, and sync-shared-scripts.sh vendors it with a scripts/.shellcheckrc that makes shellcheck follow its source. Each plugin keeps only scripts/pre-push.local: six constants naming the plugin and its containers, and the push_gates and container_gates slots holding its own gates. The hook fails the push when that file, a constant or a slot is missing, and the sync never touches it. The substrate floor check is one of the shared gates, after lint-wp-pin; it passes at once here, where no floor is declared. This repo's .local carries the shared tooling's self-tests, test-fix-blank-lines.sh among them, and the sync no longer vendors any test-*.sh. A failed npm run build prints its last 20 lines and names the file holding the rest. lint:shell and lint-staged check pre-push.local too.
Added
Rate_Limit::claim( $name, $burst, $window_s ), one atomic call budget for any door. Each admitted call claims one of $burst slots with Cache_Backend::shared_first()'s atomic add() under a $window_s TTL, which memcached ends $window_s - 1 to $window_s seconds after the claim and APCu $window_s to $window_s + 1. It answers Rate_Limit::ADMITTED, THROTTLED or UNAVAILABLE, which the caller maps to its own response, and throws on a burst outside 1 to Rate_Limit::MAX_BURST (256), a window below 1 or a name holding whitespace. Rate_Limit::admit_unmetered( $door ) admits an unmetered call and warns once per door, for a door that must not fail closed. /command and the spawn endpoint meter through it, and so does event-logger-nodes' MCP route, which drops its own copy.
A class declares the siblings the console draws as owned, once, in node_schema()['owns']. The map runs suffix to class: Crawler_Node declares seen => Table and not the curl sibling it also builds, and Node::node_schema() defaults it to []. Only a palette class may declare it — Node::in_palette(), which classes dump filters on too — and Topology_Analyzer::owned_nodes() refuses an owner off the palette with <Class> declares owns but is not a palette class. owned_nodes() reads the declaration for every make_node whose class resolves, in place of a hardcoded Crawler check, and declared_tables() declares each owned Table from it, with the arguments the owner's owned_table() reads off its line; a class owning a Table, or a Table subclass, overrides Node::owned_table( $suffix, $owner, $arguments ), whose base refuses with <Class> declares no owned Table <suffix> for <owner>. Crawler_Node::seen_table() is gone: build_siblings() and the analyzer both read Crawler_Node::owned_table(). write_set() claims an owned sqlite Table's file through the same path, so two topologies whose owners share one file still conflict, and the analyzer names no owner class. topologies get's owned, the declared Tables, the write set and the editor share one source. write_set() now refuses a Crawler declaring no TTL, as declared_tables() already did, and Bootstrap::split_readable() builds each active topology's write set as well as its graph, so such a topology — or one whose owner is off the palette or gives its Table no arguments — is listed unreadable and the rest of the fleet still spawns. The analyzer reads each class's owns once per process.
classes dump ships each class's owns, an empty list for a class declaring none, which the topology editor reads to draw owned nodes.
Fixed
Edit mode lays out and saves an owned node's position. The editor derives its owned nodes from the draft itself: ownedOf( draft, catalog ) reads each class's catalog owns declaration and names each sibling through Node.siblingNameOf(), the JS twin of PHP Node::sibling_name_of(), and withOwnedNodes() composes them into the edit canvas as view mode's seed does. Edit-mode autoLayout seats a Crawler's {name}:seen Table beside its owner, the card drags, and Save layout writes its position. Because the list follows the draft, a renamed Crawler takes its Table and the Table's position along, a newly dropped Crawler gets one at once, and an Echo renamed to a removed Crawler's name gets none. Edit-mode layout waits for the class catalog, so an owned node is placed with the rest rather than tucked in after. The owned node is never a line in the document, so a save writes none.
An owned node's edge is no route.withOwnedNodes() wires each owned node with roles: [ 'extra' ], as extra_targets() declares it (ADR-19), so the edit canvas offers no edge selection on it and the owner's Routing list omits it. As a bare edge it read as connect, and deleting it would have run disconnect_node <owner> <owned>, clearing the owner's real target.
The edit Inspector shows an owned node read-only. A node carrying owner takes the locked form a borrowed node does, badged OWNED and keeping the Owned by <owner> note, and shows no Routing, Constructor or Verbs section: the server holds those values, and the draft has none to show. No edit reaches set_arguments or replaceVerbs, so a save never writes a command_node <owner>:seen:config … line for a node no line of the document declares.
No wire starts at an owned node in edit mode. Its OUT port is no wire source there, takes no edit-mode fill, and its pointer handlers start no drag, so no gesture writes a connect_node naming a node the document does not declare. View mode keeps the port.
The Vault matches a server URL's https:// scheme in any case.Vault::add() and update() compare it with strncasecmp(), as https_required() does. Under WordPress esc_url_raw() already lowercases the scheme before the check, so this changes behaviour only where sanitized_url() runs without WordPress. The Vault admin form's own check is case-insensitive too, so it no longer refuses HTTPS://host before the store sees it.
The build kit removes a stylesheet an entry no longer imports. When a build emits no CSS for an entry, as its metafile shows, the stale <base>.css and <base>-rtl.css an earlier build left in outDir are deleted rather than kept, and the RTL step no longer regenerates the stale companion.
A watch rebuild drops a removed import's handle from <base>.asset.php. The wp-externals collector empties as each build starts, so the manifest lists only the handles this build reached for.
lint-styles checks a block's final declaration even when it has no semicolon..widget .button { color: red } is now judged like color: red;.