Skip to content

docs(api): describe course kinds and opaque content in openapi.yaml - #3471

Open
Ethanlita wants to merge 13 commits into
goplus:issue-3403-tutorial-v2from
Ethanlita:issue-3422-openapi
Open

Ethanlita wants to merge 13 commits into
goplus:issue-3403-tutorial-v2from
Ethanlita:issue-3422-openapi

Conversation

@Ethanlita

@Ethanlita Ethanlita commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Syncs docs/openapi.yaml (the backend API spec source of truth) with the Tutorial v2 course storage and API changes. Part of #3422. Backend: goplus/builder-backend#350 / goplus/builder-backend#353 / goplus/builder-backend#354 (expand release) and goplus/builder-backend#352 (contract release).

The spec documents the final contract as specified by docs/develop/tutorial-v2/module_CourseApis.ts, not the transitional shapes of the rollout: the document has no machine consumers (the frontend client is hand-written, nothing generates from it), so there is no reason to publish an intermediate spec. Until goplus/builder-backend#352 is deployed, the Release 1 backend serves a superset of this spec — it still returns top-level entrypoint / prompt on guided courses, still accepts the flat {entrypoint, prompt} create/update payload, and defaults an omitted series kind to guided. A client written against this spec works on both releases; nothing new needs the superset.

  • Course gains kind (immutable, guided | playground) and kind-specific content. content is selected by kind through allOf / if / then branches (the AIGCEnrich… settings precedent) into GuidedCourseContent (entrypoint, prompt) or PlaygroundCourseContent (a file collection mapping paths to universal URLs; may be empty so a course can be created before its files are uploaded). Not oneOf: a guided content document is itself a string→string map and would satisfy both alternatives.
  • Course create requires kind + content; update replaces content as a whole after validating it against the course's kind and documents kind immutability.
  • CourseSeries gains kind (required on create); the three series list endpoints gain a kind query filter; the series kind is immutable on update, and both create and update state the member-kind consistency rule.
  • New POST /user/courses/playground/copilot-context endpoint (courseAdmin gate, copilotMessage quota, refunded on failure).

API surface changes (as documented in this spec)

No new routes besides the copilot-context endpoint — a Playground Course is a kind of the existing course resource, so support lands as payload changes on the existing endpoints.

