Skip to content

Releases: Automattic/newspack-nodes

v2.52.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 16:01

Changed

  • BREAKING: vault test probes the substrate's own status node and answers { id, status } alone. It probed discovery, which the substrate never registers — only event-logger-nodes mounts one — so a spoke running the substrate without that plugin failed the test with server returned TM_ERROR for probe while being perfectly reachable under a perfectly good credential. Status_CI is mounted unconditionally by newspack_nodes_mount_substrate_cis() and its get verb gates at READ, the same role discovery get declared, so no spoke has to grant anything new. It arrived in substrate 0.19.0, which is the floor this verb now carries: a spoke below that release answers status with a TM_ERROR and fails vault test where an ELN-carrying spoke used to pass, so upgrade the spoke rather than reading the failure as a bad credential. The response field carrying registered_hooks, custom_events and lag is gone: nothing read it, and those two keys are event-logger-nodes' ruleset vocabulary, sanitized a second time here beside Discovery_Collector_Node::sanitized_names(), which owns that payload.
  • BREAKING: the vault verbs take --user= and --password= where they took --auth_username= and --auth_password=. An operator now writes cmd vault update austinchronicle --user=newspack-nodes-hub --password=<app password>, and a script still passing the old spellings is REFUSED rather than ignored — add and update now refuse any option they do not read, naming it and listing the ones they take. Nothing validates a verb's args declaration, so an unrecognized --key=value was simply an option the handler never looked up: silent in opposite directions, since add stored an empty credential and answered with the id it had registered, while update read the absence as "leave it alone" and answered a save that changed nothing. A stale spelling, a typo (--pasword=) and an invented flag are one bug, and the refusal runs ahead of any lookup or write. Only the two OPTION names moved: the stored keys, the list/get public shape and the auth_username / auth_password node arguments on HTTP_Out, Remote_Link, Remote_Source and SSE_In are unchanged, so every stored credential and every topology stands. The Vault admin tab sends the new spellings. A valueless option is refused with them. A bare --key parses as boolean true and (string) true is '1', so cmd vault update spoke --password — the value eaten by a shell, or simply forgotten — stored the literal 1 as the credential, announced the change and answered success, leaving that spoke answering 401 with nothing in the vault to say why. Every option these two verbs read names a value, a stored field or the id an entry moves to, so the rule keys on the option's shape rather than on a list of which ones. add refuses an absent --url in its own words as well, where Vault::add()'s generic check URL format (must be HTTPS) named the wrong cause; url is declared as the OPTION the handler reads, because the one command grammar rides a required arg POSITIONALLY and the console duly wrote cmd vault add <id> <url>, which the handler saw as no url at all.

Fixed

  • wp nodes cli printed two prompts after every command when readline was unavailable but stdout was a real terminal. Two prompt owners collided: TTY_Out_Node::write() redrew a prompt behind each newline-terminated reply, and TTY_In_Node::drain_once() then drew its own fallback prompt for the next read. The readline path had never doubled, because it retired the prompt as readline handed a line over. Both paths now retire it in ONE place, TTY_In_Node::emit_line(), which clears both flags before the sink runs — so the reply written inside that dispatch takes the plain write path and only the fallback prompt reaches the screen. The readline callback records its line and nothing else: drain_once() emits from the queue on the very next statement, with nothing writing to the terminal in between.

v2.51.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 14:22

