fix(api): validate prerequisite parents on feature and experiment writes; block deleting a flag experiments gate on - #6950
Open
bryce-fitzsimons wants to merge 9 commits into
Open
Conversation
A prerequisite may now only point at an existing, unarchived boolean flag that does not itself depend on the feature being written, matching what the dashboard's prerequisite picker allows. Previously the API stored a missing, archived, non-boolean, or cyclic parent and the SDK payload builder then silently dropped the child flag. The check runs on per-rule add/patch (v1, v2), bulk create/update (v1, v2, including the feature-level prerequisites list, which had no existence check at all), and the revision prerequisites endpoint. Only parents a write introduces are checked, so a stored reference to a since-archived parent still posts back unchanged. Cycle detection walks upward from the new parents one query per hop instead of loading the org's whole feature graph.
Contributor
|
Member
Author
|
@greptile review |
The cycle walk now loads ancestors through the org-wide scan context the delete guard uses, so a parent in a project the caller cannot read still contributes its edges; the caller's own permissions still decide whether a direct parent is "not found". A chain still open after fifty hops is refused instead of silently accepted. The v2 bulk update checked the stored rules rather than the inbound ones, so rule-level parents there were never validated. Per-rule handlers share one revision-aware wrapper so the draft's prerequisites list is the baseline on both sides.
bryce-fitzsimons
had a problem deploying
to
preview
September 13, 2026 06:01 — with
GitHub Actions
Error
Member
Author
|
@greptile review |
The saved-group reference check no longer reads a rule's prerequisites, so its input type and the PUT handlers' call sites stop mentioning them. Unit cases cover what the HTTP harness cannot reach: a cycle closed through an ancestor the caller cannot read, the depth refusal, and the no-query path when a write adds no prerequisite.
Member
Author
|
@greptile review |
The delete guard counted only feature prerequisites, so a flag listed in a running experiment's phase prerequisites could be deleted and the experiment then evaluated its gate against nothing and vanished from the SDK payload. Delete and archive now share one experiment-dependents collector, so both directions see the same dependents.
Member
Author
|
@greptile review |
bryce-fitzsimons
had a problem deploying
to
preview
September 13, 2026 06:19 — with
GitHub Actions
Error
The guard's REST and dashboard wiring predates this change; what changed is the service function, so its cases live in a unit test with the loaders stubbed instead of an HTTP harness.
Member
Author
|
@greptile review |
Member
Author
|
@greptile review |
…service The prerequisite-parent check moves out of the features API directory into services/prerequisiteParents so experiments can use it. Experiment phase prerequisites now get the same existence, archived, and boolean checks on the REST create/update endpoints and the app API create, phase edit, new phase, and targeting writes. Experiments are leaves of the prerequisite graph, so no cycle walk runs for them. As with features, only parents a write adds are checked.
bryce-fitzsimons
had a problem deploying
to
preview
September 13, 2026 06:44 — with
GitHub Actions
Error
Member
Author
|
@greptile review |
REST update and both create paths validate the prerequisites of all inbound phases against everything the stored experiment already references, so an earlier phase cannot smuggle in a bad parent.
Member
Author
|
@greptile review |
bryce-fitzsimons
had a problem deploying
to
preview
September 13, 2026 06:48 — with
GitHub Actions
Error
…experiment updates
Member
Author
|
@greptile review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Features and Changes
Prerequisites now get the same constraints on the API that the dashboard's prerequisite picker applies. A prerequisite must point at a flag that:
Previously the API stored any of these and the SDK payload builder then silently dropped the dependent flag or experiment, so it fell back to its code default with no error anywhere.
Features, every write path: per-rule add and patch (v1 and v2), bulk create and update (v1 and v2, including the feature-level
prerequisiteslist, which had no existence check before), and the revision prerequisites endpoint. Cycle detection walks upward from the new parents one query per hop, through an org-wide read so a parent in a project the caller cannot see still counts, rather than loading the whole feature graph on every write. A chain still open after fifty hops is refused.Experiments, every write path: REST create and update, and the app API create, phase edit, new phase, and targeting writes. Experiments are leaves of the prerequisite graph, so only the parent checks apply.
Only parents that a write introduces are checked. Anything already pointing at a since-archived parent still reads and posts back unchanged. On a full-array experiment update, the served phase is checked against the latest stored phase and earlier phases against anything the stored experiment already references.
Reverse direction
Deleting a flag that an experiment's latest phase lists as a prerequisite used to succeed; the experiment then gated on nothing and was dropped from the SDK payload. The delete guard now counts experiments as well as features, using the same collector as the archive warning.
Testing
prerequisiteParents.test.ts(HTTP) covers each bad-parent case on the v2 rule add endpoint, one case per remaining feature write path, and the stale-reference echo.test/services/prerequisiteParents.test.tscovers the helper directly: no query when nothing is added, a cycle closed through an ancestor the caller cannot read, the depth refusal, and the experiment variant.test/services/featureDeleteGuard.test.tscovers the delete guard. The experiments REST harness gains a wiring case.