Endpoint Change
POST /user/courses Request is {kind, title, thumbnail, content}; kind: guided with GuidedCourseContent, kind: playground with PlaygroundCourseContent. Response gains kind / content.
PATCH /courses/{courseID} Request accepts content (validated against the course's kind, replaced as a whole); kind may be echoed but is immutable.
GET /courses/{courseID}, GET /courses, GET /user/courses, GET /users/{username}/courses Course schema gains kind and content; the pre-kind top-level entrypoint / prompt are gone from the spec (the Release 1 backend still returns them, see above).
GET /courses, GET /user/courses, GET /users/{username}/courses New optional kind query parameter (goplus/builder-backend#353).
DELETE /courses/{courseID} Unchanged.
POST /user/course-series, PATCH /course-series/{courseSeriesID} kind required on create and immutable afterwards (PATCH accepts only an echoed, unchanged kind); both state that every course in courseIDs must have the series' kind.
GET /course-series, GET /user/course-series, GET /users/{username}/course-series New kind query parameter; CourseSeries schema gains kind.
GET /course-series/{courseSeriesID}, DELETE /course-series/{courseSeriesID} Response carries kind; otherwise unchanged.
POST /user/courses/playground/copilot-context New. {title, thumbnail, content}{copilotContext}; courseAdmin gate; copilotMessage quota with ForbiddenOrQuotaExceeded / RateLimitExceeded responses (40301/42901 + Retry-After); server-side failures refund, client cancellation does not.

Beyond the spec, this PR carries three small companions to the course list kind filter, each in its own commit so any can be dropped: docs/develop/tutorial-v2/module_CourseApis.ts gains kind?: CourseKind on ListCoursesParams (an amendment to the design contract, mirroring ListCourseSeriesParams), spx-gui/src/apis/course.ts types the same parameter, and the two call sites that filtered guided courses client-side after paginating (CourseManagementModal.vue, CourseSeriesEditModal.vue) now pass kind: 'guided'. The last one fixes a visible bug: with the most recently updated courses all being playground courses, the management modal's first page showed "no results" while the pager counted every kind. It needs goplus/builder-backend#353 deployed to take effect; against an older backend the parameter is ignored and behavior is unchanged.

The documented 400s and response shapes were verified against the Release 2 backend binary in the goplus/builder-backend#352 rehearsal (see its description).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates the backend API source-of-truth OpenAPI spec to reflect Tutorial v2’s transitional “expand” contract for Course/CourseSeries kinds and content, and documents the new playground Copilot-context generation endpoint.

Changes:

  • Extends Course and CourseSeries schemas with kind, and adds Course.content as the kind-specific opaque payload.
  • Updates course and course-series create/update/list endpoints to document transitional legacy behavior and adds kind filtering for series list endpoints.
  • Adds POST /user/courses/playground/copilot-context for generating an editable Copilot context draft from unsaved playground content.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/openapi.yaml
Comment thread docs/openapi.yaml Outdated
Comment thread docs/openapi.yaml Outdated
@Ethanlita
Ethanlita marked this pull request as ready for review August 27, 2026 06:45

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FennoAI Review — PR #3471 (OpenAPI: course kind / playground content)

The change is a well-structured, single-file extension of docs/openapi.yaml. It cleanly introduces the guided/playground kind enum for Course and CourseSeries, a new content field, a new POST /user/courses/playground/copilot-context endpoint, and kind query filters, while documenting the transitional legacy (entrypoint/prompt) shape.

Strengths

  • kind enums are single-sourced via $ref and consistent between Course and CourseSeries.
  • The kind query filter is applied uniformly across all three course-series list endpoints with identical wording.
  • The new endpoint documents the courseAdmin restriction, the quota/refund behavior, and a complete 400/401/403/429 response set, inheriting the global bearerAuth scheme correctly.

Cross-cutting notes (no reliable single line):

  • Unbound "API contract release" reference. The phrasing "accepted/removed until/by the API contract release" appears in several places (createCourse, updateCourse, createCourseSeries, and the deprecated fields) but is never resolved to a concrete version/date. Consider defining the target release once so readers can determine when the transitional shape disappears.
  • SSRF / arbitrary-fetch surface (backend verification). The playground content values use format: uri-reference, which permits relative and arbitrary-scheme URIs. If the backend dereferences these caller-supplied "universal URLs" while assembling copilot context, ensure it allowlists accepted schemes (e.g. kodo://, data:) and rejects http(s)/file/etc. This cannot be enforced in the spec — please confirm against the handler.
  • Quota refund-on-failure. "Refunded when generation fails" is a common spot for double-spend / refund-race bugs and, combined with unbounded input (see inline), can allow repeated expensive-but-failing submissions without depleting quota. Verify the refund is atomic and that per-request cost is bounded before the expensive work runs.

Comment thread docs/openapi.yaml Outdated

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FennoAI Review — PR #3471 (continued)

Two additional inline findings that did not fit in the first review submission. See the prior review for the summary and cross-cutting notes.

Additional findings

  • docs/openapi.yaml:2867: [P3] kind+content vs legacy shape exclusivity is prose-only: The description says the request "either provides kind and content, or uses the transitional legacy flat shape (entrypoint and prompt without kind and content)", but the schema declares all four as flat optional properties with only title/thumbnail required. A codegen consumer cannot tell that mixing content with entrypoint/prompt, or kind: playground with entrypoint, is invalid, nor that one of the two shapes is effectively required.

Comment thread docs/openapi.yaml Outdated
@Ethanlita

Copy link
Copy Markdown
Collaborator Author

对 review 正文里三条横切备注的处理:

  • "API contract release" 无具体指向:采纳(a6942a0),四处引用现在都指向 goplus/builder-backend#352。
  • SSRF / 任意抓取:已在 handler 侧确认——goplus/builder-backend#354 用 safehttpclient(dial 阶段拒绝 loopback/私网/link-local,重定向同样受管)抓取,kodo:// 只接受本服务配置的 bucket,data: 本地解码不发请求。http(s) 不做 scheme 级拒绝(universal URL 契约允许),由 SSRF 防护 client 兜底。
  • 退款竞态:端点复用仓库共用的 maybeRefundQuotaOnError(与 /copilot/messages 同一策略:只对 5xx 退款、跳过取消),退款经 authz.RefundQuota 原子执行;单请求成本已有上界(见上面对 content 上限那条的回复)。

Ethanlita added a commit to Ethanlita/builder that referenced this pull request Sep 7, 2026
…oneOf

A guided content document is itself a string-to-string map, so it also
satisfies the playground file-collection shape and a oneOf on content would
reject valid guided courses (the point made in the goplus#3471 review). Follow the
AIGC settings precedent instead: Course and the create/update bodies keep a
flat object with an allOf of if/then branches keyed on kind, which is what
selects GuidedCourseContent or PlaygroundCourseContent.
Comment thread docs/openapi.yaml
$ref: "#/components/schemas/Course/properties/title"
thumbnail:
$ref: "#/components/schemas/Course/properties/thumbnail"
content:

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.

这里应该可以复用 PlaygroundCourseContent

content:
  description: Current, possibly unsaved, playground course content.
  $ref: "#/components/schemas/PlaygroundCourseContent"
  minProperties: 1

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

好的,我修改这个

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed @b6286a99

Comment thread docs/openapi.yaml
owner:
description: Username of the course series' owner.
$ref: "#/components/schemas/User/properties/username"
kind:

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.

这个 kindCourseCourseSeries 里重复定义了,可以考虑提取一个 CourseKind,然后它俩 $ref

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

好的

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed @b6286a99

Comment thread docs/openapi.yaml Outdated
description: |
Kind-specific course content, selected by `kind`: `GuidedCourseContent` for a `guided` course,
`PlaygroundCourseContent` for a `playground` course. The API validates only the top-level shape; internals
follow the Tutorial content-format contract and stay opaque to the API and storage.

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.

这个 Tutorial content-format contract 有固定的文档链接吗?或者 issue 之类的

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

稍等,我把链接发出来,是在代码库里面(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed @b6286a99

Comment thread docs/openapi.yaml Outdated
PlaygroundCourseContent:
description: |
Content of a `playground` course, driven by course code: a file collection mapping file paths to universal URLs.
The referenced files form the Tutorial project; its layout is owned by the Tutorial Class Framework contract. An

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.

Tutorial Class Framework contract 还有这个也可以补上链接,如果有的话

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

好的

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed @b6286a99

Ethanlita and others added 11 commits September 21, 2026 12:02
Sync the API spec with the Tutorial v2 backend changes
(goplus/builder-backend#350/goplus#353/goplus#354), documenting the transitional
expand-release shape:

- Course gains kind (immutable, guided | playground) and kind-specific
  opaque content; the top-level entrypoint/prompt copies are marked
  deprecated and noted for removal by the API contract release.
- Course create/update accept both the kind+content shape and the
  transitional legacy flat payload; update documents kind immutability.
- CourseSeries gains kind; series lists gain a kind filter; series
  create/update document the kind semantics.
- New POST /user/courses/playground/copilot-context endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns with GeneratePlaygroundCourseCopilotContextInput in
module_CourseApis.ts and the backend validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The endpoint draws from the copilotMessage quota, so its 403/429
responses follow the same contracts as the other quota-backed endpoints
(40301/42901 with Retry-After). Also state the refund policy precisely:
server-side failures refund, client cancellation does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Course.content now shows both kind-specific shapes, and every mention of
the transitional shape's removal points at the concrete contract PR
(goplus/builder-backend#352) instead of an unnamed future release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…guided too

Matches goplus/builder-backend#353: the flat entrypoint/prompt shape applies
to guided creates whether kind is omitted or given, and content wins when
both are present.
Pairs with goplus/builder-backend#352 (migration 018 plus its application
change): the transitional shapes introduced for the expand phase are gone.

- `Course` drops the deprecated top-level `entrypoint` / `prompt`; `content`
  becomes a `oneOf` of the new `GuidedCourseContent` and
  `PlaygroundCourseContent` schemas, selected by `kind`.
- `POST /user/courses` requires `kind` and `content`; the legacy flat payload
  is no longer documented. `PATCH /courses/{id}` no longer accepts the flat
  fields and states that `content` is replaced as a whole.
- `POST /user/course-series` requires `kind`.

BREAKING CHANGE: clients must send `kind` + `content` (and `kind` on course
series) and must read guided course data from `content`.
…oneOf

A guided content document is itself a string-to-string map, so it also
satisfies the playground file-collection shape and a oneOf on content would
reject valid guided courses (the point made in the goplus#3471 review). Follow the
AIGC settings precedent instead: Course and the create/update bodies keep a
flat object with an allOf of if/then branches keyed on kind, which is what
selects GuidedCourseContent or PlaygroundCourseContent.
Review feedback from the course API spec sync:

- The copilot-context request body repeated the playground file-collection
  shape; it now references PlaygroundCourseContent, with minProperties: 1
  expressing the one difference (a course may be saved empty, a context
  request may not be).
- The guided/playground enum was written twice; it becomes a shared
  CourseKind schema, following the Visibility precedent, with the
  kind-specific wording staying at each use site.
- The "Tutorial content-format contract" and "Tutorial Class Framework
  contract" mentions now link to the design docs they name.

Requested by review (aofei).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GET /courses, GET /user/courses and GET /users/{username}/courses accept an
optional `kind` query parameter, mirroring the course series lists. Without
it a client has to filter after paginating, which leaves pages short or
empty once both kinds exist while `total` still counts every kind.

Backend: goplus/builder-backend#353.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ListCoursesParams gains an optional `kind`, mirroring ListCourseSeriesParams.
Both kinds now share the course lists, so a consumer that wants one kind
needs the server to filter before paginating; filtering a page client-side
leaves it short while `total` still counts every kind.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Types the `kind` query parameter of the course list endpoints
(goplus/builder-backend#353) on ListCoursesParams.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ethanlita and others added 2 commits September 21, 2026 12:33
UpdateCourseSeriesParams in the Tutorial v2 contract carries no kind
(9822f64), so a series keeps the kind it was created with. The spec had
still described changing it. PATCH now documents kind the way the course
PATCH does: an echoed, unchanged kind is accepted, a different one is
rejected. Both create and update state the membership rule.

Backend: goplus/builder-backend#353.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The course management modal and the course series edit modal paginated the
signed-in user's courses and then dropped non-guided ones client-side. Once
playground courses exist that breaks both:

- The management modal pages by 8 ordered by updatedAt desc. With the 8 most
  recently updated courses all being playground courses, its first page
  showed "no results" while the pager still counted every kind.
- The series edit modal takes the first 100 courses; enough playground
  courses push guided ones out of that window.

Both now pass `kind: 'guided'` so the server filters before paginating. The
`filter(isGuidedCourse)` stays only to narrow `Course[]` to `GuidedCourse[]`.

Requires the backend filter from goplus/builder-backend#353.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

3 participants