Expose readinessScoreFeedback on WorkItemWidgetAgentPlan (read-only)
What does this MR do and why?
Exposes readinessScoreFeedback and its rendered counterpart readinessScoreFeedbackHtml as read-only fields on WorkItemWidgetAgentPlan, gated on the existing workplan_score feature flag.
Part of #617982 (closed) — MR 2 of 3 (GraphQL read). Depends on !252883 (merged) (model field); retarget to master once that merges.
Until MR 3 (write path) lands, both fields resolve to nil in production, which is harmless behind the flag.
What changes
ee/app/graphql/types/work_items/widgets/agent_plan_type.rb: addsreadiness_score_feedback(String, nullable,experiment: { milestone: '19.4' },FieldCallCount limit: 1, scopes[:api, :read_api, :ai_workflows]) andreadiness_score_feedback_htmlas amarkdown_fieldwith the same nullability, scopes, milestone, and call-count limit. Both resolvers short-circuit before touching the attribute when the flag is off, so a disabled flag pays no object storage cost.markdown_fieldgenerates its own resolver and takes no feature-flag argument, so the HTML field is gated by a prependedReadinessScoreFeedbackHtmlGatingmodule. Without it the rendered feedback would be readable whileworkplan_scoreis disabled, even though the raw field is gated. The module also maps the empty string thatmarkdown_fieldrenders fornilmarkdown back tonil, to keep the field's null contract.ee/app/models/work_items/agent_plan.rb: addscache_markdown_field :readiness_score_feedback, storage: :externaland aCONTENT_LENGTH_MAXlength validation.ee/app/models/work_items/widgets/agent_plan.rb: delegatesreadiness_score_feedbackandreadiness_score_feedback_htmlto the agent plan record.ee/app/services/work_items/data_sync/widgets/agent_plan.rb: rewrites and copies the feedback markdown when a work item moves, alongsidecontent.scripts/lint/keela_excluded.yml: recordsreadiness_score_feedback_htmlas an intentionally unused-looking delegation (it is read by the GraphQL markdown field).doc/api/graphql/reference/_index.mdandpublic/-/graphql/introspection_result.json: regenerated GraphQL reference docs and introspection snapshot.- Specs: field presence, nullability, scopes, call-count limits, and flag on/off resolver behavior for both fields; model validation and external-storage round-trip coverage; data-sync copy coverage.
Performance note
content, readiness_score_feedback, and the cached HTML all live in the same object storage blob, and Gitlab::ExternallyStoredField loads every externally stored field in a single fetch and memoizes it per record. So the first of these fields read on a given agent plan costs a round-trip, and the rest are in-memory attribute reads. A query asking for all of them on one work item costs one fetch, not three.
The FieldCallCount limit: 1 guard on both new fields is still needed, because that memoization is per record: without the limit, requesting either field across a list of work items would be one blob fetch per work item. The feature-flag check short-circuits before the attribute is touched, so a disabled flag pays nothing.
How to set up and validate locally
The source branch already includes the model change from !252883 (merged), so checking it out is enough.
-
Check out the branch and make sure migrations are up to date:
git checkout duo/feature/617982-readiness-score-feedback-graphql-read bin/rails db:migrate -
In
rails console, enable the flag for a group and seed some feedback on a work item's agent plan (the namespace is derived from the work item):group = Group.find_by_full_path('flightjs') Feature.enable(:workplan_score, group) work_item = group.work_items.first # or any work item under that group plan = WorkItems::AgentPlan.find_or_initialize_by(work_item: work_item) plan.update!(readiness_score: 42, readiness_score_feedback: 'Add **acceptance criteria**.') work_item.to_global_id.to_s -
Open the GraphiQL explorer at
/-/graphql-explorerand run the query with the global ID from the previous step:query { workItem(id: "gid://gitlab/WorkItem/1") { widgets { ... on WorkItemWidgetAgentPlan { readinessScore readinessScoreFeedback readinessScoreFeedbackHtml } } } }Expected:
readinessScoreFeedbackreturns the raw markdown"Add **acceptance criteria**.", andreadinessScoreFeedbackHtmlreturns the rendered HTML with a<strong>aroundacceptance criteria. -
Verify the flag gate. Run
Feature.disable(:workplan_score)in the console and re-run the query. Expected: bothreadinessScoreFeedbackandreadinessScoreFeedbackHtmlarenullwhile the rest of the widget still resolves. -
Verify the null contract for the HTML field. Clear the feedback (
plan.update!(readiness_score_feedback: nil)) with the flag enabled and re-run the query. Expected:readinessScoreFeedbackHtmlisnull, not an empty string. -
Verify the call-count limits by requesting each field for two work items in a single query (for example, through
group.workItems). Expected: a"readinessScoreFeedback" field can only be requested for one work item at a time-style error (and the equivalent forreadinessScoreFeedbackHtml) rather than one object storage read per work item. -
Run the specs and the GraphQL docs check:
bin/rspec ee/spec/graphql/types/work_items/widgets/agent_plan_type_spec.rb \ ee/spec/models/work_items/agent_plan_spec.rb \ ee/spec/models/work_items/widgets/agent_plan_spec.rb \ ee/spec/services/work_items/data_sync/widgets/agent_plan_spec.rb bundle exec rake gitlab:graphql:check_docs
References
- Issue: #617982 (closed)
- MR 1 (model): !252883 (merged)
- Precedent: !249498 (merged)
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.