Skip to content

feat(platform): serve the merge-settings and security-policy lanes from the snapshot, consume the served merge verdict and raw config - #463

Merged
thomasboni merged 10 commits into
mainfrom
platform-mode-collection
Sep 9, 2026
Merged

feat(platform): serve the merge-settings and security-policy lanes from the snapshot, consume the served merge verdict and raw config#463
thomasboni merged 10 commits into
mainfrom
platform-mode-collection

Conversation

@thomasboni

Copy link
Copy Markdown
Contributor

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.

  • Decoded (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 more degraded_fields identifiers recognized (raw_config, source_catalog, security_policy_project, includes_jobs).
  • Served lanes (gitlab/snapshot.go, control/lanes.go, control/task.go): mergeRequestSettingsMustBeCompliant (ISSUE-506) evaluates from project_details; projectMustHaveSecurityPolicySource (ISSUE-601) from security_policy_project. Both leave controlsWithNoPlatformLane; a degraded lane reports snapshot_lane_degraded; an absent lane stays not_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 a false.
  • Served verdict and raw config (gitlab/utilsCI.go): the platform's merged_yaml_status and ci_errors are used when the config in use is the snapshot's (ConfigAndIncludesAgree), with today's synthesis as the fallback; the served raw_config feeds the two pre-merge controls when the checkout is not the analyzed project or the root file is unreadable, gated on degraded_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_TOKEN in 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.md keeps the correction paragraph), and a duplicate cidigest package 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.go and gitlab/request.go gofmt, are untouched). Load-bearing tests: the lanes table (neither control lane_not_served), the end-to-end platform-mode run where both controls produce real findings with zero GitLab requests, the degraded-lane not_evaluable cases, 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

thomasboni and others added 10 commits September 9, 2026 14:18
…tLab token

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
…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
@thomasboni

Copy link
Copy Markdown
Contributor Author

Whole-branch review results

Three lenses on the branch (security, quality, code review), one consolidated fix round, one scoped re-review.

Round 1 (6f2320c)

  • security: SIGN-OFF. Trust boundary holds: every absent or degraded lane projects to nil and the control abstains; served strings are only compared or parsed through the existing YAML path; no variable value ever enters; the served raw config is never logged or written to an artifact; standalone mode is byte-identical and no network call was added. Minors: no per-field cap on the served raw config, a fallback in the anchor check that could match on sha when the anchor ref was empty, and the ref-only staleness question below.
  • quality: CHANGES REQUESTED. One blocking point: matching the served raw config on the ref alone let an older snapshot's root file pair with a merged pipeline resolved at a newer commit on the same branch (an under-report where the code used to abstain). Lows: the spec still promised a CHANGELOG entry that the generated changelog forbids, a test comment, a silent fallback on an unrecognised merge status.
  • code review: CHANGES REQUESTED, same blocking point; plus an absent (not degraded) lane left the control not evaluable with no machine-readable reason, and one test exercised a state production cannot reach.

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 project_details or security_policy_project lane now reports lane_not_served, a degraded one snapshot_lane_degraded; unrecognised merge status logged at debug on fallback; the e2e test moved to the reachable resolved state; spec, README (singular claim: only pipelineMustNotOverrideJobVariables is unblocked by the served file) and test comments corrected.

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 collection.missing_fields does not yet name the two new lanes, and the captured snapshot fixture lacks the new fields.

Suite: go test ./..., go vet, make lint (0 issues) and the deadcode target green (the two pre-existing findings untouched); the request-inventory goldens still pin 7 platform requests vs 17 standalone.

Comment thread gitlab/utilsCI.go
@thomasboni
thomasboni merged commit 7faf706 into main Sep 9, 2026
16 checks passed
@thomasboni
thomasboni deleted the platform-mode-collection branch September 9, 2026 20:15
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.

1 participant