mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 21:20:25 +00:00
0ce9754766
* fix(gitops): make automatic placement approvals reach execution Two defects on the bounded automatic placement path. A removal was never auto-approved: the decision read `gitops_targets.connectivity`, a column seeded to null that no producer ever wrote, so a withdrawal resolved to `unknown_connectivity` even though the target projection already derived reachability from the recorded observation. The decision now reads the projection's own `targetConnectivity`, requires the node registry to have the node online (the same gate an addition passes), and carries an observation that cannot be decoded as a limitation, so the refusal names malformed evidence instead of sending the operator to check a node that answered. An Inline Blueprint's policy placement approval was recorded and then never read: an operational edit clears the combined approval, and the reconciler returned at the legacy gate before consulting the policy's approval. The reconciler now executes a plan a current policy placement approval covers, under seven conditions: the application is inline, the placement policy is still the one that could have written the approval, the approval is a policy's, the rollout authorization policy is automatic, at least one node is already placed at the current revision, every retained node runs the current revision, and the approval covers every action in the plan. An uncovered plan waits for the operator rather than partly executing, a first placement always waits for Apply, and a retained node is observed but never repaired under this approval, because the placement policy decided the node set and not the content. A node-driven roster change no longer hides a policy approval behind a current combined one: a label or cordon change moves the node set without moving the combined approval's fingerprint, so the reconciler tries the policy approval first and falls back to the combined approval when the policy cannot run the whole plan. That fallback narrows the operator's blast to the placement's frozen target set instead of invalidating it, so a removal a policy approved no longer stops drift observation on the nodes that stay; the preview and the lightweight list and detail evaluators read the same narrowing, so no surface reports an approval the executor withholds or withholds one it will run. An operator's approval is never discarded by a policy decision. Demoting an application back to Inline takes the Git-era authority with it: the placement pointer is cleared and the rollout authorization policy returns to operator authorization, because neither was an operator's choice about Inline placement. An Inline Blueprint can reach the automatic rollout authorization the policy path requires from the control beside the placement policy. * fix(gitops): say when automatic placement waits for Apply A selector edit that moves one stateless node runs on the next tick, and Enforce holds a policy-approved plan until Apply. The docs described both as waiting, and the preview showed a pending plan with no reason. * fix(gitops): keep a cleared pin from automatic placement A pin clear left the new intent unpinned, so bounded auto could place the freed node without Confirm Apply. The decision now treats a clear as pin-driven when the standing approval was recorded on a pinned intent, or when a pin was written after an older approval and never confirmed. * fix(gitops): keep a detached snapshot off automatic placement Git-managed deploys stamp the Blueprint revision, and Git content never bumps it, so after Detach a retained node still looked current. Recording a new revision on Detach makes that stamp stale until Apply. * fix(gitops): hold Enforce plans that drop a mid-deploy repair A retained node that is correcting or deploying leaves the plan with no repair, so a policy approval could place an added node in Enforce. The mode itself now holds that plan until Apply, and the preview says so only when Enforce is the reason it is waiting. * fix(gitops): keep a pin chain and a re-armed policy from placing on their own Three authority gaps, all on the automatic placement path, found by the pre-merge audit. A pin that was set and cleared with no Apply between them read as no pin at all once a third intent followed: the intent before the current one was the clear, so the pin that moved the work was out of view and the freed node was placed without Apply. With no decomposed placement approval to anchor on there is no record of any confirmation, so the whole intent history now counts. A Confirm Apply mints the approval the decision anchors on from, and the next change is judged against that, so the wait ends at the next operator decision rather than never. Re-enabling a policy revived a decision the operator had taken back: execution stopped the moment the policy went, but the approval the policy wrote stayed on the row, so setting it back handed the next tick a decision made days earlier on evidence nobody had read since. Arming either policy again now withdraws what that policy decided, with the rollout authorization that only resolves through it. An operator's approval is untouched, the approval rows stay as the record of what was decided, the rollout generation stays so a fresh approval re-drives it, and the history row records the pointers that went with the policy. The refusals a withdrawal produces now read as what the backend checks: a node that has not reported, or that is not a node this Sencho knows about, is a different answer from a node that is not answering, and a withdrawal needs the node to have reported the workload it was running. * fix(gitops): clear the attention a policy placement left behind A Blueprint the placement policy placed a node for sat at Needs attention with a stale acknowledgement and a runtime drift, and neither Confirm Apply nor an API apply cleared it: the retained nodes' plan is observation-only, so nothing ever re-acknowledged them. Two causes, both fixed. The Inline freeze is now keyed on the compose digest rather than the intent revision, so a roster change no longer drops the accepted generation and re-points every placed target at a new generation id for the content it already runs. And the projection's identity test yields when both intent revisions resolve with the same compose digest and the current candidate still requires that node: a node the candidate dropped keeps its signal, because a withdrawal the plan is asking for is not a superseded acknowledgement. Two roster changes inside one tick also left both additions unplaced with no refusal recorded. The reconciler runs one approval and refuses a plan it does not cover whole, so the second approval named only its own addition while the plan still wanted the first. The blast now names every node the fleet does not hold, carrying the earlier decision's unlanded placement forward, while the decision still reads the approved set for its size and safety reasons and the evidence is read for every node the blast names. Removals stay on the approved set, because a withdrawal is judged from the observation of the workload that ran there. The Confirm rollout dialog no longer claims the fleet cannot change until it is confirmed when a configured policy has already approved the plan. * fix(gitops): write the placement blast in canonical order and name a withdrawn node again Two findings from the QA run of this PR, both now pinned by failing-first tests. The first: a roster move that adds a higher node id while withdrawing a lower one left the approval unwritable and the operator looking in the wrong place. The blast effect was assembled places-then-removals, while the decoder requires the node ids strictly increasing, so the write was refused as out of canonical order. The refusal reason then reported that refusal as `conflicting_operation`, so the operator was told an operation was in flight when none was, the plan read `reapproval_required`, and nothing executed. The effect is now sorted by node id, which is the order the decoder wants and carries no meaning of its own: the reconciler reads the effect as a map keyed by node id and the transition re-encodes it to canonical form anyway. Blast decode failures are also recorded as `malformed_evidence` rather than falling through to the conflict default, because a blast that did not decode is a data fault. The roster window that reaches it: the reconciler executes one approval per tick, so an addition approved but not yet landed is carried into the next approval's blast, which then names a placed node alongside the withdrawal. The second: a withdrawn node that the selector wants again produced no row in the plan at all. The tombstone holds deliberately, since only an explicit deploy re-opens a severed placement, but the node stayed online and labelled with nothing to say about why it was skipped, so the plan read as nothing to do. A new informational `skip_withdrawn` row now names what the operator has to do. It is excluded from the confirmable set and from both executor groups, so it authorizes nothing and cannot be confirmed into a mutate, and it yields to the status-precedence pass so an in-flight or guard row for the same node wins rather than leaving two explanations stacked on one node. Follow-up, not addressed here: the replay-guard refusal (`placement approval already recorded for this intent and candidate`) still maps to `conflicting_operation`, which is the same class of untruthful reason on the crash-retry path. It predates this change and is left for its own pass. Tests: a new case drives the add-then-withdraw window end to end and asserts the approval lands with `stateless_removal` and no refusal recorded; a second drives a severed target the selector wants again and asserts the informational row; a third pins that a severed node carrying a `withdrawing` row reports once, as the in-flight withdrawal. Backend: `npx tsc --noEmit`, `npm run lint` (0 errors), `npm test` (11013 passed; the single file-level failure is `git-transport-ssh.integration.test.ts`, which requires loopback port 22 free and fails identically on an unmodified tree because this host runs sshd on 22, freed in CI by `scripts/ci-free-loopback-ssh-port.sh`). Frontend: `npx tsc -b --noEmit`, `npm run lint` (0 errors).