Security

  • The cache scope was computable on any install that had never rotated its salt. Cache_Backend::site() folds salt() into the install scope, and salt() returned '' until an operator pressed the admin flush button or ran wp nodes memcache flush — leaving the scope derived from DB_NAME and the network table prefix alone, both knowable by a co-tenant on the same memcached. A computable scope is what turns a shared cache from readable into injectable: Spawn_Coordinator::load_spawn_ts() replanted stops the fleet spawning, silently. New Cache_Backend::ensure_salt() seeds one and is idempotent — the difference from rotate_salt(), which activation runs on every plugin update would otherwise use to orphan a live keyspace each time. Bootstrap::activate() calls it, and so does self_heal_reconcile_cron() AHEAD of its own early return, because an install activated before the salt existed has a scheduled cron and would never reach activate() again.
  • with_index() now requires a sole-writer partition. An .idx row records the offset a record landed at, and on a log peers append to that offset is the writer's guess. allow_large_writes() proves exclusivity with a held lock and void_warranty() asserts it; with neither, with_index() throws. Every shipped topology already paired them — flames:partition, flame-stats:partition and requests:partition each call void_warranty on the line before — so this makes an existing convention enforced. dump_config() already emits the large-write line first, so a graph still round-trips. The dead-letter queue followed the same rule: it armed an index unconditionally while opting into sole-writer only when the SOURCE was, so a quarantine shared with peers now goes unindexed, losing triage metadata and nothing else (wp nodes ingest replays the .log verbatim either way).

Fixed

  • A short write no longer attempts a quarantine it cannot write. The dead-letter queue is a Partition on the same filesystem, so whatever refused the batch refuses that too — the attempt only bought a second failure. The stall path is now loud, indexes nothing, and truncates the torn record only on a partition that has claimed sole writer; on a shared segment the tail rides and the reader dead-letters the line it cannot unpack. Note the cost on a shared segment: the torn record has no terminator, so the next append lands on that line and is lost with it.
  • scan_index() and locate_by() no longer require a write-side formatter. Both returned early unless with_index() had been called, which is meaningless for a READER over another process's directory — locate_by()'s own docblock says "@api Readers resolving many keys to positions". Consumers were arming a write formatter on a read-only handle purely to unlock the read, which the sole-writer rule above then refused; event-logger-nodes did it in two places. The locator memo is keyed by directory and static, and the guard existed so a formatterless instance could not poison it with false misses — reading the real index removes that hazard instead of guarding it, because the entries such a reader records are true.

v2.50.2

Choose a tag to compare

@github-actions github-actions released this 08 Sep 12:33

Fixed

  • allow_replies_to was not declared repeatable, so the console could express only one. The Inspector branches on a verb's multiple flag: without it a verb renders as a single checkbox and a single argument field, and a round trip through the console keeps the last value and drops the rest. A hub egress needs a declaration per destination — settings-sync, and discovery-collector on a hub running the event logger's hub-control — so the cap was one short of the minimum, and the allowlist fails closed, which means what could not be declared was silently dropped. Now 'multiple' => true, as Settings_Sync_Node::add_setting already was; Classes_CI_Node carries the flag through to the catalog the console reads.

Fixed

  • The Aurora Glass skin painted its surfaces translucent, so a dashboard read through to the WordPress admin behind it. paper, paper-2 and paper-shadow were white at 6-16% over paper-3, and are now exactly that composite as an opaque value, so the ramp holds and nothing shows through. repl-bg could not be treated the same way: it was a 55% scrim of paper-3 over the canvas, which composites back to paper-3 itself and leaves the REPL with nothing but its 1px border to separate it — it is paper-3 darkened to 81% instead, the same repl-under-canvas relationship blueprint, nord, synthwave, neotokyo and scada all carry. Two .theme-aurora overrides repainting canonical roles with hardcoded rgba + backdrop-filter.topology-header and .topology-repl — are gone, and the defaults are correct on their own. Six --on-* contrast overrides went with them: against an opaque paper the derived default already clears AA. Two consequences are deliberate rather than pixel-identical: node fills no longer read the canvas gradient through, and a dialog is no longer see-through onto the page. The skin keeps its aurora canvas, accents and type.
  • A debug-overlay dialog escaped the panel it belonged to. ModalShell anchored horizontally only, so a tall dialog centred on the VIEWPORT and painted well below the panel. It now centres on the panel in both axes and hands that box down as --nodes-modal-max-w / --nodes-modal-max-h, which the stylesheet narrows its standing caps with rather than replacing them — the cap is what makes the vertical anchor safe, since an over-tall dialog scrolls in its own body instead of stranding its head above the fixed backdrop. It also measures the panel it is NESTED IN, from a sentinel at the render site, instead of whichever panel a global selector found first: the DevTools hub mounts the floating overlay beside every tab, so a Console-tab dialog followed a panel it had nothing to do with. A panel too small to contain a dialog is left alone, and the dialog stays viewport-centred there as it always has. The panel's box is re-read through a ResizeObserver on the panel itself rather than a window listener, because useDebugFrame re-clamps the panel on a window resize through a batched state update a same-event listener would read straight past.

