name: bc-registry-asset-comments-and-issues
description: Use when academy staff need to manage registry asset comments (optional academies=<id,id> querystring on GET for multi-academy scope) and asset error issues end-to-end including deduping legacy duplicate error rows, and do NOT use for public asset browsing or non-registry workflows.
requires: []
Skill: Manage Registry Asset Comments and Issues
When to Use
Use this skill when staff need to create, assign, review, resolve, update, or delete comments on registry assets, and when they need to list/update/delete asset error issues. Use it for academy-scoped moderation and QA workflows. Do NOT use this skill for public learner asset browsing or for editing syllabus/admissions entities outside registry.
Concepts
- An asset comment is a human note attached to an asset (
text, assignment fields, and resolution flags). - An asset issue here refers to an
AssetErrorLogrecord with status lifecycleERROR->FIXEDorIGNORED. - An asset issue has a logical identity used for deduplication and grouped operations: the same
slug+asset_type+path+asset(whenassetis present). Whenassetis missing, the logical identity isslug+asset_type+path. - Newly logged issues should collapse into that logical identity (re-logging updates the existing row instead of inserting another copy), and the database prevents multiple rows with the same identity going forward.
- A comment is considered "closed" by setting
resolved=true(there is no separate comment status enum). - Comments and error logs use different capabilities and different endpoints.
- Listing comments requires a single
Academyheader scope and supports optionalacademies=<id,id,...>on GET for read aggregation. The API returns only academies where the user hasread_asset; if some requested academies are not allowed, response includesacademy_scopemetadata with requested/applied IDs andresolution=partial. If none are allowed, the API returns403.
flowchart LR
commentOpen[Comment resolved=false] -->|set resolved=true| commentResolved[Comment resolved=true]
commentResolved -->|set resolved=false| commentOpen
flowchart LR
errorState[ERROR] --> fixedState[FIXED]
errorState --> ignoredState[IGNORED]
fixedState --> errorState
ignoredState --> errorState
Workflow
Set request headers for all calls:
Authorization: Bearer <token>Academy: <academy_id>(required for/academy/endpoints)- Optional:
Accept-Language: en|esfor translated errors.
For comment creation, resolve the target asset first:
- Use the asset ID or slug known by the caller.
POST /v1/registry/academy/asset/commentacceptsassetas either numeric ID or slug.
Create a comment:
- Send
assetandtext. authoris set by the backend from the authenticated user.
- Send
Review/filter comments:
GET /v1/registry/academy/asset/commentwith filters such asasset,resolved,delivered,owner,author, andacademies.- The
Academyheader is required (single academy). For read aggregation, sendacademies=<id1,id2,...>in querystring; the API applies only academies where the user hasread_asset, and partial application returnsacademy_scopemetadata. - This list is paginated and sorted by newest (
-created_at) by default. - Optional sort overrides are supported, including
sort=priorityandsort=-priority.
Manage comment lifecycle:
- Use
PUT /v1/registry/academy/asset/comment/<comment_id>to update assignment/flags. - Use
DELETE /v1/registry/academy/asset/comment/<comment_id>to remove a comment. - To close a comment, set
resolved=true.
- Use
Manage asset error issues:
- List with
GET /v1/registry/academy/asset/error. - Fetch error legend/catalog with
GET /v1/registry/academy/asset/error/catalog. - If the UI still shows multiple identical rows for the same logical issue (legacy data), pick the row you want to keep and call
POST /v1/registry/academy/asset/error/<error_id>/dedupeon that row id. - Update one or many with
PUT /v1/registry/academy/asset/error(single object or list). - Delete one by URL ID or bulk-delete via query lookups.
- List with
Confirm outcomes after writes:
- Re-fetch comment/error records after update/delete.
- For error status updates, expect grouped status propagation across matching rows.
Endpoints
Asset Comments
| Action | Method | Path | Required headers | Required body fields | Response notes |
|---|---|---|---|---|---|
| List comments | GET | /v1/registry/academy/asset/comment |
Authorization, Academy |
None | Paginated list (default sort: newest first). Supports sort=priority and sort=-priority. Use querystring academies=<id1,id2,...> for multi-academy read aggregation. |
| Create comment | POST | /v1/registry/academy/asset/comment |
Authorization, Academy |
asset, text |
201 comment object; author set from session user. |
| Update comment | PUT | /v1/registry/academy/asset/comment/<comment_id> |
Authorization, Academy |
None globally required; send fields to change | 200 updated comment object. |
| Delete comment | DELETE | /v1/registry/academy/asset/comment/<comment_id> |
Authorization, Academy |
None | 204 no content. |
Permissions
- GET requires
read_asset. - POST/PUT/DELETE require
crud_asset.
List scope and filters
academies=<id1,id2,...>on GET (optional multi-academy) — each ID must be numeric. The API applies only IDs where the user hasread_asseton an ACTIVE academy. If some are filtered, response includesacademy_scopemetadata; if all are filtered, response is403.asset=<id1,id2>orasset=<slug1,slug2>(mixed values are supported)resolved=true|falsedelivered=true|falseowner=<owner_email>author=<author_email>sort=priority|-priority(optional; default remains-created_at)
Create comment request
{
"asset": "javascript-arrays-intro",
"text": "The instructions need clearer expected output examples."
}
Create comment response
{
"id": 823,
"text": "The instructions need clearer expected output examples.",
"asset": {
"id": 301,
"slug": "javascript-arrays-intro",
"title": "JavaScript Arrays Intro"
},
"resolved": false,
"delivered": false,
"priority": 0,
"author": {
"id": 17,
"email": "reviewer@academy.io"
},
"owner": null,
"created_at": "2026-04-02T12:15:10.103Z"
}
Update comment request (close + assign)
{
"owner": 42,
"resolved": true,
"delivered": true,
"urgent": false,
"priority": 2
}
Update comment response
{
"id": 823,
"text": "The instructions need clearer expected output examples.",
"asset": {
"id": 301,
"slug": "javascript-arrays-intro",
"title": "JavaScript Arrays Intro"
},
"resolved": true,
"delivered": true,
"priority": 2,
"author": {
"id": 17,
"email": "reviewer@academy.io"
},
"owner": {
"id": 42,
"email": "owner@academy.io"
},
"created_at": "2026-04-02T12:15:10.103Z"
}
Asset Error Issues
| Action | Method | Path | Required headers | Required body fields | Response notes |
|---|---|---|---|---|---|
| List issues | GET | /v1/registry/academy/asset/error |
Authorization, Academy |
None | Paginated list; supports issue filters and `sort=priority |
| Get issue catalog (legend) | GET | /v1/registry/academy/asset/error/catalog |
Authorization, Academy |
None | Returns dynamic catalog from AssetErrorLogType with descriptions and trigger hints. |
| Dedupe legacy duplicate issues | POST | /v1/registry/academy/asset/error/<error_id>/dedupe |
Authorization, Academy |
{} (empty JSON object) |
200 JSON with kept, deleted_ids, deleted_count. |
| Update issues (single or bulk) | PUT | /v1/registry/academy/asset/error |
Authorization, Academy |
id per object, plus fields to update |
200 list of updated issue objects. |
| Delete one issue | DELETE | /v1/registry/academy/asset/error/<error_id> |
Authorization, Academy |
None | 204 no content. |
| Delete many issues | DELETE | /v1/registry/academy/asset/error?<lookups> |
Authorization, Academy |
Query lookups | 204 no content. |
Permissions
- GET requires
read_asset_error. - PUT/DELETE/POST dedupe require
crud_asset_error.
Dedupe behavior (POST /v1/registry/academy/asset/error/<error_id>/dedupe)
- Call this on the issue row id you want to keep.
- The API deletes other rows that match the same logical identity as the kept row.
- If there are no duplicates, the API still returns
200withdeleted_count: 0. - If duplicates exist, the API merges a small set of fields onto the kept row before deleting duplicates:
statususes precedenceERROR>FIXED>IGNOREDacross the kept row + duplicates.prioritybecomes the maximum value across the kept row + duplicates.- If the kept row has an empty
status_text, it is filled from duplicates (preferring the newest duplicate that has text). - If the kept row has no
user, it may be filled from duplicates (preferring the newest duplicate that has a user).
Dedupe request
{}
Dedupe response (no duplicates)
{
"kept": {
"id": 911,
"slug": "readme-syntax",
"priority": 0,
"status": "ERROR",
"path": "README.md",
"asset_type": "LESSON",
"created_at": "2026-04-02T09:18:01.122Z"
},
"deleted_ids": [],
"deleted_count": 0
}
Dedupe response (duplicates removed)
{
"kept": {
"id": 911,
"slug": "readme-syntax",
"priority": 3,
"status": "ERROR",
"path": "README.md",
"asset_type": "LESSON",
"created_at": "2026-04-02T09:18:01.122Z"
},
"deleted_ids": [912, 913],
"deleted_count": 2
}
Catalog (legend) response fields
slug: canonical error slug.label: short human-readable name.description: what the error means.common_trigger_situations: common scenarios where it is generated.severity_hint: triage hint (high,medium,low,unknown).status_notes: practical guidance forERROR->FIXED/IGNORED.
Catalog behavior
- The endpoint is dynamic and auto-discovers values from
AssetErrorLogType. - If a new error type is added in code, it appears automatically in the catalog.
- If a slug has no custom metadata yet, fallback defaults are returned so the endpoint remains stable.
- Supports multi-academy read aggregation through
Academyheader (for example,Academy: 1,2) with partial-scope metadata when some academies are not allowed.
List filters (GET /v1/registry/academy/asset/error)
asset=<slug1,slug2>: exact asset slug match (asset__slug__in), lowercased server-side.slug=<slug1,slug2>: exact error slug match (slug__in), lowercased server-side.status=ERROR,FIXED,IGNORED: exact status match (status__in), uppercased server-side.asset_type=<type1,type2>: exact type match (asset_type__in), uppercased server-side.like=<search_text>: fuzzy match over errorslugorpath(icontains).sort=priority|-priority: optional ordering override (default remains newest first).
Filtering examples
GET /v1/registry/academy/asset/error?status=ERROR&asset_type=PROJECTGET /v1/registry/academy/asset/error?slug=invalid-url,empty-readmeGET /v1/registry/academy/asset/error?asset=javascript-arrays-intro,python-loopsGET /v1/registry/academy/asset/error?like=telemetry
Update issue request (single)
{
"id": 911,
"status": "FIXED"
}
Update issue request (bulk list)
[
{
"id": 911,
"status": "FIXED"
},
{
"id": 912,
"status": "IGNORED"
}
]
Update issue response
[
{
"id": 911,
"slug": "readme-syntax",
"priority": 0,
"status": "FIXED",
"path": "README.md",
"asset_type": "LESSON",
"created_at": "2026-04-02T09:18:01.122Z"
}
]
Edge Cases
Single or multiple academy IDs in header
Academycan be a single ID (Academy: 1) or a comma-separated list (Academy: 1,2,3) for GET aggregation. Mutating endpoints still expect a single academy scope.Comment text is immutable after creation
PUTfor comments cannot updatetext,asset, orauthor; only lifecycle/assignment fields should be changed.Owner cannot self-resolve
If the current session user is the same asowner, changingresolvedraises a validation error.Comment response does not expose all stored fields
urgentexists on the model, but the comment response serializer does not return it.Comment PUT
statusis not a supported lifecycle field
The view checksstatus=NOT_STARTED, but comment serializer logic excludesauthor; do not rely onstatuswhen updating comments.Error status updates can affect grouped rows
Updatingstatusfor one error can propagate to other rows with the sameslug,asset_type,path, andasset.Error delete mode validation
Do not mix URLerror_idwith bulk lookup query params in the same delete request.Dedupe can change fields on the kept row
If duplicates disagree onstatus,priority,status_text, oruser, the merge rules on the dedupe endpoint may update the kept row before deleting duplicates. Re-fetch the kept id after dedupe if the UI caches the old values.Recurring errors return to
ERRORwhen re-logged
If an issue was markedFIXED/IGNOREDbut the underlying problem happens again, the system may upsert the same logical row back toERRORwith refreshed details.
Checklist
- Confirmed headers:
AuthorizationandAcademyare set on every/academy/call. - Used comment
POSTwithasset+text, and verifiedauthorcame from session. - Used comment
PUTonly for assignment/lifecycle fields, nottext. - Closed or reopened comments using
resolvedflag and handled owner-resolve restriction. - Listed and filtered comments/issues with proper query parameters and used multi-academy
Academyheader on GET when cross-academy aggregation was needed. - Updated error issues with awareness of grouped status propagation.
- If duplicate identical issues were still visible, called
POST /v1/registry/academy/asset/error/<keeper_id>/dedupeand verifieddeleted_countand the returnedkept.id. - Deleted comments/issues using the correct single or bulk endpoint pattern.