{"record":{"id":"675b481b7454cb89","repo":"agalwood/Motrix","slug":"plugin-manifest-role-requires-builtin","errorCode":null,"errorMessage":"plugin.manifest.role.requires_builtin","messagePattern":"plugin\\.manifest\\.role\\.requires_builtin","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/core/plugin/hooks/role-band.ts","lineNumber":20,"sourceCode":"  | 'pre-resolve'\n  | 'resolve'\n  | 'enrich'\n  | 'post-process'\n  | 'audit'\nconst ORDER: RoleBand[] = [\n  'pre-resolve',\n  'resolve',\n  'enrich',\n  'post-process',\n  'audit',\n]\n\nexport function bandIndex(\n  role: RoleBand,\n  opts?: { builtin?: boolean }\n): number {\n  if (role === 'pre-resolve' && opts?.builtin === false)\n    throw new Error('plugin.manifest.role.requires_builtin')\n  const i = ORDER.indexOf(role)\n  if (i < 0) throw new Error(`unknown role band: ${role}`)\n  return i\n}\n\nexport interface BandSortable {\n  pluginId: string\n  role: RoleBand\n}\nexport function sortByBand<T extends BandSortable>(arr: ReadonlyArray<T>): T[] {\n  return [...arr].sort((a, b) => {\n    const da = bandIndex(a.role) - bandIndex(b.role)\n    return da !== 0 ? da : a.pluginId.localeCompare(b.pluginId)\n  })\n}\n\nexport function isMostCritical(role: RoleBand): boolean {\n  return role === 'pre-resolve' || role === 'resolve' || role === 'post-process'","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/hooks/role-band.ts#L2-L38","documentation":"Thrown by bandIndex() in role-band.ts when role === 'pre-resolve' AND opts.builtin !== true (explicitly false). The 'pre-resolve' band is reserved for engine built-in plugins; third-party/user plugins may not claim it. Note: unlike most errors here this is a plain Error (no ErrorCode), surfaced during manifest validation/sorting.","triggerScenarios":"A non-builtin plugin's manifest declares role 'pre-resolve' and the loader calls bandIndex(role, { builtin: false }) during manifest validation or sortByBand. Line 19-20 throws before the ORDER.indexOf lookup.","commonSituations":"Third-party plugin copies a builtin manifest verbatim; manifest author picks 'pre-resolve' thinking it runs first; loader regression that forgets to set builtin:true for engine plugins; plugin scaffold template shipped with the wrong default role.","solutions":["Change the plugin manifest role from 'pre-resolve' to 'resolve' (or another non-restricted band) — these are available to third-party plugins.","If you are authoring a genuine engine builtin, ensure the loader marks the plugin builtin:true before bandIndex is called.","Validate the role band at manifest-load time and reject 'pre-resolve' for user plugins with a clear message before it reaches bandIndex."],"exampleFix":"// before — plugin.manifest.json (third-party)\n{ \"role\": \"pre-resolve\" }\n// after\n{ \"role\": \"resolve\" }","handlingStrategy":"type-guard","validationCode":"// During manifest validation, reject 'pre-resolve' for non-builtin plugins.\nconst USER_ALLOWED_ROLES = ['resolve','enrich','post-process','audit'] as const\nfunction allowedRole(role: string, builtin: boolean): boolean {\n  if (role === 'pre-resolve') return builtin === true\n  return (USER_ALLOWED_ROLES as readonly string[]).includes(role)\n}","typeGuard":"function isUserAllowedRole(role: string): role is 'resolve'|'enrich'|'post-process'|'audit' {\n  return ['resolve','enrich','post-process','audit'].includes(role)\n}","tryCatchPattern":null,"preventionTips":["Document that 'pre-resolve' is builtin-only in the plugin authoring guide.","Validate role at manifest load with a clear message rather than letting bandIndex throw.","Keep the scaffold template's default role as 'enrich', never 'pre-resolve'."],"tags":["plugin","manifest","role-band"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}