v2.50.1

Choose a tag to compare

@github-actions github-actions released this 08 Sep 11:42

Security

  • The remote chose which arm of accept_inbound() its message landed in, and only one arm consulted the allowlist. 2.50.0 gated the REPLY arm on allow_replies_to, and the reply bit is the remote's to set — so on a target-less HTTP_Out a spoke cleared TM_RESPONSE, fell through to the untargeted pass-through, and addressed any node in the graph. That pass-through is what the release notes told operators to avoid by pointing the egress at a Null; the gate should not have depended on their doing so. One rule now: an ADDRESSED message must name a declared destination whatever type bits ride with it, and nothing declared still means nothing addressed passes. The target arm keeps its job — stamping the target onto UNADDRESSED output — and its own refusal is gone, subsumed by the declaration.
  • A spoke could vary its TO to defeat the drop-rate limiter. The refusal reason interpolated the remote's path, and Node::drop_message() KEYS print_less_often() on that reason — so every distinct TO logged again and added a permanent entry to the process's Core::$recent_log_timers. The reason is constant now. drop_message() itself also keyed on the message's TYPE, a bitmask of eleven flags the SENDER picks, which is 2048 keys and 2048 timers from any one drop site; the type moved to the unkeyed tail beside FROM, TO and the payload, so the key is the caller's reason and nothing the message decides. The rendered line is unchanged.
  • allow_replies_to matched the TO's HEAD segment, which made every declaration a prefix rule. 2.50.0 stored the head on the reasoning that a head is what Router_Node peels. But the Router peels the head and then dispatches on the REST, so allow_replies_to _router admitted _router/<anything> and re-opened the whole graph through the list that exists to bound it — and _command_interpreter and _fleet are the same shape. It matches the WHOLE path now, exactly: a declared settings-sync admits a TO of settings-sync and nothing else, and a remote that legitimately answers on a deeper path is declared at that path. This supersedes the head-matching paragraph in 2.50.0's note.
  • $ matches before a trailing newline, and nothing in this tree was D-anchored. The SECURITY-labelled HANDLER_NAME_PATTERN on Job_Intake and Job_Worker_Node let "work\n" through into a line-oriented log; event-logger-nodes carries the third copy. The sweep took every anchored pattern that VALIDATES a whole token — the two handler gates, Core::canonical_decimal() (where "42\n" is not the canonical spelling of 42, which is the function's entire job), the worker-id gates in Bootstrap, Cli, Spawn_Coordinator and SSE_Out_Node, Service_CI_Node's verb-argument default, Layouts_CI_Node::ID_PATTERN, the segment-filename patterns in Partition_Node, Log_Node and Workers_CI_Node, the scaffold CLI's class and slug gates, the memcached server-line gate in the settings UI, Dumper_Node's session filter and Topology_Analyzer's verb and key gates. Deliberately NOT anchored: Flame_Tree::PATTERN_{START,COMPLETE}, Request_Builder_Node's method line and Shell_Node::VAR_GRAMMAR all PARSE a line rather than validate a token — there the trailing newline is incidental, and D would make the line stop matching and lose the capture.
  • The segment directory was world-traversable. Segments are 0600, but a 0755 directory still publishes the listing, and a segment name is an offset — how much was written, and when it rotated. One ensure_segment_dir() now owns all three sites and TIGHTENS what it finds, because a mode passed to mkdir applies once: an install already on disk would otherwise keep 0755 forever, and nothing else probes permissions (Health_Checks has no fileperms call). It masks group and other off rather than setting 0700, so a deliberately read-only directory is left alone, and only the LEAF tightens — mkdir with $recursive applies the mode to every ancestor it creates, and a 0700 logs_dir locks out a deployment whose web request and worker run as different uids. The rotated-segment permission test pins the process umask to 022 besides: a runner already at 0077 masked the bits itself and passed against the defect the test exists to catch.
  • Command_Interpreter_Node executed a TM_COMMAND|TM_ERROR in the JS runtime. PHP masks TM_ERROR beside TM_RESPONSE; the JS half masked only the latter. _respond() addresses a reply TO the request's FROM, so an errored command that arrived with an empty FROM came back as a fresh command — unbounded synchronous recursion. Mirrored.

Fixed

  • A batch every message of which was refused at the boundary answered as if it had routed. stamp_message() warns to stderr, the request-scope stderr sink writes that warning into _output, and the client read a 200 carrying a stderr frame that retires no ask — so a client tracking replies by ID waited for one that would never come. A refused message now gets a TM_RESPONSE|TM_ERROR frame carrying its own ID, addressed back along the FROM it sent.
    That warning was ALSO spending the status line. fill() decides the status on its first write and the status is spent for good, so a batch whose first message overflowed MAX_FROM_SIZE and whose second was an unauthorized command answered 200 and the 401 never ran — $out->reset() then cleared the latch and a second status_header() went out behind the body. dispatch() now ASKS the new Node::can_stamp() instead of attempting the stamp, and emits both the diagnostic and the frame after the batch, once the latch has settled. The latch is deliberately not raised for a boundary refusal: an overflowing path is not an auth failure, and a 401 makes HTTP_Out_Node forget its session and re-handshake, which for a path the client keeps sending is a loop.
  • settings-sync.tsl documents the per-spoke egress in full: the Null, the connect_node, and the allow_replies_to declaration without which the acks are dropped. 2.50.0 required both and the topology said neither.
  • ADR-18 documented the refusal 2.50.0 deleted. It now states the rule that shipped: a spent remainder costs the entry its cache slot, not the read, and what a re-materialized entry is warmed for is the backing's to state.

v2.50.0

Choose a tag to compare

@github-actions github-actions released this 08 Sep 09:59

Security

  • A spoke's reply chose its own destination inside the hub, and every node routes. HTTP_Out_Node::accept_inbound() returns early for a TM_RESPONSE/TM_ERROR so a reply can self-route on the FROM breadcrumb we minted — the Remote_Link heartbeat and event-logger-nodes' Discovery_Collector both depend on it. But the REMOTE sets the bit that makes a message a reply, and that early return precedes the target refusal twelve lines below whose docblock names this exact case: "an addressed non-reply arriving while a target is set is the remote picking its own destination inside us — refused." Every node sinks into _command_interpreter and then _router (ADR-7), so the spoke's TO is routed, not decorative: on a live aggregator hub that is thirty names, _router, _command_interpreter, _fleet and two offsetlogs among them. Command execution stays blocked — the verifier is process-wide, the reply is unsigned, and Message::LOCAL cannot cross unpacked() — but a node's fill() is reachable, and Discovery_Collector::fill() merges an unsolicited payload straight into the hook picker's staging options.
    New allow_replies_to <path> verb on HTTP_Out_Node declares which destinations a reply may address; anything else is dropped with the path named. Matching is on the TO's HEAD segment, because that is what Router_Node peels: a declared head admits the deeper path a breadcrumb echo arrives on, and what the remainder means belongs to the node receiving it. Fail closed: an empty list admits nothing addressed. Remote_Link_Node declares its own name beside the target it already re-addresses in address_null_sink(), so a patron's heartbeat needs no operator action and survives a rename. This is HTTP_Out's first declared verb, so __construct() now calls auto_wire_interpreter() — without it there is no <name>:config sidecar, the verb is unreachable, and the allowlist fails closed with no way to open it. dump_config() replays each declaration so a graph round-trips.
    A per-spoke egress needs a target AND a declaration. With no target set, accept_inbound() passes an addressed message from the remote whatever it carries, so a target-less egress admits MORE than a declared one — point it at a Null, as Remote_Link_Node does for its own patron.
    Operator action required on any hand-wired HTTP_Out. A per-spoke egress created from the topology console — the shape settings-sync.tsl documents — carries no declaration and will drop its acks until one is added: cmd <name>:config allow_replies_to settings-sync and, on a hub running the event logger's hub-control, allow_replies_to discovery-collector.

Changed

  • A durable backing's record now outranks the cache TTL it was written with. Table_Node::read_through() refused an entry whose stated remaining life had run out — "a STATED lifetime that ran out is a miss, not a resurrection." That treats a cache TTL as a statement about the DATA, and it is not one: event-logger-nodes keeps a fine URL bucket two hours in memcache because the fine tier is the largest thing that schema puts in a 512MB cache, while mirroring it for twice the stats window. The refusal therefore made the durable tier useless for exactly the data whose cache lifetime is shortest — an evicted urls_h key could never be rebuilt from the fine buckets decision 17 says it derives from, which is the premise for leaving the coarse tier unmirrored at all. The entry is now SERVED either way, and warmed only when its stated remainder is positive: a spent one is not worth a cache slot, and a backing that wants an entry gone stops returning it. Stats_Store supplies the honest number — window_remaining(), seconds until the bucket leaves the RETENTION window — so a 23-hour-old hour re-warms for an hour rather than for a fresh full one.

  • The cross-language statement pin no longer pins line NUMBERS. tests/fixtures/statements/*.json are the golden the PHP StatementFrontEndParityTest and the JS parse-statements.fixture.test.js both hold their front end to. They carried each statement's line, so editing a COMMENT in a shipped .tsl failed both suites and demanded a fixture regeneration — churn standing between an author and a comment rather than a guarantee. Both halves now drop line before comparing. The parser still emits it and Shell_Node still accumulates it; it is diagnostic, and what the pin is for is the verb, its arguments and the raw text.

Fixed

  • A rotated segment was created world-readable. get_handle() wraps its fopen in umask( 0077 ); the rotation path created the next segment with touch() outside that guard, so under the usual 022 every segment after the first landed 0644 — and a firehose segment carries request URLs and environment values. Confirmed live before the fix: firehose.p0/1520.log was -rw-r--r-- while the same partition's first segment was -rw-------, and a read as another uid returned a real record.
  • /command ingress ignored stamp_message()'s documented refusal. Node::stamp_message() returns false and leaves FROM UNCHANGED when the stamped path would pass MAX_FROM_SIZE, and its docblock states the contract — "Refused two ways, and the caller must drop the message on either." The two other call sites honour it; HTTP_In_Node::dispatch() discarded the boolean and filled the message anyway, so a FROM in the eight-byte window between 1017 and 1024 bytes reached the graph unstamped, escaping the _output boundary and naming its own reply destination.
  • The command interpreter re-dispatched a TM_ERROR, so a refusal answered a refusal. Every refusal is minted TM_COMMAND|TM_ERROR addressed TO = FROM, and the interpreter's gate excluded only TM_RESPONSE — where Command_Auth::is_request_command() already excluded both. Each pass minted another, inside one PHP process.
  • Vault::is_valid_id() accepted a trailing newline. $ also matches before one, so an id that the method's own docblock requires to stay unquoted in an audit line and in a message path could split the record it was written into. Anchored with D.

Removed

  • GET_LAG, the Consumer/Tail request verb, is gone — it had no production caller. Nothing in the substrate or any consumer plugin sent it; the only callers were the REPL's request_node, the console Inspector and the tests. Lag itself is untouched: compute_lag() remains the one seam, probe_stats() and idle_since() still read it, and an operator reads the distance off wp nodes status's Behind column, which renders the probe record's distance. With the verb went Consumer_Node::handle_request(), the requests entry in its node_schema() — the key is absent now, not empty, so help Consumer and the Inspector render no request section — and Consumer_Node::fill(), whose only reason to exist was dispatching that verb. Removing the override changes no behavior: its body had become parent::fill(), and Tail_Node::forward_line()'s parent::fill() now resolves to Node::fill() directly, which is where its TM_BYTESTREAM already landed. The six tests that read the reply payload for its lag arithmetic — including the two wedged-cursor regressions from 0.15.0 — now call the compute_lag() seam instead, so that coverage survives the verb.

Documentation

  • Six documents, plus three docblock citations in the example plugin. The architecture guide now says the substrate ships TWO request verbs, GET_HEALTH and Table's GET <key>, and names Job_Worker_Node::handle_request as the canonical example; it also drops Consumer_Node::fill() from the list of overrides that inherit the FROM stamp by forwarding through parent::fill. writing-a-plugin.md moves its "shape to copy" exemplar to Job_Worker_Node and adds Table_Node as the deliberate contrast — case-SENSITIVE match, no TM_RESPONSE bit, no {verb, data} envelope, an unknown verb dropped with no reply at all. getting-started.md no longer promises a requests section for help Consumer. The three demo nodes in example-ai-newsletter cited Consumer_Node::handle_request for their reply shape and now cite Job_Worker_Node::handle_request, which matches it line for line. AGENTS.md, nodes-review and nodes-debugging follow, the last pointing an operator at wp nodes status's Behind column instead of a verb.

v2.49.3

Choose a tag to compare

@github-actions github-actions released this 07 Sep 23:41

Fixed

  • Partition_Node's locator memo capped keys per DIRECTORY while holding unlimited directories. MAX_LOCATOR_MEMO_KEYS (100,000) bounds one slot, so the real ceiling was that number times however many partition dirs a process ever touched — no ceiling at all. A reader that fans across one mirror per partition fills a slot fast, and event-logger-nodes' PHP suite, which builds a fresh temp dir per test, ran the pool to 672MB and killed PHPUnit with no message. MAX_LOCATOR_MEMO_DIRS (8) now drops every slot past that count, on the same discard-whole idiom the key cap already uses — a discard costs a re-walk, never a wrong answer. The suite peaks at 102MB.

v2.49.2

Choose a tag to compare

@github-actions github-actions released this 07 Sep 04:21

Fixed

  • A dead RuntimeException catch in Job_Intake::feed(). Documenting write_feed()'s @throws made it provably unreachable: the only RuntimeException on that path comes from the per-partition write lock, and partition_handle() acquires that lock inside if ( $large ), where the feed path passes false. The sibling catch in queue() stays — write_job() takes the large path and can genuinely throw.
  • lint:js scans examples/ — it used to skip them entirely. eslint's globs named src/ and scripts/, and lint-contract.mjs walked src/ alone, so the AI-newsletter example sat on three name-lookup-in-option violations while the gate reported clean. An example is the code a reader copies, so a violation there teaches itself onward. SCAN_ROOTS now carries examples beside src; walk() returns nothing for a root that does not exist, so a plugin with no examples scans exactly as before.
  • useStreamGraph declares what setPaused actually takes. Its @return called it a bare Function, which carries no call signature, so the three consumers that correctly declare (paused: boolean) => void — event-logger-nodes' glob, error-log and request-log hooks — each failed the type gate in their own repo with the cause a repo away.
  • The AI-newsletter example passes its view CLASSES, not their registered names. CommandInterpreterNode's name table is a per-bundle static (ADR-16), so a name resolves only through an interpreter that bundle mounted, and a hub tab building the graph through another bundle's would find nothing. ../nodes/register still runs, for the TSL and palette lookups that have no class to hand.

Documentation

  • The twenty-two documents verified against source, then completed from it. Two passes held every existing claim to the code, the second re-deriving each rather than reviewing the first's reasoning; 195 claims across the corpus were refuted, among them TM_COMPLETION attributed to this runtime when it is Tachikoma's flag and 512 is TM_NOREPLY here. Every file:line citation became a symbol reference, several having already drifted — Core::print_less_often() was cited 22 lines early. Then all 931 non-test PHP and JS files across the six plugins were read to establish what these documents omit, and each resulting finding was verified against source again before being written.
  • Every source file's documentation reviewed against its code, one agent per file. All 412 PHP and JS files, tests excluded: missing docblocks written, @param/@return drift corrected against the real signature, documentation for deleted behaviour removed, and the why recorded where the what was already plain. Comments that described history rather than the present are gone. Three node_schema() descriptions were wrong about their own node — Lock_Node advertised a file lock where it takes a directory with mkdir and a PID heartbeat. The second half turned up more of the same: ProbeStreamViewNode claimed its per-record path was O(1) and published nothing, where fill() sweeps every live key and _maybePublish() fires on the leading edge; useTopologyManager documented a dispatchAwaited that exists nowhere in the tree; TreeEntity put its indent at 14px per level where the nested wrappers make it 39px.
  • JSDoc types tightened where prose had loosened them. A @return {Function} carries no call signature, so every typed prop it reaches becomes unassignable and the error lands in a file whose author cannot see the cause; useDebugFrame's resize handlers, TopologyRow's onToggleFold and TopologySection's onToggle all carry real signatures now.

v2.49.1

Choose a tag to compare

@github-actions github-actions released this 02 Sep 18:25

Fixed

  • lint-comments.php no longer wraps a config ledger's own entries. A ledger documents every key by showing the declaration commented out, so the line's width is the code's, not a sentence someone chose to run long — wrapping // 'objecteditor_url' => '/wp-admin/admin.php?page=…' makes it stop looking like the line it documents. Scoped to ledgers and matched with LEDGER_ENTRY, the constant the block rule already identifies them by, so no other file can hide a long line behind a //. Ledger prose still obeys the budget.

Changed

  • The config ledger's entries are aligned and uniformly indented.

v2.49.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 07:36

Added

  • The inspector shows Routing for an included node. A borrowed node rendered its constructor and verbs but no targets, so the one thing this document can say about it — where its output goes — had no control. LockedForm now renders the same editable TargetsField the owned form does, on the same nodeHasTarget() guard. It is not a contradiction of the read-only include: the INCLUDE half stays the defining topology's, while the lines this document aims at the node are this document's, which is what handleUpdateVerbs already assumed for verbs.

Fixed

  • Flush Caches sits on the Save Settings row. It posts its own form, which is why it had been stranded on a line of its own; it now submits that form from a hidden sibling, exactly as Reset to Defaults already did.
  • A settings checkbox is no longer flush against its label. Settings_Renderer::checkbox() emitted <input><label> adjacently, so the box and its first word read as one glyph. 6px between them.
  • A chart legend swatch is no longer flush against its label. The text sat at x=14, four pixels clear of a ten-pixel swatch; it now starts at x=18.

v2.48.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 15:16

Fixed

  • Query and outbound-HTTP spans get their own colors. Both are named base: detail, so getStateColor() resolves them on the base — and neither sql nor http was in SYSTEM_COLORS, so they fell through to the same grey as request. That left the two most expensive things in a trace the two least visible in it. HTTP takes #42A5F5, the hook categorizer's own HTTP color, so a span reads like the hooks around it; SQL takes #8E24AA, a hue none of the 63 categories in hook_categories.json uses.