@things-factory/auth-base 10.1.218 → 10.1.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/service/role/role-mutation.js +5 -0
- package/dist-server/service/role/role-mutation.js.map +1 -1
- package/dist-server/service/role/role-types.d.ts +1 -0
- package/dist-server/service/role/role-types.js +4 -0
- package/dist-server/service/role/role-types.js.map +1 -1
- package/dist-server/service/role/role.d.ts +10 -0
- package/dist-server/service/role/role.js +5 -0
- package/dist-server/service/role/role.js.map +1 -1
- package/dist-server/service/role-template/role-template-gap-resolver.d.ts +2 -2
- package/dist-server/service/role-template/role-template-gap-resolver.js +7 -13
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
- package/dist-server/service/role-template/role-template-mutation.d.ts +7 -4
- package/dist-server/service/role-template/role-template-mutation.js +15 -15
- package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
- package/dist-server/service/role-template/role-template-types.d.ts +3 -3
- package/dist-server/service/role-template/role-template-types.js.map +1 -1
- package/dist-server/service/role-template/role-template.d.ts +14 -15
- package/dist-server/service/role-template/role-template.js +8 -8
- package/dist-server/service/role-template/role-template.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/tests/role-template-gap-db.test.ts +58 -7
- package/tests/role-template-seed-db.test.ts +5 -4
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ── What a template is not ───────────────────────────────────────────────────
|
|
12
12
|
* It is a seed, not a role. Once it has been used the resulting role belongs to the
|
|
13
|
-
* administrator: the template is never re-applied and never overwrites.
|
|
14
|
-
*
|
|
15
|
-
* that ("the template gained two privileges, here they are") is the template's current
|
|
13
|
+
* administrator: the template is never re-applied and never overwrites. The role keeps only
|
|
14
|
+
* which template it is based on (`Role.templateId`, one at most); what an administrator wants
|
|
15
|
+
* from that ("the template gained two privileges, here they are") is the template's current
|
|
16
16
|
* grants against the role's current privileges, and that is computed rather than stored.
|
|
17
17
|
*
|
|
18
18
|
* ── A role is one thing seen from three sides ────────────────────────────────
|
|
@@ -40,12 +40,11 @@ export type RoleTemplate = {
|
|
|
40
40
|
/** Declaration key, namespaced by the declaring app: `plant.quality-inspector`. */
|
|
41
41
|
id: string;
|
|
42
42
|
/**
|
|
43
|
-
* The role name created in the domain: `QUALITY-INSPECTOR`.
|
|
43
|
+
* The role name created in the domain by default: `QUALITY-INSPECTOR`.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* the first. Renaming is a migration, not a template edit.
|
|
45
|
+
* A role is paired with its template by `Role.templateId`, never by this name. Renaming
|
|
46
|
+
* here still makes the next default-named role a second one beside the first, which
|
|
47
|
+
* approval lines may already point at — renaming is a migration, not a template edit.
|
|
49
48
|
*/
|
|
50
49
|
name: string;
|
|
51
50
|
/**
|
|
@@ -161,15 +160,15 @@ export type RoleTemplateGap = {
|
|
|
161
160
|
*/
|
|
162
161
|
export declare function templateGapAgainst(template: RoleTemplate, held: ReadonlySet<string>, declared?: Record<string, unknown>): RoleTemplateGap;
|
|
163
162
|
/**
|
|
164
|
-
* The gap against the
|
|
163
|
+
* The gap against the role's base template (`Role.templateId`) — what the role list shows unasked.
|
|
165
164
|
*
|
|
166
|
-
* Paired by
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
165
|
+
* Paired by the stored base, never by name (ADR-0075 decision 1, amended 2026-10-26): operato-office's roles are
|
|
166
|
+
* named in the site's own words, and pairing by name left them without a gap to show. The role is never renamed
|
|
167
|
+
* to make a pair: approval lines hold the role, and the name is the site's.
|
|
168
|
+
* `null` when the role has no base, or its base is not declared in this installation; a gap of 0 missing is a
|
|
169
|
+
* role that is up to date.
|
|
171
170
|
*/
|
|
172
|
-
export declare function templateGapOf(
|
|
171
|
+
export declare function templateGapOf(templateId: string | null | undefined, held: ReadonlySet<string>, templates?: RegisteredRoleTemplate[], declared?: Record<string, unknown>): RoleTemplateGap | null;
|
|
173
172
|
/**
|
|
174
173
|
* Names a template's own grants mention that this installation does not declare.
|
|
175
174
|
*
|
|
@@ -204,16 +204,16 @@ function templateGapAgainst(template, held, declared = process['PRIVILEGES'] ||
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
/**
|
|
207
|
-
* The gap against the
|
|
207
|
+
* The gap against the role's base template (`Role.templateId`) — what the role list shows unasked.
|
|
208
208
|
*
|
|
209
|
-
* Paired by
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
209
|
+
* Paired by the stored base, never by name (ADR-0075 decision 1, amended 2026-10-26): operato-office's roles are
|
|
210
|
+
* named in the site's own words, and pairing by name left them without a gap to show. The role is never renamed
|
|
211
|
+
* to make a pair: approval lines hold the role, and the name is the site's.
|
|
212
|
+
* `null` when the role has no base, or its base is not declared in this installation; a gap of 0 missing is a
|
|
213
|
+
* role that is up to date.
|
|
214
214
|
*/
|
|
215
|
-
function templateGapOf(
|
|
216
|
-
const template = templates.find(one => one.
|
|
215
|
+
function templateGapOf(templateId, held, templates = allRoleTemplates(), declared = process['PRIVILEGES'] || {}) {
|
|
216
|
+
const template = templateId ? templates.find(one => one.id === templateId) : undefined;
|
|
217
217
|
return template ? templateGapAgainst(template, held, declared) : null;
|
|
218
218
|
}
|
|
219
219
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-template.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger } from '@things-factory/env'\n\nimport { readOnlyAxes, readOnlyAxesAreKnown } from '../privilege/privilege-axis.js'\nimport { mergeGrants, privilegeKey, roleTaskById, roleTasks, undeclaredTaskGrants } from './role-task.js'\n\n/**\n * Role templates — the seeds an application offers so a new domain does not start with\n * zero roles.\n *\n * ── Why this is a boot registry and not a table ───────────────────────────────\n * A template is a declaration. Copying it into rows per domain gives 100 domains x 3\n * templates = 300 rows, and once the code changes the template there is nowhere to ask\n * which of the two is true: the declaration, or the row that was written from an older\n * one. `@privilege` lives in the boot registry for the same reason.\n *\n * ── What a template is not ───────────────────────────────────────────────────\n * It is a seed, not a role. Once it has been used the resulting role belongs to the\n * administrator: the template is never re-applied and never overwrites. Nothing is written\n * onto the role to remember where it came from — what an administrator would want from\n * that (\"the template gained two privileges, here they are\") is the template's current\n * grants against the role's current privileges, and that is computed rather than stored.\n *\n * ── A role is one thing seen from three sides ────────────────────────────────\n * operato-plant uses roles as privilege bundles; operato-twin uses them as the seat an\n * approval line points at (`worklist` resolves an approver of type `Role` by id). Those\n * are not two concepts. A role is a seat in the organisation: what it opens, who fills it,\n * and whether approvals route to it. Splitting them would put two QUALITY-INSPECTORs in\n * one plant and force every person into both.\n */\n\n/** One module and the axes this template grants on it. Axes are always enumerated. */\nexport type RoleTemplateGrant = {\n /** `@privilege(category:)` — what is guarded, not the package name. */\n category: string\n /**\n * `query` / `mutation` / ... — listed one by one.\n *\n * There is deliberately no `'all'`. A template written as \"everything\" would widen on\n * its own the day a new axis is declared, with nobody having touched it. The single\n * place that may widen is the framework-computed administrator template, which means\n * \"whatever exists now\" by definition.\n */\n axes: string[]\n}\n\nexport type RoleTemplate = {\n /** Declaration key, namespaced by the declaring app: `plant.quality-inspector`. */\n id: string\n /**\n * The role name created in the domain: `QUALITY-INSPECTOR`.\n *\n * Two apps may declare the same name on purpose — that is one seat, and the privileges\n * become the union. The name is also a stored identifier: approval lines hold the role's\n * id, so renaming here creates a second role and leaves the approval line pointing at\n * the first. Renaming is a migration, not a template edit.\n */\n name: string\n /**\n * Plain text, not an i18n key.\n *\n * `createRoleFromTemplate` copies this onto the role it seeds, and the role list draws\n * the role's description as a value — so a key written here reaches the screen as\n * `label.role-template.administrator`. That is exactly what the framework's own two\n * templates did until 2026-09-11.\n *\n * operato-plant had it right from the start: \"검사 기준과 처분을 정하는 자리\".\n */\n description?: string\n /**\n * Required unless `tasks` is written, and may be empty.\n *\n * `[]` is a statement: this seat opens nothing, it exists for approval routing. A\n * missing `grants` is a different thing — someone forgot — and is rejected here, so the\n * screen can tell \"opens nothing on purpose\" from \"nothing was written down\".\n *\n * Read a template's privileges through `templateGrantsOf`, never from here: with `tasks`\n * this is only the part written directly.\n */\n grants?: RoleTemplateGrant[]\n /**\n * The work this seat does, by task id (`registerRoleTask`, ADR-0139 decision 11).\n *\n * The seat holds every privilege of every task here, plus `grants`, each privilege once.\n * A task id nobody registered is a definition error: boot reports it and the template is\n * not applied at all, rather than applied without that work.\n */\n tasks?: string[]\n}\n\n/**\n * Registered form of a template.\n *\n * Identical to the declaration today. It stays a separate name because the registry is\n * what every reader goes through, and giving it its own type keeps a later addition from\n * rippling into every call site.\n */\nexport type RegisteredRoleTemplate = RoleTemplate\n\nconst REGISTRY = 'ROLE_TEMPLATES'\n\nprocess[REGISTRY] = {}\n\nfunction registry(): Record<string, RegisteredRoleTemplate> {\n return process[REGISTRY]\n}\n\n/**\n * Declares a role template. Call it from the application's server entry.\n *\n * Categories are not checked here: registration runs while modules load, before the\n * schema is built and therefore before `process['PRIVILEGES']` exists. The name check\n * happens at boot instead (`reportUnknownTemplateGrants`).\n */\nexport function registerRoleTemplate(template: RoleTemplate): void {\n if (!template?.id || !template?.name) {\n throw new Error('A role template needs both an id and a name.')\n }\n\n if (template.tasks !== undefined && (!Array.isArray(template.tasks) || template.tasks.some(task => !task || typeof task !== 'string'))) {\n throw new Error(`Role template \"${template.id}\" has tasks that are not a list of task ids.`)\n }\n\n if (!Array.isArray(template.grants) && !template.tasks?.length) {\n throw new Error(\n `Role template \"${template.id}\" has no grants. Write \\`grants: []\\` if this seat is meant to ` +\n 'open nothing — leaving it out is indistinguishable from forgetting, and the screen cannot ' +\n 'tell an administrator which one it is.'\n )\n }\n\n for (const grant of template.grants || []) {\n if (!grant?.category || !Array.isArray(grant.axes) || grant.axes.length === 0) {\n throw new Error(\n `Role template \"${template.id}\" has a grant without a category or with no axes. ` +\n 'Axes are always listed one by one.'\n )\n }\n\n /*\n * Across the nine roles operato-plant declares, no role is granted `mutation` on a\n * category without `query` on the same category — you cannot fix what you cannot see.\n * A template that does so is almost certainly a slip, so say it out loud rather than\n * silently granting a half-usable seat.\n */\n if (grant.axes.includes('mutation') && !grant.axes.includes('query')) {\n logger.warn(\n `[role-template] \"${template.id}\" grants ${grant.category}:mutation without ${grant.category}:query. ` +\n 'Someone who may change a thing but not see it can rarely use either.'\n )\n }\n }\n\n if (registry()[template.id]) {\n throw new Error(`Role template \"${template.id}\" is declared twice.`)\n }\n\n registry()[template.id] = { ...template, grants: template.grants || [] }\n}\n\nexport function roleTemplates(): RegisteredRoleTemplate[] {\n return Object.values(registry())\n}\n\nexport function roleTemplateById(id: string): RegisteredRoleTemplate | undefined {\n return registry()[id]\n}\n\n/**\n * The two templates the framework can build without knowing a single category name.\n *\n * Everything else has to be declared by an application: a job title spans modules\n * (a production lead uses worklist *and* order *and* ops-master) and no one package knows\n * that. These two are different — they are defined over whatever is declared, so they can\n * be computed from the registry `@privilege` fills.\n */\nexport function computedRoleTemplates(): RegisteredRoleTemplate[] {\n const declared: [string, string][] = Object.values(process['PRIVILEGES'] || {})\n const axesOf = new Map<string, Set<string>>()\n\n for (const [category, axis] of declared) {\n if (!axesOf.has(category)) {\n axesOf.set(category, new Set())\n }\n axesOf.get(category)!.add(axis)\n }\n\n const categories = [...axesOf.keys()].sort()\n\n /*\n * No description on either of these.\n *\n * They carried i18n keys, which the seeding path stored verbatim onto the role and the\n * list then drew as the description. And a static sentence would be worse than what the\n * screen already computes from `grants` — \"opens 30 things across 30 modules\" is exact\n * and moves when the installation does.\n */\n /*\n * VIEWER is only offered when someone has said which axes are read-only.\n *\n * This used to match the literal word `query`, which is right in this house and wrong in\n * dssp, where the read axis is spelled `read` and `kpi` has no `query` at all — VIEWER\n * would have quietly dropped that installation's main module.\n *\n * And when nobody has said, the template is **not offered at all** rather than offered\n * empty. An empty VIEWER is the quiet failure this whole design is built to avoid: an\n * administrator picks it, gets a role, no door opens, and the list counts the zero\n * correctly. \"We do not know\" is not \"there are none\".\n */\n const readOnly = readOnlyAxes()\n\n const viewer: RoleTemplate | undefined = readOnlyAxesAreKnown()\n ? {\n id: 'framework.viewer',\n name: 'VIEWER',\n grants: categories\n .map(category => ({ category, axes: [...axesOf.get(category)!].filter(axis => readOnly.has(axis)).sort() }))\n .filter(grant => grant.axes.length > 0)\n }\n : undefined\n\n /*\n * The one place that widens on purpose.\n *\n * Everywhere else an unenumerated axis is a defect waiting for the day a new axis is\n * declared. Here \"everything that exists\" is the meaning of the template, so picking up\n * a newly declared axis is correct rather than accidental.\n */\n const administrator: RoleTemplate = {\n id: 'framework.administrator',\n name: 'ADMINISTRATOR',\n grants: categories.map(category => ({ category, axes: [...axesOf.get(category)!].sort() }))\n }\n\n return viewer ? [viewer, administrator] : [administrator]\n}\n\n/** Every template this installation offers — declared by apps, plus the computed two. */\nexport function allRoleTemplates(): RegisteredRoleTemplate[] {\n return [...computedRoleTemplates(), ...roleTemplates()]\n}\n\n/**\n * **A template's privileges** — its tasks' privileges and its own grants, each privilege once.\n *\n * Every reader of a template's privileges goes through this: the role screen's gap, applying it, the view the\n * template picker draws. A task id nobody registered adds nothing here; `templateDefinitionErrors` says so, and\n * applying the template refuses.\n */\nexport function templateGrantsOf(template: RoleTemplate): RoleTemplateGrant[] {\n const tasks = (template.tasks || []).map(id => roleTaskById(id)?.grants || [])\n\n return mergeGrants([...tasks, template.grants || []])\n}\n\n/**\n * What is wrong with a template as declared, before anything is applied (ADR-0139 decision 11).\n *\n * A task id nobody registered, and a privilege a task names that this installation does not declare. A task is\n * registered only by a module that is installed, so either one is a slip in code — a task id mistyped, a module the\n * application names but does not install, a category renamed under a task — and applying the template anyway would\n * give a seat that is short of work it says it does. So these refuse the template.\n *\n * A template's own `grants` naming an undeclared privilege is not here: an application may write grants for modules\n * one installation has and another does not, and those are skipped as before (`unknownTemplateGrants`).\n */\nexport function templateDefinitionErrors(template: RoleTemplate, declared: Record<string, unknown> = process['PRIVILEGES'] || {}): string[] {\n return (template.tasks || []).flatMap(id => {\n const task = roleTaskById(id)\n if (!task) return [`task \"${id}\" is not registered`]\n return undeclaredTaskGrants(task, declared).map(grant => `task \"${id}\" names ${grant}, which this installation does not declare`)\n })\n}\n\n/** What a role lacks against a template, and what it holds beyond it - see `templateGapAgainst`. */\nexport type RoleTemplateGap = {\n templateId: string\n templateName: string\n missing: { category: string; axes: string[] }[]\n missingCount: number\n /** Declared privileges the role holds that the template does not grant — wider than the template, not wrong. */\n extra: { category: string; axes: string[] }[]\n extraCount: number\n}\n\n/**\n * **A role laid against a template** (ADR-0075 decision 1, ADR-0139 decision 11).\n *\n * A template is a seed: the role it made belongs to the administrator afterwards, and nothing\n * pushes a template's later grants into it. When a template gains a door - operato-plant's nine\n * gained ai-assistant - the roles already standing fall quietly behind. The answer is the one this\n * file's header gives: the template's current grants against the role's current privileges,\n * computed, never stored. The screen shows it and the administrator adds it; nothing adds it for them.\n *\n * `extra` is the other side: what the role holds that the template does not grant. It is not a fault —\n * a site may have widened the role on purpose — and nothing takes it away; the screen says \"wider than\n * the template\". Only privileges this installation declares count on either side: a grant it does not\n * declare is `missingCount` on the template, and a held privilege nothing declares is struck through\n * on the role screen already.\n */\nexport function templateGapAgainst(\n template: RoleTemplate,\n held: ReadonlySet<string>,\n declared: Record<string, unknown> = process['PRIVILEGES'] || {}\n): RoleTemplateGap {\n const grants = templateGrantsOf(template)\n const granted = new Set(grants.flatMap(grant => grant.axes.map(axis => privilegeKey(grant.category, axis))))\n\n const missing = grants\n .map(grant => ({\n category: grant.category,\n axes: grant.axes.filter(axis => declared[privilegeKey(grant.category, axis)] && !held.has(privilegeKey(grant.category, axis)))\n }))\n .filter(grant => grant.axes.length > 0)\n\n const extra = mergeGrants([\n [...held]\n .filter(key => declared[key] && !granted.has(key))\n .sort()\n .map(key => {\n const [category, axis] = key.split(' ')\n return { category, axes: [axis] }\n })\n ])\n\n return {\n templateId: template.id,\n templateName: template.name,\n missing,\n missingCount: missing.reduce((count, grant) => count + grant.axes.length, 0),\n extra,\n extraCount: extra.reduce((count, grant) => count + grant.axes.length, 0)\n }\n}\n\n/**\n * The gap against the template of this role's name — the pair the role list shows unasked.\n *\n * Paired by name, because provenance is not stored. A role whose name no template carries — renamed by its\n * administrator, or named in the site's own words as operato-office's roles are — has no pair here; the role\n * screen lets the administrator pick the template to lay it against (`templateGapAgainst`). The role is never\n * renamed to make a pair: approval lines hold the role, and the name is the site's.\n * `null` when no template carries this name; a gap of 0 missing is a role that is up to date.\n */\nexport function templateGapOf(\n roleName: string,\n held: ReadonlySet<string>,\n templates: RegisteredRoleTemplate[] = allRoleTemplates(),\n declared: Record<string, unknown> = process['PRIVILEGES'] || {}\n): RoleTemplateGap | null {\n const template = templates.find(one => one.name === roleName)\n return template ? templateGapAgainst(template, held, declared) : null\n}\n\n/**\n * Names a template's own grants mention that this installation does not declare.\n *\n * A template that survives a category rename becomes a role that grants nothing, and that\n * is the quietest way this design can fail — the administrator picks \"quality inspector\",\n * gets a role, and it opens no doors. operato-plant's seed already does this for its own\n * table (`seed-demo-org.ts` warns about grants it could not attach); this is the same\n * check moved to where templates are declared. A task's privileges are checked as\n * definition errors instead (`templateDefinitionErrors`).\n */\nexport function unknownTemplateGrants(template: RoleTemplate): string[] {\n const declared = process['PRIVILEGES'] || {}\n\n return (template.grants || []).flatMap(grant =>\n grant.axes.filter(axis => !declared[privilegeKey(grant.category, axis)]).map(axis => `${grant.category}:${axis}`)\n )\n}\n\n/**\n * Boot's one look at what templates and tasks name — after every module has loaded and `@privilege` has filled\n * `process['PRIVILEGES']`, the first moment both registries and the declarations are complete.\n */\nexport function reportUnknownTemplateGrants(): void {\n const unknown = roleTemplates().flatMap(template =>\n unknownTemplateGrants(template).map(grant => `${template.id} → ${grant}`)\n )\n\n if (unknown.length) {\n logger.warn(\n `[role-template] ${unknown.length} grants name privileges this installation does not declare — ` +\n `${unknown.join(', ')}. Privilege definitions come from @privilege at boot, so either the query ` +\n 'is not installed here or the name has moved. A role seeded from such a template opens nothing.'\n )\n }\n\n const broken = [\n ...roleTasks().flatMap(task => undeclaredTaskGrants(task).map(grant => `task \"${task.id}\" names ${grant}`)),\n ...roleTemplates().flatMap(template =>\n (template.tasks || []).filter(id => !roleTaskById(id)).map(id => `${template.id} → task \"${id}\" is not registered`)\n )\n ]\n\n if (broken.length) {\n logger.error(\n `[role-task] ${broken.length} definition errors — ${broken.join(', ')}. ` +\n 'A template that names any of these is refused when applied, so no seat is stood up short of the work it says it does.'\n )\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"role-template.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template.ts"],"names":[],"mappings":";;AAgHA,oDA4CC;AAED,sCAEC;AAED,4CAEC;AAUD,sDA2DC;AAGD,4CAEC;AASD,4CAIC;AAaD,4DAMC;AA4BD,gDAiCC;AAWD,sCAQC;AAYD,sDAMC;AAMD,kEA0BC;AAhZD,6CAA4C;AAE5C,sEAAmF;AACnF,iDAAyG;AA8FzG,MAAM,QAAQ,GAAG,gBAAgB,CAAA;AAEjC,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAA;AAEtB,SAAS,QAAQ;IACf,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAA;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,oBAAoB,CAAC,QAAsB;IACzD,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAA;IACjE,CAAC;IAED,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC;QACvI,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,CAAC,EAAE,8CAA8C,CAAC,CAAA;IAC9F,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CACb,kBAAkB,QAAQ,CAAC,EAAE,iEAAiE;YAC5F,4FAA4F;YAC5F,wCAAwC,CAC3C,CAAA;IACH,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9E,MAAM,IAAI,KAAK,CACb,kBAAkB,QAAQ,CAAC,EAAE,oDAAoD;gBAC/E,oCAAoC,CACvC,CAAA;QACH,CAAC;QAED;;;;;WAKG;QACH,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACrE,YAAM,CAAC,IAAI,CACT,oBAAoB,QAAQ,CAAC,EAAE,YAAY,KAAK,CAAC,QAAQ,qBAAqB,KAAK,CAAC,QAAQ,UAAU;gBACpG,sEAAsE,CACzE,CAAA;QACH,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,CAAC,EAAE,sBAAsB,CAAC,CAAA;IACtE,CAAC;IAED,QAAQ,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,CAAA;AAC1E,CAAC;AAED,SAAgB,aAAa;IAC3B,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;AAClC,CAAC;AAED,SAAgB,gBAAgB,CAAC,EAAU;IACzC,OAAO,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAA;AACvB,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,qBAAqB;IACnC,MAAM,QAAQ,GAAuB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAA;IAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAA;IAE7C,KAAK,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;QACxC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,GAAG,EAAE,CAAC,CAAA;QACjC,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAE5C;;;;;;;OAOG;IACH;;;;;;;;;;;OAWG;IACH,MAAM,QAAQ,GAAG,IAAA,gCAAY,GAAE,CAAA;IAE/B,MAAM,MAAM,GAA6B,IAAA,wCAAoB,GAAE;QAC7D,CAAC,CAAC;YACE,EAAE,EAAE,kBAAkB;YACtB,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,UAAU;iBACf,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;iBAC3G,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;SAC1C;QACH,CAAC,CAAC,SAAS,CAAA;IAEb;;;;;;OAMG;IACH,MAAM,aAAa,GAAiB;QAClC,EAAE,EAAE,yBAAyB;QAC7B,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;KAC5F,CAAA;IAED,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;AAC3D,CAAC;AAED,yFAAyF;AACzF,SAAgB,gBAAgB;IAC9B,OAAO,CAAC,GAAG,qBAAqB,EAAE,EAAE,GAAG,aAAa,EAAE,CAAC,CAAA;AACzD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,QAAsB;IACrD,MAAM,KAAK,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAA,2BAAY,EAAC,EAAE,CAAC,EAAE,MAAM,IAAI,EAAE,CAAC,CAAA;IAE9E,OAAO,IAAA,0BAAW,EAAC,CAAC,GAAG,KAAK,EAAE,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAA;AACvD,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,wBAAwB,CAAC,QAAsB,EAAE,WAAoC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE;IAC9H,OAAO,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;QACzC,MAAM,IAAI,GAAG,IAAA,2BAAY,EAAC,EAAE,CAAC,CAAA;QAC7B,IAAI,CAAC,IAAI;YAAE,OAAO,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAA;QACpD,OAAO,IAAA,mCAAoB,EAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,4CAA4C,CAAC,CAAA;IACnI,CAAC,CAAC,CAAA;AACJ,CAAC;AAaD;;;;;;;;;;;;;;GAcG;AACH,SAAgB,kBAAkB,CAChC,QAAsB,EACtB,IAAyB,EACzB,WAAoC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE;IAE/D,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IACzC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAA,2BAAY,EAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAE5G,MAAM,OAAO,GAAG,MAAM;SACnB,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACb,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAA,2BAAY,EAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAA,2BAAY,EAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;KAC/H,CAAC,CAAC;SACF,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAEzC,MAAM,KAAK,GAAG,IAAA,0BAAW,EAAC;QACxB,CAAC,GAAG,IAAI,CAAC;aACN,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;aACjD,IAAI,EAAE;aACN,GAAG,CAAC,GAAG,CAAC,EAAE;YACT,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACvC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAA;QACnC,CAAC,CAAC;KACL,CAAC,CAAA;IAEF,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,EAAE;QACvB,YAAY,EAAE,QAAQ,CAAC,IAAI;QAC3B,OAAO;QACP,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5E,KAAK;QACL,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;KACzE,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,aAAa,CAC3B,UAAqC,EACrC,IAAyB,EACzB,YAAsC,gBAAgB,EAAE,EACxD,WAAoC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE;IAE/D,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACtF,OAAO,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACvE,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,qBAAqB,CAAC,QAAsB;IAC1D,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,CAAA;IAE5C,OAAO,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAC7C,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAA,2BAAY,EAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC,CAClH,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,2BAA2B;IACzC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CACjD,qBAAqB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,MAAM,KAAK,EAAE,CAAC,CAC1E,CAAA;IAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,YAAM,CAAC,IAAI,CACT,mBAAmB,OAAO,CAAC,MAAM,+DAA+D;YAC9F,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,4EAA4E;YACjG,gGAAgG,CACnG,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAG;QACb,GAAG,IAAA,wBAAS,GAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAA,mCAAoB,EAAC,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,IAAI,CAAC,EAAE,WAAW,KAAK,EAAE,CAAC,CAAC;QAC3G,GAAG,aAAa,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CACpC,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,IAAA,2BAAY,EAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,YAAY,EAAE,qBAAqB,CAAC,CACpH;KACF,CAAA;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,YAAM,CAAC,KAAK,CACV,eAAe,MAAM,CAAC,MAAM,wBAAwB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YACvE,uHAAuH,CAC1H,CAAA;IACH,CAAC;AACH,CAAC","sourcesContent":["import { logger } from '@things-factory/env'\n\nimport { readOnlyAxes, readOnlyAxesAreKnown } from '../privilege/privilege-axis.js'\nimport { mergeGrants, privilegeKey, roleTaskById, roleTasks, undeclaredTaskGrants } from './role-task.js'\n\n/**\n * Role templates — the seeds an application offers so a new domain does not start with\n * zero roles.\n *\n * ── Why this is a boot registry and not a table ───────────────────────────────\n * A template is a declaration. Copying it into rows per domain gives 100 domains x 3\n * templates = 300 rows, and once the code changes the template there is nowhere to ask\n * which of the two is true: the declaration, or the row that was written from an older\n * one. `@privilege` lives in the boot registry for the same reason.\n *\n * ── What a template is not ───────────────────────────────────────────────────\n * It is a seed, not a role. Once it has been used the resulting role belongs to the\n * administrator: the template is never re-applied and never overwrites. The role keeps only\n * which template it is based on (`Role.templateId`, one at most); what an administrator wants\n * from that (\"the template gained two privileges, here they are\") is the template's current\n * grants against the role's current privileges, and that is computed rather than stored.\n *\n * ── A role is one thing seen from three sides ────────────────────────────────\n * operato-plant uses roles as privilege bundles; operato-twin uses them as the seat an\n * approval line points at (`worklist` resolves an approver of type `Role` by id). Those\n * are not two concepts. A role is a seat in the organisation: what it opens, who fills it,\n * and whether approvals route to it. Splitting them would put two QUALITY-INSPECTORs in\n * one plant and force every person into both.\n */\n\n/** One module and the axes this template grants on it. Axes are always enumerated. */\nexport type RoleTemplateGrant = {\n /** `@privilege(category:)` — what is guarded, not the package name. */\n category: string\n /**\n * `query` / `mutation` / ... — listed one by one.\n *\n * There is deliberately no `'all'`. A template written as \"everything\" would widen on\n * its own the day a new axis is declared, with nobody having touched it. The single\n * place that may widen is the framework-computed administrator template, which means\n * \"whatever exists now\" by definition.\n */\n axes: string[]\n}\n\nexport type RoleTemplate = {\n /** Declaration key, namespaced by the declaring app: `plant.quality-inspector`. */\n id: string\n /**\n * The role name created in the domain by default: `QUALITY-INSPECTOR`.\n *\n * A role is paired with its template by `Role.templateId`, never by this name. Renaming\n * here still makes the next default-named role a second one beside the first, which\n * approval lines may already point at — renaming is a migration, not a template edit.\n */\n name: string\n /**\n * Plain text, not an i18n key.\n *\n * `createRoleFromTemplate` copies this onto the role it seeds, and the role list draws\n * the role's description as a value — so a key written here reaches the screen as\n * `label.role-template.administrator`. That is exactly what the framework's own two\n * templates did until 2026-09-11.\n *\n * operato-plant had it right from the start: \"검사 기준과 처분을 정하는 자리\".\n */\n description?: string\n /**\n * Required unless `tasks` is written, and may be empty.\n *\n * `[]` is a statement: this seat opens nothing, it exists for approval routing. A\n * missing `grants` is a different thing — someone forgot — and is rejected here, so the\n * screen can tell \"opens nothing on purpose\" from \"nothing was written down\".\n *\n * Read a template's privileges through `templateGrantsOf`, never from here: with `tasks`\n * this is only the part written directly.\n */\n grants?: RoleTemplateGrant[]\n /**\n * The work this seat does, by task id (`registerRoleTask`, ADR-0139 decision 11).\n *\n * The seat holds every privilege of every task here, plus `grants`, each privilege once.\n * A task id nobody registered is a definition error: boot reports it and the template is\n * not applied at all, rather than applied without that work.\n */\n tasks?: string[]\n}\n\n/**\n * Registered form of a template.\n *\n * Identical to the declaration today. It stays a separate name because the registry is\n * what every reader goes through, and giving it its own type keeps a later addition from\n * rippling into every call site.\n */\nexport type RegisteredRoleTemplate = RoleTemplate\n\nconst REGISTRY = 'ROLE_TEMPLATES'\n\nprocess[REGISTRY] = {}\n\nfunction registry(): Record<string, RegisteredRoleTemplate> {\n return process[REGISTRY]\n}\n\n/**\n * Declares a role template. Call it from the application's server entry.\n *\n * Categories are not checked here: registration runs while modules load, before the\n * schema is built and therefore before `process['PRIVILEGES']` exists. The name check\n * happens at boot instead (`reportUnknownTemplateGrants`).\n */\nexport function registerRoleTemplate(template: RoleTemplate): void {\n if (!template?.id || !template?.name) {\n throw new Error('A role template needs both an id and a name.')\n }\n\n if (template.tasks !== undefined && (!Array.isArray(template.tasks) || template.tasks.some(task => !task || typeof task !== 'string'))) {\n throw new Error(`Role template \"${template.id}\" has tasks that are not a list of task ids.`)\n }\n\n if (!Array.isArray(template.grants) && !template.tasks?.length) {\n throw new Error(\n `Role template \"${template.id}\" has no grants. Write \\`grants: []\\` if this seat is meant to ` +\n 'open nothing — leaving it out is indistinguishable from forgetting, and the screen cannot ' +\n 'tell an administrator which one it is.'\n )\n }\n\n for (const grant of template.grants || []) {\n if (!grant?.category || !Array.isArray(grant.axes) || grant.axes.length === 0) {\n throw new Error(\n `Role template \"${template.id}\" has a grant without a category or with no axes. ` +\n 'Axes are always listed one by one.'\n )\n }\n\n /*\n * Across the nine roles operato-plant declares, no role is granted `mutation` on a\n * category without `query` on the same category — you cannot fix what you cannot see.\n * A template that does so is almost certainly a slip, so say it out loud rather than\n * silently granting a half-usable seat.\n */\n if (grant.axes.includes('mutation') && !grant.axes.includes('query')) {\n logger.warn(\n `[role-template] \"${template.id}\" grants ${grant.category}:mutation without ${grant.category}:query. ` +\n 'Someone who may change a thing but not see it can rarely use either.'\n )\n }\n }\n\n if (registry()[template.id]) {\n throw new Error(`Role template \"${template.id}\" is declared twice.`)\n }\n\n registry()[template.id] = { ...template, grants: template.grants || [] }\n}\n\nexport function roleTemplates(): RegisteredRoleTemplate[] {\n return Object.values(registry())\n}\n\nexport function roleTemplateById(id: string): RegisteredRoleTemplate | undefined {\n return registry()[id]\n}\n\n/**\n * The two templates the framework can build without knowing a single category name.\n *\n * Everything else has to be declared by an application: a job title spans modules\n * (a production lead uses worklist *and* order *and* ops-master) and no one package knows\n * that. These two are different — they are defined over whatever is declared, so they can\n * be computed from the registry `@privilege` fills.\n */\nexport function computedRoleTemplates(): RegisteredRoleTemplate[] {\n const declared: [string, string][] = Object.values(process['PRIVILEGES'] || {})\n const axesOf = new Map<string, Set<string>>()\n\n for (const [category, axis] of declared) {\n if (!axesOf.has(category)) {\n axesOf.set(category, new Set())\n }\n axesOf.get(category)!.add(axis)\n }\n\n const categories = [...axesOf.keys()].sort()\n\n /*\n * No description on either of these.\n *\n * They carried i18n keys, which the seeding path stored verbatim onto the role and the\n * list then drew as the description. And a static sentence would be worse than what the\n * screen already computes from `grants` — \"opens 30 things across 30 modules\" is exact\n * and moves when the installation does.\n */\n /*\n * VIEWER is only offered when someone has said which axes are read-only.\n *\n * This used to match the literal word `query`, which is right in this house and wrong in\n * dssp, where the read axis is spelled `read` and `kpi` has no `query` at all — VIEWER\n * would have quietly dropped that installation's main module.\n *\n * And when nobody has said, the template is **not offered at all** rather than offered\n * empty. An empty VIEWER is the quiet failure this whole design is built to avoid: an\n * administrator picks it, gets a role, no door opens, and the list counts the zero\n * correctly. \"We do not know\" is not \"there are none\".\n */\n const readOnly = readOnlyAxes()\n\n const viewer: RoleTemplate | undefined = readOnlyAxesAreKnown()\n ? {\n id: 'framework.viewer',\n name: 'VIEWER',\n grants: categories\n .map(category => ({ category, axes: [...axesOf.get(category)!].filter(axis => readOnly.has(axis)).sort() }))\n .filter(grant => grant.axes.length > 0)\n }\n : undefined\n\n /*\n * The one place that widens on purpose.\n *\n * Everywhere else an unenumerated axis is a defect waiting for the day a new axis is\n * declared. Here \"everything that exists\" is the meaning of the template, so picking up\n * a newly declared axis is correct rather than accidental.\n */\n const administrator: RoleTemplate = {\n id: 'framework.administrator',\n name: 'ADMINISTRATOR',\n grants: categories.map(category => ({ category, axes: [...axesOf.get(category)!].sort() }))\n }\n\n return viewer ? [viewer, administrator] : [administrator]\n}\n\n/** Every template this installation offers — declared by apps, plus the computed two. */\nexport function allRoleTemplates(): RegisteredRoleTemplate[] {\n return [...computedRoleTemplates(), ...roleTemplates()]\n}\n\n/**\n * **A template's privileges** — its tasks' privileges and its own grants, each privilege once.\n *\n * Every reader of a template's privileges goes through this: the role screen's gap, applying it, the view the\n * template picker draws. A task id nobody registered adds nothing here; `templateDefinitionErrors` says so, and\n * applying the template refuses.\n */\nexport function templateGrantsOf(template: RoleTemplate): RoleTemplateGrant[] {\n const tasks = (template.tasks || []).map(id => roleTaskById(id)?.grants || [])\n\n return mergeGrants([...tasks, template.grants || []])\n}\n\n/**\n * What is wrong with a template as declared, before anything is applied (ADR-0139 decision 11).\n *\n * A task id nobody registered, and a privilege a task names that this installation does not declare. A task is\n * registered only by a module that is installed, so either one is a slip in code — a task id mistyped, a module the\n * application names but does not install, a category renamed under a task — and applying the template anyway would\n * give a seat that is short of work it says it does. So these refuse the template.\n *\n * A template's own `grants` naming an undeclared privilege is not here: an application may write grants for modules\n * one installation has and another does not, and those are skipped as before (`unknownTemplateGrants`).\n */\nexport function templateDefinitionErrors(template: RoleTemplate, declared: Record<string, unknown> = process['PRIVILEGES'] || {}): string[] {\n return (template.tasks || []).flatMap(id => {\n const task = roleTaskById(id)\n if (!task) return [`task \"${id}\" is not registered`]\n return undeclaredTaskGrants(task, declared).map(grant => `task \"${id}\" names ${grant}, which this installation does not declare`)\n })\n}\n\n/** What a role lacks against a template, and what it holds beyond it - see `templateGapAgainst`. */\nexport type RoleTemplateGap = {\n templateId: string\n templateName: string\n missing: { category: string; axes: string[] }[]\n missingCount: number\n /** Declared privileges the role holds that the template does not grant — wider than the template, not wrong. */\n extra: { category: string; axes: string[] }[]\n extraCount: number\n}\n\n/**\n * **A role laid against a template** (ADR-0075 decision 1, ADR-0139 decision 11).\n *\n * A template is a seed: the role it made belongs to the administrator afterwards, and nothing\n * pushes a template's later grants into it. When a template gains a door - operato-plant's nine\n * gained ai-assistant - the roles already standing fall quietly behind. The answer is the one this\n * file's header gives: the template's current grants against the role's current privileges,\n * computed, never stored. The screen shows it and the administrator adds it; nothing adds it for them.\n *\n * `extra` is the other side: what the role holds that the template does not grant. It is not a fault —\n * a site may have widened the role on purpose — and nothing takes it away; the screen says \"wider than\n * the template\". Only privileges this installation declares count on either side: a grant it does not\n * declare is `missingCount` on the template, and a held privilege nothing declares is struck through\n * on the role screen already.\n */\nexport function templateGapAgainst(\n template: RoleTemplate,\n held: ReadonlySet<string>,\n declared: Record<string, unknown> = process['PRIVILEGES'] || {}\n): RoleTemplateGap {\n const grants = templateGrantsOf(template)\n const granted = new Set(grants.flatMap(grant => grant.axes.map(axis => privilegeKey(grant.category, axis))))\n\n const missing = grants\n .map(grant => ({\n category: grant.category,\n axes: grant.axes.filter(axis => declared[privilegeKey(grant.category, axis)] && !held.has(privilegeKey(grant.category, axis)))\n }))\n .filter(grant => grant.axes.length > 0)\n\n const extra = mergeGrants([\n [...held]\n .filter(key => declared[key] && !granted.has(key))\n .sort()\n .map(key => {\n const [category, axis] = key.split(' ')\n return { category, axes: [axis] }\n })\n ])\n\n return {\n templateId: template.id,\n templateName: template.name,\n missing,\n missingCount: missing.reduce((count, grant) => count + grant.axes.length, 0),\n extra,\n extraCount: extra.reduce((count, grant) => count + grant.axes.length, 0)\n }\n}\n\n/**\n * The gap against the role's base template (`Role.templateId`) — what the role list shows unasked.\n *\n * Paired by the stored base, never by name (ADR-0075 decision 1, amended 2026-10-26): operato-office's roles are\n * named in the site's own words, and pairing by name left them without a gap to show. The role is never renamed\n * to make a pair: approval lines hold the role, and the name is the site's.\n * `null` when the role has no base, or its base is not declared in this installation; a gap of 0 missing is a\n * role that is up to date.\n */\nexport function templateGapOf(\n templateId: string | null | undefined,\n held: ReadonlySet<string>,\n templates: RegisteredRoleTemplate[] = allRoleTemplates(),\n declared: Record<string, unknown> = process['PRIVILEGES'] || {}\n): RoleTemplateGap | null {\n const template = templateId ? templates.find(one => one.id === templateId) : undefined\n return template ? templateGapAgainst(template, held, declared) : null\n}\n\n/**\n * Names a template's own grants mention that this installation does not declare.\n *\n * A template that survives a category rename becomes a role that grants nothing, and that\n * is the quietest way this design can fail — the administrator picks \"quality inspector\",\n * gets a role, and it opens no doors. operato-plant's seed already does this for its own\n * table (`seed-demo-org.ts` warns about grants it could not attach); this is the same\n * check moved to where templates are declared. A task's privileges are checked as\n * definition errors instead (`templateDefinitionErrors`).\n */\nexport function unknownTemplateGrants(template: RoleTemplate): string[] {\n const declared = process['PRIVILEGES'] || {}\n\n return (template.grants || []).flatMap(grant =>\n grant.axes.filter(axis => !declared[privilegeKey(grant.category, axis)]).map(axis => `${grant.category}:${axis}`)\n )\n}\n\n/**\n * Boot's one look at what templates and tasks name — after every module has loaded and `@privilege` has filled\n * `process['PRIVILEGES']`, the first moment both registries and the declarations are complete.\n */\nexport function reportUnknownTemplateGrants(): void {\n const unknown = roleTemplates().flatMap(template =>\n unknownTemplateGrants(template).map(grant => `${template.id} → ${grant}`)\n )\n\n if (unknown.length) {\n logger.warn(\n `[role-template] ${unknown.length} grants name privileges this installation does not declare — ` +\n `${unknown.join(', ')}. Privilege definitions come from @privilege at boot, so either the query ` +\n 'is not installed here or the name has moved. A role seeded from such a template opens nothing.'\n )\n }\n\n const broken = [\n ...roleTasks().flatMap(task => undeclaredTaskGrants(task).map(grant => `task \"${task.id}\" names ${grant}`)),\n ...roleTemplates().flatMap(template =>\n (template.tasks || []).filter(id => !roleTaskById(id)).map(id => `${template.id} → task \"${id}\" is not registered`)\n )\n ]\n\n if (broken.length) {\n logger.error(\n `[role-task] ${broken.length} definition errors — ${broken.join(', ')}. ` +\n 'A template that names any of these is refused when applied, so no seat is stood up short of the work it says it does.'\n )\n }\n}\n"]}
|