Skip to content

api Mod Controller Compatibility Info and Required Disclosures system#138

Open
budak7273 wants to merge 95 commits into
stagingfrom
feat/new-fields
Open

api Mod Controller Compatibility Info and Required Disclosures system#138
budak7273 wants to merge 95 commits into
stagingfrom
feat/new-fields

Conversation

@budak7273
Copy link
Copy Markdown
Member

@budak7273 budak7273 commented May 24, 2026

Backend side of satisfactorymodding/smr-frontend#255


  • Controller compatibility info
    • Can be set back to Unknown unlike EA/EXP compatibility info
  • Unused ToggleNetworkUse removed
  • Required Disclosures system (this+AI Disclosures #132)
    • Network Activity Transparency is now a dedicated field instead of to be put in mod descriptions
    • New AI usage disclosure dedicated field
    • New Mod flow intentionally does not allow specifying these disclosures, they can be filled out later once the author has actual answers to them
    • New mod version uploads are blocked until disclosures have been provided
    • Existing mods have no disclosure set

- update devcontainer.json to customizations spec
- switch to modern container repository and pin to 1.22-bullseye (same as go.mod)
- install protoc as part of container setup, and its deps https://github.com/ericslandry/grpc/blob/dcc9b228a2189933b0666eeea23852a12225092b/.devcontainer/post-create.sh
note: must go directly to .56 because of this bug 99designs/gqlgen#3321
(I uncommented the toolchain directive and tidy removed it)
```
failed parsing config: failed to parse configFile /etc/tempo.yaml: yaml: unmarshal errors:

  line 31: field compactor not found in type app.Config
  ```
@codecov
Copy link
Copy Markdown

codecov Bot commented May 24, 2026

Codecov Report

❌ Patch coverage is 80.28169% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.23%. Comparing base (7b96fac) to head (ce51290).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
gql/resolver_versions.go 0.00% 3 Missing and 3 partials ⚠️
gql/gql_types.go 88.23% 1 Missing and 1 partial ⚠️
gql/gql_utils.go 50.00% 1 Missing and 1 partial ⚠️
gql/resolver_mods.go 86.66% 1 Missing and 1 partial ⚠️
workflows/versionupload/extract_mod_info.go 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           staging     #138      +/-   ##
===========================================
+ Coverage    41.73%   42.23%   +0.50%     
===========================================
  Files          128      128              
  Lines         6146     6203      +57     
===========================================
+ Hits          2565     2620      +55     
+ Misses        3191     3185       -6     
- Partials       390      398       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Vilsol
Copy link
Copy Markdown
Member

Vilsol commented May 24, 2026

@claude review

Copy link
Copy Markdown
Member

@Vilsol Vilsol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Bot didn't work when I tagged it, so I had Claude Code run it, here is the result of that:)

Did a thorough pass over the hand-written changes (excluding generated/ and mise.lock). Most of the PR looks great — the disclosure plumbing is well-placed and the new gating correctly rides the already-fetched mod row instead of adding a DB round-trip. A few things to address before merging though, plus some cleanup suggestions.

🔴 Ship-blockers

1. Nil-pointer panic in UpdateMod AI disclosure handling

gql/resolver_mods.go:197

if requiresMessage(aiDisclosureUpdate.DisclosureType) && *aiDisclosureUpdate.DisclosureString == "" {

schemas/mod.graphql declares disclosure_string: String (nullable), so aiDisclosureUpdate.DisclosureString is *string. A client sending {disclosure_type: ai_usage} without a disclosure_string will crash the resolver on the deref.

// suggested
if requiresMessage(aiDisclosureUpdate.DisclosureType) &&
    (aiDisclosureUpdate.DisclosureString == nil || *aiDisclosureUpdate.DisclosureString == "") {

2. requiresMessage disagrees with the version-upload gate

gql/resolver_mods.go:162-164 says a message is required only for ai_usage / runtime_ai_usage.
gql/resolver_versions.go:54-60 treats anything != NoAiUsage (i.e. including no_disclosure) as "needs a message" before unblocking uploads.

So no_disclosure is a valid persisted state from UpdateMod's perspective but a blocker from CreateVersion's perspective. Pick one definition of "set" and centralize it — currently each resolver answers a slightly different question.

3. testza.AssertNoError uses token as the failure message

tests/version_test.go:112

token, _, err = makeUser(ctx)
testza.AssertNoError(t, err, token)

token is being passed as msg ...interface{}. If makeUser fails, the test failure will print the (likely empty) token rather than something useful. Drop the third arg.

🟠 Concrete cleanup

4. Controller-default migration does two UPDATEs on the same rows

migrations/sql/20260501041455_add_default_controller_compatibility.up.sql

The first statement writes {"Controller":""}, the second overwrites it with {"Note":"","State":"Untested"}. Every matching row is rewritten twice (2× WAL, 2× heap update, 2× lock window) and the intermediate Controller: "" is semantically an invalid Compatibility. Collapses to one statement:

UPDATE "mods"
  SET "compatibility" = jsonb_set(compatibility, '{Controller}',
                                   '{"Note":"","State":"Untested"}'::jsonb)
  WHERE NOT compatibility ? 'Controller' AND compatibility ? 'EA';

5. util.AIUseDisclosureInfo is stringly-typed

util/db.go:30-33

type AIUseDisclosureInfo struct {
    DisclosureType   string
    DisclosureString string
}

Forces .String() / raw-string comparisons at gql/resolver_versions.go:58 (!= generated.AIUseDisclosureTypeNoAiUsage.String()) and gql/resolver_mods.go:201. The existing util.Stability (top of the same file) is the established pattern for a typed enum-string:

type AIUseDisclosureType string
const ( AIUseNoDisclosure  AIUseDisclosureType = "no_disclosure" /* ... */ )

Drops every .String() dance and lets go vet catch typos.

6. GenControllerCompToDBControllerComp duplicates GenCompToDBComp

gql/gql_types.go:27 — same util.Compatibility{State, Note} build from a near-identical generated input. Either generalize with generics over a small interface{ GetState() string; GetNote() *string }, or share one input type in the schema (controller-specific enum on a separate field).

7. Two Count() queries in the extract activity

workflows/versionupload/extract_mod_info.go:54-77 — the new soft-deleted check sits right next to the existing live check on the same (mod_id, version) key. Two Postgres round-trips back-to-back. Both are presence-only — Exist(ctx) is cheaper than Count(ctx) (SELECT 1 LIMIT 1 vs aggregate), or merge into a single SkipSoftDelete query partitioned in Go.

8. Duplicated default-disclosure test setup

tests/version_test.go:156 and tests/sml_versions_test.go:77 — near-verbatim mutation block setting AiUseDisclosure: NoAiUsage so uploads aren't blocked. Belongs as a helper in tests/utils.go next to makeUser — any future upload test will need it too.

9. network_use_disclosure is the only .Nillable() optional string in db/schema/mod.go

db/schema/mod.go:43 — every other optional string field uses bare .Optional(). The choice looks intentional (the gating needs *string to distinguish "unset" from "explicitly empty"), but it silently introduces a new convention. Worth a comment on the field. It also creates an asymmetry with ai_use_disclosure (a field.JSON(...).Optional(), not Nillable) that happens to behave the same way only because JSON fields default to nil pointer.

🟡 Style / next time

10. Migration filename outlier

20250411023207_AddNetworkUsageDisclosure.up.sql is the only migration in the repo using PascalCase, and its timestamp is from 2025-04 (a year behind the other three migrations in this PR). Looks carried over from an earlier branch. Renaming now is high-friction (atlas.sum re-hash, anyone with the migration already applied needs a manual fix-up), so feel free to leave — but a next time note for the snake_case + current-date convention.

11. AI + Network disclosure update branches have parallel control flow

gql/resolver_mods.go:195-211 — "if set: validate-and-apply; if cleared while previously set: error" is duplicated across both fields. Could be a SetOmittableNoClear[T] helper next to the existing SetINNF family in gql/gql_utils.go.

✅ Verified non-issues

  • New disclosure gating in CreateVersion does not add a DB round-trip — reads off the mod row the resolver already loaded.
  • No missing indexes — disclosure columns are only read via Mod.Get(id), never filtered/sorted on.
  • Windows .ps1 vs Linux .sh mise tasks: duplication is acceptable given genuine shell semantics differences.
  • Controller unknown state and Compatibility re-use: correctly threaded through util.Compatibility.

@budak7273 budak7273 changed the title Mod Controller Compatibility Info and Required Disclosures system api Mod Controller Compatibility Info and Required Disclosures system May 24, 2026
@budak7273
Copy link
Copy Markdown
Member Author

budak7273 commented May 25, 2026

Addressed 1, 3, 4, 6, 7, 8, 9, 10

Sorta addressed 2

Removed the Unspecified enum option, now covered by the field being unset

Skipped 5

Not sure how to make this work with the gorm:"type:string" json:"DisclosureType" directive present too

Skipped 11

Enough purpose built logic where trying to extract would be more work than it's worth

Comment thread mise.toml
Comment on lines -33 to -48
[tasks.setup]
run = [
"docker compose --file docker-compose-dev.yml up --detach --wait && sleep 5",
"mc alias set local http://localhost:9000 minio minio123",
"mc admin user svcacct remove local/ REPLACE_ME_KEY --dp || echo It is okay for this REMOVE command to fail if the account does not exist",
"mc admin user svcacct add local minio --access-key REPLACE_ME_KEY --secret-key REPLACE_ME_SECRET",
"mc anonymous set public local/smr"
]
run_windows = [
"docker compose --file docker-compose-dev.yml up --detach --wait && timeout /t 5",
"mc alias set local http://localhost:9000 minio minio123",
"mc admin user svcacct remove local/ REPLACE_ME_KEY --dp || echo It is okay for this REMOVE command to fail if the account does not exist",
"mc admin user svcacct add local minio --access-key REPLACE_ME_KEY --secret-key REPLACE_ME_SECRET",
"mc anonymous set public local/smr"
]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was something broken why this had to be done?

Comment thread mise.toml
[tools]
"aqua:minio/mc" = "RELEASE.2025-04-08T15-39-49Z"
atlas = "0.32.0"
atlas = "latest"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be pinned to "latest"

Comment thread migrations/atlas.sum
@@ -0,0 +1 @@
h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file needed?

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.

4 participants