feat(platform): serve the merge-settings and security-policy lanes from the snapshot, consume the served merge verdict and raw config - #463
Conversation
…tLab token Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
…ection Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
…0034 rule 1) Port platform/backend/cidigest (digest.go + traverse.go) byte-for-byte into the CLI module so both sides compute Version "1" identically: sha256 of a fixed prefix plus each visited path's content hash (or the ABSENT marker) in byte-wise sorted order, discovered via a breadth-first, cycle-safe local-include scan capped at 50 distinct files. All four golden vectors from the platform reference match exactly, including the Traverse-then-Compute vector covering mixed include forms (bare string, array string, local map, whole-value map) and a genuine ErrNotFound absent entry. No caller yet: Task 4 wires this package in, so `make deadcode` flags Compute/Traverse/normalizeIncludePath/localIncludePaths/isAbsent as unreachable until then. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
…ca (ADR-0034 rule 1)" This reverts commit e65a89f.
…the cli does not consume yet The first draft assumed the CLI still required GITLAB_TOKEN in platform mode; that was read from a stale v0.4.44 checkout. Platform mode has been token-free since v0.4.50. The real gap is narrower: project_details (merge settings, ISSUE-506), security_policy_project (ISSUE-601), raw_config, merged_yaml_status, ci_errors are served but not decoded, and four degraded_fields identifiers are unknown to the CLI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
…nd merge status from the snapshot Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
…snapshot lanes The platform has served project_details since 2026-08-27, its eight merge settings since 2026-08-28, and security_policy_project since 2026-08-27, while the CLI still wrote both controls off as lane_not_served on comments that had gone stale. In platform mode mergeRequestSettingsMustBeCompliant (ISSUE-506) and projectMustHaveSecurityPolicySource (ISSUE-601) were therefore permanently not_evaluable over data the platform was already sending. ProtectionFromSnapshot now populates MRSettings from project_details, and a new SecurityPolicyFromSnapshot carries the linkage's Known flag verbatim with the linked project when one was served. Both controls leave controlsWithNoPlatformLane and join the degraded-lane mapping instead, so a lane the platform reports as a FAILED collection degrades its control to snapshot_lane_degraded rather than being written off unconditionally. project_details is deliberately NOT added to lanesWhoseAbsenceIsAFailure: an absent lane leaves MRSettings nil, which StatusFor already reports as not_evaluable. The merge settings are projected all-or-nothing. ir.MRSettings has no pointer members and no absence marker, so a nil optional pointer copied through would reach the ISSUE-506 rule as a real false or "" and be compared for exact equality against the operator's expectation - a fabricated deviation on a setting nobody read. A pre-2026-08-28 snapshot therefore leaves MRSettings nil, the same honest absence an unread project payload has always produced, and self-heals on the next refresh. No new GitLab request in either mode: platform mode never falls back to the GraphQL linkage read (a CI job token cannot reach it), and standalone behaviour is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
Three snapshot fields the platform has served since 2026-08-27/28 reached
the CLI and were dropped on the floor.
merged_yaml_status and ci_errors are now the reported verdict for the
platform-supplied merge. They are the only way a snapshot-path run can
learn it: StartRunConfigResolution starts every resolution Valid and never
clears the flag when the merged configuration comes from the snapshot, so
the local synthesis could only ever say VALID. A configuration GitLab
itself rejected was therefore analysed as a clean one, with the jobs that
failed to merge simply absent and every control passing over what was
left - the silent-green mode platformMergedConfig's own doc comment says
must never happen. The synthesis stays the fallback for snapshots that
serve neither field, and the served verdict is attached only when the
configuration being evaluated IS the snapshot's, the same test the include
attribution already passes: on a digest-divergent branch the merged
document came from the resolve endpoint for THAT branch, and the anchor's
verdict is a statement about a different document.
raw_config becomes the pre-merge document when neither the checkout nor
this project's file API produced the root file - a checkout that is not
the analysed project, a ci_config_path in another project, a refused read.
pipelineMustNotOverrideJobVariables and pipelineMustNotIncludeHardcodedJobs
then report instead of abstaining with raw_config_unavailable on every
tokenless CI run. A lane the platform reports degraded is withheld: what is
on offer there is a truncation past its size cap, and a short root file
yields fewer hardcoded jobs and fewer overridden variables, which is a
silent pass. The served file carries the snapshot's staleness, like every
other lane, and the run prints its collected_at.
Docs: CHANGELOG entry for this and for the two controls Task 2 unblocked,
a README platform bullet for the no-checkout case, and the design spec
moves to IMPLEMENTED. GITLAB_TOKEN has not been required in platform mode
since v0.4.50; the CHANGELOG says so, because the gap closed here was
mistaken for that one.
Also four review nits from the previous commit, all in tests and none
changing a verdict: the ISSUE-506 message assertion now names the clause
only the snapshot can produce ("merge method is merge (expected ff)")
instead of matching "merge", which every ISSUE-506 message contains; the
fixture comments no longer claim the snapshot's merge settings mirror the
fake GitLab's payload, which carries none; the ledger assertion parses the
request count instead of comparing the whole line, so a third read of the
project payload cannot slip past an equality test written for the second;
and LaneMissing's answers for project_details and security_policy_project
get a table test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
… merge status Review fixes on 22b4f04. The served raw_config is the root file collected at the snapshot's ANCHOR, while the merged document under evaluation can be the analysed branch's: a run with no checkout has no local digest, so it is always divergent and its config comes from the resolve endpoint at the job's own commit. Pairing two revisions does not degrade a verdict, it invents one - a variable the anchor declares globally and the branch does not still reads as declared, which is a fabricated Critical, and every finding the root file carries points at line numbers in a file this ref does not have. SnapshotRawConfig now takes the revision under analysis and withholds the file unless it is the anchor's. The REF decides whenever both refs are known; the commit answers only when there is no ref to compare, because the analysed sha a caller has is the head of the analysed REF and, when that lookup fails, control/task.go keeps the default branch's head - the anchor's own. A sha that could stand in for a ref would therefore hand the anchor's root file to a feature branch exactly when the run knew least about it. Withheld, the run keeps today's raw_config_unavailable abstention. The served merged_yaml_status is accepted only as VALID or INVALID. Everything downstream compares against those two strings and reads anything else as "not INVALID", so forwarding a third value would be a pass decided by a word nobody understood; an unrecognized status falls back to the synthesis. Served ci_errors are attached to an INVALID verdict only: the origin collector reads a non-empty error list as an invalid configuration on its own, so errors beside a VALID status would withhold the score over a merge the git host accepted. The CHANGELOG entry is dropped: the file is generated by semantic-release and a hand-written Unreleased section would be duplicated or misplaced at tag time, with the conventional commit subjects carrying the notes anyway. Its one fact worth keeping - platform mode has not needed a GITLAB_TOKEN since v0.4.50 - moves into the README bullet that already says so, and the raw-config bullet now states that the served copy is the default branch's at collection time and is used only when that is the ref being analysed. Also softens the LaneMissing table's comment, which claimed a coverage guarantee a hand-maintained table cannot give. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
…me absent lanes Consolidated review round on 6f2320c. The raw-config gate compared REFS, and a ref cannot answer the question. A run with no checkout has no local digest, so it is always divergent and its merged pipeline is resolved at the job's own commit: on the anchor's own branch, every commit made since the snapshot was collected was analysed against an older copy of its CI file. policies/job_variable_override.rego reads that copy directly, so a protected variable removed in the analysed commit was still reported as an ISSUE-205 Critical and one added in it was missed - the exact fabrication the gate was added to prevent, surviving inside it, on the commonest shape of run there is ("the CI file changed, so the pipeline ran"). The rule is now the commit alone. The served file is used when the anchor's sha is non-empty and the configuration under evaluation came from it: the snapshot path covers by construction (the merged document IS the anchor's merged_yaml), and the resolved path requires ResolvedSha to equal the anchor. An empty anchor sha covers nothing. No ref arm, so a caller's own idea of the analysed commit cannot smuggle the anchor's sha in through the default-branch fallback a failed head lookup leaves behind. An ABSENT project_details or security_policy_project lane now says lane_not_served instead of abstaining with no reason. The verdict was honest either way through StatusFor, but the terminal report's "Not evaluated" bucket and the push's not_evaluable reason both read result.NotEvaluable, so on a snapshot from a platform older than 2026-08-27 the two controls rendered as ordinary controls with zero findings and the platform was sent a status with no explanation. Degraded still says snapshot_lane_degraded: nothing failed here, and telling an on-prem operator otherwise sends them looking for a fault that is not there. Also: the served raw config is bounded by the same 2 MiB ceiling the checkout's own file is read under, past which it is withheld; an unrecognised merged_yaml_status is logged at Debug rather than silently falling back; the raw-config e2e now runs in the shape production can produce (SourceResolved at the anchor's sha) and claims only the one control this lane can unblock, since pipelineMustNotIncludeHardcodedJobs needs include attribution a divergent run never has; the spec's docs item no longer promises a CHANGELOG entry; and two stale test comments are corrected - the fake GitLab and the snapshot agree where both carry a fact rather than describing the same project, and the security-policy request is absent because the snapshot answers it, not because no lane can. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
Whole-branch review resultsThree lenses on the branch (security, quality, code review), one consolidated fix round, one scoped re-review. Round 1 (6f2320c)
Fix round (ed5bfe7): the served raw config is used only when the evaluated config came from the snapshot's anchor commit (sha equality, no ref arm; an empty anchor sha never covers), capped at the local 2 MiB limit; an absent Re-review: all five items addressed with file:line evidence, standalone behavior untouched, request goldens unchanged. Two notes recorded rather than fixed: the raw-config lane now fires only while the snapshot covers the analysed commit, so on an active default branch the served file is usually withheld (honesty over coverage, the ruling behind the sha gate); and three accepted follow-ups stay open: an unrecognised merge status still falls back to VALID rather than abstaining, the push's Suite: |
What
Platform mode catches up with what the platform snapshot has served since 2026-08-27/28. Two controls stop being permanently
not_evaluable(lane_not_served) when a platform is linked, and the CLI reads five served fields it ignored.internal/platform):project_details(default branch, archived, path, the eight merge settings as optional pointers),security_policy_project(known,id,full_path),raw_config,merged_yaml_status,ci_errors; four moredegraded_fieldsidentifiers recognized (raw_config,source_catalog,security_policy_project,includes_jobs).gitlab/snapshot.go,control/lanes.go,control/task.go):mergeRequestSettingsMustBeCompliant(ISSUE-506) evaluates fromproject_details;projectMustHaveSecurityPolicySource(ISSUE-601) fromsecurity_policy_project. Both leavecontrolsWithNoPlatformLane; a degraded lane reportssnapshot_lane_degraded; an absent lane staysnot_evaluable. The merge-settings projection is all-or-nothing: the IR has no "absent" marker, so any missing optional setting (snapshots stored before 2026-08-28, self-healing on refresh) leaves the settings unset rather than fabricating afalse.gitlab/utilsCI.go): the platform'smerged_yaml_statusandci_errorsare used when the config in use is the snapshot's (ConfigAndIncludesAgree), with today's synthesis as the fallback; the servedraw_configfeeds the two pre-merge controls when the checkout is not the analyzed project or the root file is unreadable, gated ondegraded_fields.No behavior change outside platform mode; zero GitLab calls are added in platform mode (the request-inventory tests pin the counts).
Context and a correction
This branch started from a spec that claimed the CLI still required
GITLAB_TOKENin platform mode. That premise was read from a stale v0.4.44 checkout: platform mode has been token-free since v0.4.50. The spec was rewritten to the real gaps above (docs/superpowers/specs/2026-09-09-platform-mode-collection-design.mdkeeps the correction paragraph), and a duplicatecidigestpackage the first pass produced was reverted before any of this landed. The platform side removes its obsolete onboarding token warning in getplumber/monorepo PR #269.Testing
TDD red-before-green per task;
go test ./...,go vet,make lint(0 issues) and the deadcode target green (the two pre-existing findings,configuration/schema.goandgitlab/request.gogofmt, are untouched). Load-bearing tests: the lanes table (neither controllane_not_served), the end-to-end platform-mode run where both controls produce real findings with zero GitLab requests, the degraded-lanenot_evaluablecases, and the served-verdict passthrough and fallback.Review
Per-task reviews passed each task (one fix round). Whole-branch review: code-reviewer, security and quality lenses; results in the PR conversation. Release notes come from the conventional commits (this repo's CHANGELOG is generated by semantic-release).
🤖 Generated with Claude Code
https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz