@things-factory/auth-base 10.1.215 → 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.
Files changed (132) hide show
  1. package/config/config.development.js +2 -4
  2. package/config/config.production.js +1 -2
  3. package/dist-client/actions/auth.d.ts +1 -0
  4. package/dist-client/actions/auth.js +6 -0
  5. package/dist-client/actions/auth.js.map +1 -1
  6. package/dist-client/reducers/auth.d.ts +8 -0
  7. package/dist-client/reducers/auth.js +8 -2
  8. package/dist-client/reducers/auth.js.map +1 -1
  9. package/dist-client/tsconfig.tsbuildinfo +1 -1
  10. package/dist-server/constants/error-code.d.ts +1 -0
  11. package/dist-server/constants/error-code.js +2 -1
  12. package/dist-server/constants/error-code.js.map +1 -1
  13. package/dist-server/controllers/auth.d.ts +0 -1
  14. package/dist-server/controllers/auth.js +0 -1
  15. package/dist-server/controllers/auth.js.map +1 -1
  16. package/dist-server/controllers/password-setup.d.ts +33 -0
  17. package/dist-server/controllers/password-setup.js +77 -0
  18. package/dist-server/controllers/password-setup.js.map +1 -0
  19. package/dist-server/controllers/reset-password.d.ts +8 -1
  20. package/dist-server/controllers/reset-password.js +21 -11
  21. package/dist-server/controllers/reset-password.js.map +1 -1
  22. package/dist-server/controllers/signin.js +10 -0
  23. package/dist-server/controllers/signin.js.map +1 -1
  24. package/dist-server/index.d.ts +1 -0
  25. package/dist-server/index.js +2 -0
  26. package/dist-server/index.js.map +1 -1
  27. package/dist-server/middlewares/jwt-authenticate-middleware.js +8 -25
  28. package/dist-server/middlewares/jwt-authenticate-middleware.js.map +1 -1
  29. package/dist-server/router/auth-public-process-router.js +3 -41
  30. package/dist-server/router/auth-public-process-router.js.map +1 -1
  31. package/dist-server/service/access-request/access-request.js +2 -2
  32. package/dist-server/service/access-request/access-request.js.map +1 -1
  33. package/dist-server/service/audit/audit-kinds.js +1 -0
  34. package/dist-server/service/audit/audit-kinds.js.map +1 -1
  35. package/dist-server/service/audit/audit-subscriber.d.ts +1 -7
  36. package/dist-server/service/audit/audit-subscriber.js +2 -33
  37. package/dist-server/service/audit/audit-subscriber.js.map +1 -1
  38. package/dist-server/service/audit/index.d.ts +1 -1
  39. package/dist-server/service/index.d.ts +2 -4
  40. package/dist-server/service/index.js +22 -27
  41. package/dist-server/service/index.js.map +1 -1
  42. package/dist-server/service/open-fields.js +0 -2
  43. package/dist-server/service/open-fields.js.map +1 -1
  44. package/dist-server/service/role/role-mutation.js +5 -0
  45. package/dist-server/service/role/role-mutation.js.map +1 -1
  46. package/dist-server/service/role/role-types.d.ts +1 -0
  47. package/dist-server/service/role/role-types.js +4 -0
  48. package/dist-server/service/role/role-types.js.map +1 -1
  49. package/dist-server/service/role/role.d.ts +10 -0
  50. package/dist-server/service/role/role.js +5 -0
  51. package/dist-server/service/role/role.js.map +1 -1
  52. package/dist-server/service/role-template/role-template-gap-resolver.d.ts +2 -2
  53. package/dist-server/service/role-template/role-template-gap-resolver.js +7 -13
  54. package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
  55. package/dist-server/service/role-template/role-template-mutation.d.ts +7 -4
  56. package/dist-server/service/role-template/role-template-mutation.js +15 -15
  57. package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
  58. package/dist-server/service/role-template/role-template-types.d.ts +3 -3
  59. package/dist-server/service/role-template/role-template-types.js.map +1 -1
  60. package/dist-server/service/role-template/role-template.d.ts +14 -15
  61. package/dist-server/service/role-template/role-template.js +8 -8
  62. package/dist-server/service/role-template/role-template.js.map +1 -1
  63. package/dist-server/service/user/user-mutation.d.ts +27 -6
  64. package/dist-server/service/user/user-mutation.js +26 -114
  65. package/dist-server/service/user/user-mutation.js.map +1 -1
  66. package/dist-server/service/user/user-query.d.ts +3 -4
  67. package/dist-server/service/user/user-query.js +7 -32
  68. package/dist-server/service/user/user-query.js.map +1 -1
  69. package/dist-server/service/user/user-types.d.ts +6 -2
  70. package/dist-server/service/user/user-types.js +20 -9
  71. package/dist-server/service/user/user-types.js.map +1 -1
  72. package/dist-server/service/user/user.js +17 -8
  73. package/dist-server/service/user/user.js.map +1 -1
  74. package/dist-server/service/verification-token/verification-token.d.ts +1 -0
  75. package/dist-server/service/verification-token/verification-token.js +1 -0
  76. package/dist-server/service/verification-token/verification-token.js.map +1 -1
  77. package/dist-server/templates/reset-password-email.d.ts +10 -3
  78. package/dist-server/templates/reset-password-email.js +8 -4
  79. package/dist-server/templates/reset-password-email.js.map +1 -1
  80. package/dist-server/tsconfig.tsbuildinfo +1 -1
  81. package/dist-server/utils/verification-token-rule.js +3 -1
  82. package/dist-server/utils/verification-token-rule.js.map +1 -1
  83. package/package.json +2 -2
  84. package/tests/add-user-gates.test.ts +34 -0
  85. package/tests/list-inheritance-db.test.ts +0 -15
  86. package/tests/lock-recovery.test.ts +2 -2
  87. package/tests/open-doors.test.ts +2 -5
  88. package/tests/password-reset-rule-db.test.ts +17 -16
  89. package/tests/password-setup-db.test.ts +213 -0
  90. package/tests/picture-reducer.test.ts +25 -0
  91. package/tests/role-template-gap-db.test.ts +58 -7
  92. package/tests/role-template-seed-db.test.ts +5 -4
  93. package/tests/verification-token-db.test.ts +12 -7
  94. package/translations/en.json +2 -2
  95. package/translations/es.json +2 -2
  96. package/translations/ja.json +2 -2
  97. package/translations/ko.json +2 -2
  98. package/translations/ms.json +2 -2
  99. package/translations/zh.json +2 -2
  100. package/dist-server/controllers/invitation.d.ts +0 -75
  101. package/dist-server/controllers/invitation.js +0 -195
  102. package/dist-server/controllers/invitation.js.map +0 -1
  103. package/dist-server/controllers/utils/make-invitation-token.d.ts +0 -1
  104. package/dist-server/controllers/utils/make-invitation-token.js +0 -9
  105. package/dist-server/controllers/utils/make-invitation-token.js.map +0 -1
  106. package/dist-server/controllers/utils/save-invitation-token.d.ts +0 -5
  107. package/dist-server/controllers/utils/save-invitation-token.js +0 -12
  108. package/dist-server/controllers/utils/save-invitation-token.js.map +0 -1
  109. package/dist-server/service/invitation/index.d.ts +0 -5
  110. package/dist-server/service/invitation/index.js +0 -9
  111. package/dist-server/service/invitation/index.js.map +0 -1
  112. package/dist-server/service/invitation/invitation-mutation.d.ts +0 -20
  113. package/dist-server/service/invitation/invitation-mutation.js +0 -109
  114. package/dist-server/service/invitation/invitation-mutation.js.map +0 -1
  115. package/dist-server/service/invitation/invitation-query.d.ts +0 -23
  116. package/dist-server/service/invitation/invitation-query.js +0 -89
  117. package/dist-server/service/invitation/invitation-query.js.map +0 -1
  118. package/dist-server/service/invitation/invitation-types.d.ts +0 -5
  119. package/dist-server/service/invitation/invitation-types.js +0 -21
  120. package/dist-server/service/invitation/invitation-types.js.map +0 -1
  121. package/dist-server/service/invitation/invitation.d.ts +0 -26
  122. package/dist-server/service/invitation/invitation.js +0 -142
  123. package/dist-server/service/invitation/invitation.js.map +0 -1
  124. package/dist-server/templates/invitation-email.d.ts +0 -6
  125. package/dist-server/templates/invitation-email.js +0 -82
  126. package/dist-server/templates/invitation-email.js.map +0 -1
  127. package/dist-server/utils/invitation-state.d.ts +0 -52
  128. package/dist-server/utils/invitation-state.js +0 -76
  129. package/dist-server/utils/invitation-state.js.map +0 -1
  130. package/tests/invitation-db.test.ts +0 -416
  131. package/tests/invitation-route.test.ts +0 -116
  132. package/tests/invitation-state.test.ts +0 -92
@@ -50,6 +50,10 @@ tslib_1.__decorate([
50
50
  (0, type_graphql_1.Field)({ nullable: true, description: 'The new description for the role.' }),
51
51
  tslib_1.__metadata("design:type", String)
52
52
  ], RolePatch.prototype, "description", void 0);
53
+ tslib_1.__decorate([
54
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The id of the role template this role is based on; null to base it on none.' }),
55
+ tslib_1.__metadata("design:type", String)
56
+ ], RolePatch.prototype, "templateId", void 0);
53
57
  tslib_1.__decorate([
54
58
  (0, type_graphql_1.Field)(type => [shell_1.ObjectRef], { nullable: true, description: 'A new list of users to be assigned to the role.' }),
55
59
  tslib_1.__metadata("design:type", Array)
@@ -1 +1 @@
1
- {"version":3,"file":"role-types.js","sourceRoot":"","sources":["../../../server/service/role/role-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { ObjectType, InputType, Field, Int } from 'type-graphql'\nimport { ObjectRef } from '@things-factory/shell'\nimport { Role } from './role.js'\n\n@InputType({ description: 'Input for creating a new role.' })\nexport class NewRole {\n @Field({ description: 'The name of the new role.' })\n name: string\n\n @Field({ nullable: true, description: 'A description for the new role.' })\n description?: string\n\n @Field(type => [ObjectRef], { nullable: true, description: 'Users to be assigned to the new role.' })\n users?: ObjectRef[]\n\n @Field(type => [ObjectRef], { nullable: true, description: 'Privileges to be granted by the new role.' })\n privileges?: ObjectRef[]\n\n /*\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3). The name stays the same — that is the point of it, because moving a\n * domain off an inherited structure means standing up the same names here first (decision 2).\n * Absent means not said, and an inherited name is refused.\n */\n @Field({ nullable: true, description: \"Stand this role even though the domain inherits one of the same name.\" })\n separately?: boolean\n}\n\n@InputType({ description: 'Input for updating (patching) an existing role.' })\nexport class RolePatch {\n @Field({ nullable: true, description: 'The unique identifier of the role to update.' })\n id?: string\n\n /*\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3). The name stays the same — that is the point of it, because moving a\n * domain off an inherited structure means standing up the same names here first (decision 2).\n * Absent means not said, and an inherited name is refused.\n */\n @Field({ nullable: true, description: \"Stand this role even though the domain inherits one of the same name.\" })\n separately?: boolean\n\n @Field({ nullable: true, description: 'The new name for the role.' })\n name?: string\n\n @Field({ nullable: true, description: 'The new description for the role.' })\n description?: string\n\n @Field(type => [ObjectRef], { nullable: true, description: 'A new list of users to be assigned to the role.' })\n users?: ObjectRef[]\n\n @Field(type => [ObjectRef], { nullable: true, description: 'A new list of privileges to be granted by the role.' })\n privileges?: ObjectRef[]\n\n @Field({ nullable: true, description: 'A flag indicating whether the role is being created, updated, or deleted.' })\n cuFlag?: string\n}\n\n@ObjectType({ description: 'A paginated list of roles.' })\nexport class RoleList {\n @Field(type => [Role], { nullable: true, description: 'The list of role items.' })\n items?: Role[]\n\n @Field(type => Int, { nullable: true, description: 'The total number of roles.' })\n total?: number\n}\n\n@ObjectType({ description: 'A brief summary of a role, containing only the ID and name.' })\nexport class RoleBrief {\n @Field({ nullable: true, description: 'The unique identifier of the role.' })\n id?: string\n\n @Field({ nullable: true, description: 'The name of the role.' })\n name?: string\n}\n\n@ObjectType({ description: 'Represents a privilege and whether it is assigned to a specific role.' })\nexport class RolePrivilege {\n @Field({ nullable: true, description: 'The unique identifier of the privilege.' })\n id?: string\n\n @Field({ nullable: true, description: 'The name of the privilege.' })\n name?: string\n\n @Field({ nullable: true, description: 'The category of the privilege.' })\n category?: string\n\n @Field({ nullable: true, description: 'A description of the privilege.' })\n description?: string\n\n @Field({ nullable: true, description: 'Indicates whether the privilege is assigned to the role.' })\n assigned?: boolean\n}\n\n@ObjectType({ description: 'Represents a role and whether it is assigned to a specific user.' })\nexport class UserRole {\n @Field({ nullable: true, description: 'The unique identifier of the role.' })\n id?: string\n\n @Field({ nullable: true, description: 'The name of the role.' })\n name?: string\n\n @Field({ nullable: true, description: 'A description of the role.' })\n description?: string\n\n @Field({ nullable: true, description: 'Indicates whether the role is assigned to the user.' })\n assigned?: boolean\n}\n"]}
1
+ {"version":3,"file":"role-types.js","sourceRoot":"","sources":["../../../server/service/role/role-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { ObjectType, InputType, Field, Int } from 'type-graphql'\nimport { ObjectRef } from '@things-factory/shell'\nimport { Role } from './role.js'\n\n@InputType({ description: 'Input for creating a new role.' })\nexport class NewRole {\n @Field({ description: 'The name of the new role.' })\n name: string\n\n @Field({ nullable: true, description: 'A description for the new role.' })\n description?: string\n\n @Field(type => [ObjectRef], { nullable: true, description: 'Users to be assigned to the new role.' })\n users?: ObjectRef[]\n\n @Field(type => [ObjectRef], { nullable: true, description: 'Privileges to be granted by the new role.' })\n privileges?: ObjectRef[]\n\n /*\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3). The name stays the same — that is the point of it, because moving a\n * domain off an inherited structure means standing up the same names here first (decision 2).\n * Absent means not said, and an inherited name is refused.\n */\n @Field({ nullable: true, description: \"Stand this role even though the domain inherits one of the same name.\" })\n separately?: boolean\n}\n\n@InputType({ description: 'Input for updating (patching) an existing role.' })\nexport class RolePatch {\n @Field({ nullable: true, description: 'The unique identifier of the role to update.' })\n id?: string\n\n /*\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3). The name stays the same — that is the point of it, because moving a\n * domain off an inherited structure means standing up the same names here first (decision 2).\n * Absent means not said, and an inherited name is refused.\n */\n @Field({ nullable: true, description: \"Stand this role even though the domain inherits one of the same name.\" })\n separately?: boolean\n\n @Field({ nullable: true, description: 'The new name for the role.' })\n name?: string\n\n @Field({ nullable: true, description: 'The new description for the role.' })\n description?: string\n\n /* `updateRole` only: null clears it. The grid's `updateMultipleRole` edits names and descriptions and ignores this. */\n @Field({ nullable: true, description: 'The id of the role template this role is based on; null to base it on none.' })\n templateId?: string\n\n @Field(type => [ObjectRef], { nullable: true, description: 'A new list of users to be assigned to the role.' })\n users?: ObjectRef[]\n\n @Field(type => [ObjectRef], { nullable: true, description: 'A new list of privileges to be granted by the role.' })\n privileges?: ObjectRef[]\n\n @Field({ nullable: true, description: 'A flag indicating whether the role is being created, updated, or deleted.' })\n cuFlag?: string\n}\n\n@ObjectType({ description: 'A paginated list of roles.' })\nexport class RoleList {\n @Field(type => [Role], { nullable: true, description: 'The list of role items.' })\n items?: Role[]\n\n @Field(type => Int, { nullable: true, description: 'The total number of roles.' })\n total?: number\n}\n\n@ObjectType({ description: 'A brief summary of a role, containing only the ID and name.' })\nexport class RoleBrief {\n @Field({ nullable: true, description: 'The unique identifier of the role.' })\n id?: string\n\n @Field({ nullable: true, description: 'The name of the role.' })\n name?: string\n}\n\n@ObjectType({ description: 'Represents a privilege and whether it is assigned to a specific role.' })\nexport class RolePrivilege {\n @Field({ nullable: true, description: 'The unique identifier of the privilege.' })\n id?: string\n\n @Field({ nullable: true, description: 'The name of the privilege.' })\n name?: string\n\n @Field({ nullable: true, description: 'The category of the privilege.' })\n category?: string\n\n @Field({ nullable: true, description: 'A description of the privilege.' })\n description?: string\n\n @Field({ nullable: true, description: 'Indicates whether the privilege is assigned to the role.' })\n assigned?: boolean\n}\n\n@ObjectType({ description: 'Represents a role and whether it is assigned to a specific user.' })\nexport class UserRole {\n @Field({ nullable: true, description: 'The unique identifier of the role.' })\n id?: string\n\n @Field({ nullable: true, description: 'The name of the role.' })\n name?: string\n\n @Field({ nullable: true, description: 'A description of the role.' })\n description?: string\n\n @Field({ nullable: true, description: 'Indicates whether the role is assigned to the user.' })\n assigned?: boolean\n}\n"]}
@@ -9,6 +9,16 @@ export declare class Role {
9
9
  users: User[];
10
10
  privileges: Privilege[];
11
11
  description: string;
12
+ /**
13
+ * The role template this role is based on - its declaration id (`office.finance`), one at most (ADR-0075 decision 1,
14
+ * amended 2026-10-26). The role list counts what the role lacks against it and the role screen lays the role against it.
15
+ *
16
+ * It is stored because the name cannot say it: a role named in the site's own words (경영지원) has no template of
17
+ * that name, and the role list never told anyone it had fallen behind. It is not a foreign key - a template is a
18
+ * declaration in code, not a row. An id no installed app declares any more stays here and the role screen says so;
19
+ * nothing clears it on its own.
20
+ */
21
+ templateId?: string;
12
22
  creator: User;
13
23
  creatorId: string;
14
24
  updater: User;
@@ -47,6 +47,11 @@ tslib_1.__decorate([
47
47
  (0, type_graphql_1.Field)({ nullable: true, description: 'A description of the role.' }),
48
48
  tslib_1.__metadata("design:type", String)
49
49
  ], Role.prototype, "description", void 0);
50
+ tslib_1.__decorate([
51
+ (0, typeorm_1.Column)({ nullable: true }),
52
+ (0, type_graphql_1.Field)({ nullable: true, description: 'The id of the role template this role is based on; null when it is based on none.' }),
53
+ tslib_1.__metadata("design:type", String)
54
+ ], Role.prototype, "templateId", void 0);
50
55
  tslib_1.__decorate([
51
56
  (0, typeorm_1.ManyToOne)(type => user_js_1.User, { nullable: true }),
52
57
  (0, type_graphql_1.Field)(type => user_js_1.User, { nullable: true, description: 'The user who created the role.' }),
@@ -1 +1 @@
1
- {"version":3,"file":"role.js","sourceRoot":"","sources":["../../../server/service/role/role.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain } from '@things-factory/shell'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n JoinTable,\n ManyToMany,\n ManyToOne,\n RelationId,\n PrimaryGeneratedColumn,\n UpdateDateColumn\n} from 'typeorm'\nimport { Directive, ObjectType, Field, ID } from 'type-graphql'\nimport { Privilege } from '../privilege/privilege.js'\nimport { User } from '../user/user.js'\n\n@Entity()\n@Index('ix_role_0', (role: Role) => [role.name, role.domain], { unique: true })\n@ObjectType({ description: 'A role that groups a set of privileges, which can be assigned to users.' })\nexport class Role {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier for the role.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { description: 'The domain to which the role belongs.' })\n domain?: Domain\n\n @RelationId((role: Role) => role.domain)\n domainId: string\n\n @Column()\n @Field({ nullable: true, description: 'The name of the role.' })\n name: string\n\n /*\n * Who holds a role is the user admin's: reading roles does not list their people. The directive sits on the\n * declaration - type-graphql drops a field resolver's directive when the type declares the field (ADR-0139), and\n * until 2026-10-18 the one on RoleQuery.users guarded nothing.\n */\n @ManyToMany(type => User, user => user.roles)\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Field(type => [User], { nullable: true, description: 'Users who are assigned this role.' })\n users: User[]\n\n @ManyToMany(type => Privilege, privilege => privilege.roles)\n @Field(type => [Privilege], { nullable: true, description: 'Privileges granted by this role.' })\n privileges: Privilege[]\n\n @Column({\n nullable: true\n })\n @Field({ nullable: true, description: 'A description of the role.' })\n description: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who created the role.' })\n creator: User\n\n @RelationId((role: Role) => role.creator)\n creatorId: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who last updated the role.' })\n updater: User\n\n @RelationId((role: Role) => role.updater)\n updaterId: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the role was created.' })\n createdAt: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the role was last updated.' })\n updatedAt: Date\n}\n"]}
1
+ {"version":3,"file":"role.js","sourceRoot":"","sources":["../../../server/service/role/role.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain } from '@things-factory/shell'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n JoinTable,\n ManyToMany,\n ManyToOne,\n RelationId,\n PrimaryGeneratedColumn,\n UpdateDateColumn\n} from 'typeorm'\nimport { Directive, ObjectType, Field, ID } from 'type-graphql'\nimport { Privilege } from '../privilege/privilege.js'\nimport { User } from '../user/user.js'\n\n@Entity()\n@Index('ix_role_0', (role: Role) => [role.name, role.domain], { unique: true })\n@ObjectType({ description: 'A role that groups a set of privileges, which can be assigned to users.' })\nexport class Role {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier for the role.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { description: 'The domain to which the role belongs.' })\n domain?: Domain\n\n @RelationId((role: Role) => role.domain)\n domainId: string\n\n @Column()\n @Field({ nullable: true, description: 'The name of the role.' })\n name: string\n\n /*\n * Who holds a role is the user admin's: reading roles does not list their people. The directive sits on the\n * declaration - type-graphql drops a field resolver's directive when the type declares the field (ADR-0139), and\n * until 2026-10-18 the one on RoleQuery.users guarded nothing.\n */\n @ManyToMany(type => User, user => user.roles)\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Field(type => [User], { nullable: true, description: 'Users who are assigned this role.' })\n users: User[]\n\n @ManyToMany(type => Privilege, privilege => privilege.roles)\n @Field(type => [Privilege], { nullable: true, description: 'Privileges granted by this role.' })\n privileges: Privilege[]\n\n @Column({\n nullable: true\n })\n @Field({ nullable: true, description: 'A description of the role.' })\n description: string\n\n /**\n * The role template this role is based on - its declaration id (`office.finance`), one at most (ADR-0075 decision 1,\n * amended 2026-10-26). The role list counts what the role lacks against it and the role screen lays the role against it.\n *\n * It is stored because the name cannot say it: a role named in the site's own words (경영지원) has no template of\n * that name, and the role list never told anyone it had fallen behind. It is not a foreign key - a template is a\n * declaration in code, not a row. An id no installed app declares any more stays here and the role screen says so;\n * nothing clears it on its own.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'The id of the role template this role is based on; null when it is based on none.' })\n templateId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who created the role.' })\n creator: User\n\n @RelationId((role: Role) => role.creator)\n creatorId: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true, description: 'The user who last updated the role.' })\n updater: User\n\n @RelationId((role: Role) => role.updater)\n updaterId: string\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the role was created.' })\n createdAt: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'The timestamp when the role was last updated.' })\n updatedAt: Date\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { Role } from '../role/role.js';
2
2
  import { RoleTaskCoverageListView, RoleTemplateGapView } from './role-template-types.js';
3
3
  /**
4
- * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against a template and against
4
+ * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against its base template and against
5
5
  * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;
6
6
  * this only reads the role's privileges.
7
7
  *
@@ -9,6 +9,6 @@ import { RoleTaskCoverageListView, RoleTemplateGapView } from './role-template-t
9
9
  * privilege and the same domain the role list asks for. They add no door of their own.
10
10
  */
11
11
  export declare class RoleTemplateGapResolver {
12
- templateGap(role: Role, templateId?: string): Promise<RoleTemplateGapView | null>;
12
+ templateGap(role: Role): Promise<RoleTemplateGapView | null>;
13
13
  taskCoverage(role: Role): Promise<RoleTaskCoverageListView>;
14
14
  }
@@ -10,7 +10,7 @@ const role_template_js_1 = require("./role-template.js");
10
10
  const role_task_js_1 = require("./role-task.js");
11
11
  const role_template_types_js_1 = require("./role-template-types.js");
12
12
  /**
13
- * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against a template and against
13
+ * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against its base template and against
14
14
  * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;
15
15
  * this only reads the role's privileges.
16
16
  *
@@ -18,16 +18,11 @@ const role_template_types_js_1 = require("./role-template-types.js");
18
18
  * privilege and the same domain the role list asks for. They add no door of their own.
19
19
  */
20
20
  let RoleTemplateGapResolver = class RoleTemplateGapResolver {
21
- async templateGap(role, templateId) {
22
- const held = await heldBy(role);
23
- if (!templateId) {
24
- return (0, role_template_js_1.templateGapOf)(role.name, held);
21
+ async templateGap(role) {
22
+ if (!role.templateId) {
23
+ return null;
25
24
  }
26
- const template = (0, role_template_js_1.allRoleTemplates)().find(one => one.id === templateId);
27
- if (!template) {
28
- throw new Error(`There is no role template "${templateId}" in this installation.`);
29
- }
30
- return (0, role_template_js_1.templateGapAgainst)(template, held);
25
+ return (0, role_template_js_1.templateGapOf)(role.templateId, await heldBy(role));
31
26
  }
32
27
  async taskCoverage(role) {
33
28
  return (0, role_task_js_1.taskCoverageOf)(await heldBy(role));
@@ -37,12 +32,11 @@ exports.RoleTemplateGapResolver = RoleTemplateGapResolver;
37
32
  tslib_1.__decorate([
38
33
  (0, type_graphql_1.FieldResolver)(type => role_template_types_js_1.RoleTemplateGapView, {
39
34
  nullable: true,
40
- description: "What a template grants here that the role does not hold, and what the role holds beyond it. Without templateId, the template of the role's name - null when no template carries it."
35
+ description: "What the role's base template grants here that the role does not hold, and what the role holds beyond it. Null when the role has no base template, or its base is not declared in this installation."
41
36
  }),
42
37
  tslib_1.__param(0, (0, type_graphql_1.Root)()),
43
- tslib_1.__param(1, (0, type_graphql_1.Arg)('templateId', { nullable: true, description: 'The template to lay the role against. Defaults to the template of the role\'s name.' })),
44
38
  tslib_1.__metadata("design:type", Function),
45
- tslib_1.__metadata("design:paramtypes", [role_js_1.Role, String]),
39
+ tslib_1.__metadata("design:paramtypes", [role_js_1.Role]),
46
40
  tslib_1.__metadata("design:returntype", Promise)
47
41
  ], RoleTemplateGapResolver.prototype, "templateGap", null);
48
42
  tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-gap-resolver.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-gap-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAAiE;AAEjE,iDAAqD;AAErD,4DAAqD;AACrD,6CAAsC;AACtC,yDAAwF;AACxF,iDAA+C;AAC/C,qEAAwF;AAExF;;;;;;;GAOG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAM5B,AAAN,KAAK,CAAC,WAAW,CACP,IAAU,EAElB,UAAmB;QAEnB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QAE/B,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,IAAA,gCAAa,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACvC,CAAC;QAED,MAAM,QAAQ,GAAG,IAAA,mCAAgB,GAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,UAAU,CAAC,CAAA;QACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,8BAA8B,UAAU,yBAAyB,CAAC,CAAA;QACpF,CAAC;QAED,OAAO,IAAA,qCAAkB,EAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3C,CAAC;IAKK,AAAN,KAAK,CAAC,YAAY,CAAS,IAAU;QACnC,OAAO,IAAA,6BAAc,EAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3C,CAAC;CACF,CAAA;AA/BY,0DAAuB;AAM5B;IALL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,4CAAmB,EAAE;QAC1C,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,qLAAqL;KACxL,CAAC;IAEC,mBAAA,IAAA,mBAAI,GAAE,CAAA;IACN,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qFAAqF,EAAE,CAAC,CAAA;;6CAD5H,cAAI;;0DAgBnB;AAKK;IAHL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,iDAAwB,EAAE;QAC/C,WAAW,EAAE,oFAAoF;KAClG,CAAC;IACkB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,cAAI;;2DAEpC;kCA9BU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,cAAI,CAAC;GACF,uBAAuB,CA+BnC;AAED,KAAK,UAAU,MAAM,CAAC,IAAU;IAC9B,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,wBAAS,CAAC;SACxC,kBAAkB,CAAC,WAAW,CAAC;SAC/B,MAAM,CAAC,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAAC;SAChD,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC;SACpC,KAAK,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;SACvC,OAAO,EAAE,CAAA;IAEZ,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { Arg, FieldResolver, Resolver, Root } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { allRoleTemplates, templateGapAgainst, templateGapOf } from './role-template.js'\nimport { taskCoverageOf } from './role-task.js'\nimport { RoleTaskCoverageListView, RoleTemplateGapView } from './role-template-types.js'\n\n/**\n * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against a template and against\n * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;\n * this only reads the role's privileges.\n *\n * Both are fields of a role, so they are reached only through a door that already returned the role - the same\n * privilege and the same domain the role list asks for. They add no door of their own.\n */\n@Resolver(Role)\nexport class RoleTemplateGapResolver {\n @FieldResolver(type => RoleTemplateGapView, {\n nullable: true,\n description:\n \"What a template grants here that the role does not hold, and what the role holds beyond it. Without templateId, the template of the role's name - null when no template carries it.\"\n })\n async templateGap(\n @Root() role: Role,\n @Arg('templateId', { nullable: true, description: 'The template to lay the role against. Defaults to the template of the role\\'s name.' })\n templateId?: string\n ): Promise<RoleTemplateGapView | null> {\n const held = await heldBy(role)\n\n if (!templateId) {\n return templateGapOf(role.name, held)\n }\n\n const template = allRoleTemplates().find(one => one.id === templateId)\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n return templateGapAgainst(template, held)\n }\n\n @FieldResolver(type => RoleTaskCoverageListView, {\n description: \"The role's privileges laid against every registered task, and those no task names.\"\n })\n async taskCoverage(@Root() role: Role): Promise<RoleTaskCoverageListView> {\n return taskCoverageOf(await heldBy(role))\n }\n}\n\nasync function heldBy(role: Role): Promise<Set<string>> {\n const held = await getRepository(Privilege)\n .createQueryBuilder('PRIVILEGE')\n .select(['PRIVILEGE.category', 'PRIVILEGE.name'])\n .innerJoin('PRIVILEGE.roles', 'ROLE')\n .where('ROLE.id = :id', { id: role.id })\n .getMany()\n\n return new Set(held.map(privilege => `${privilege.category} ${privilege.name}`))\n}\n"]}
1
+ {"version":3,"file":"role-template-gap-resolver.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-gap-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAA4D;AAE5D,iDAAqD;AAErD,4DAAqD;AACrD,6CAAsC;AACtC,yDAAkD;AAClD,iDAA+C;AAC/C,qEAAwF;AAExF;;;;;;;GAOG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAM5B,AAAN,KAAK,CAAC,WAAW,CAAS,IAAU;QAClC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,OAAO,IAAI,CAAA;QACb,CAAC;QAED,OAAO,IAAA,gCAAa,EAAC,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3D,CAAC;IAKK,AAAN,KAAK,CAAC,YAAY,CAAS,IAAU;QACnC,OAAO,IAAA,6BAAc,EAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3C,CAAC;CACF,CAAA;AApBY,0DAAuB;AAM5B;IALL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,4CAAmB,EAAE;QAC1C,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,sMAAsM;KACzM,CAAC;IACiB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,cAAI;;0DAMnC;AAKK;IAHL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,iDAAwB,EAAE;QAC/C,WAAW,EAAE,oFAAoF;KAClG,CAAC;IACkB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,cAAI;;2DAEpC;kCAnBU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,cAAI,CAAC;GACF,uBAAuB,CAoBnC;AAED,KAAK,UAAU,MAAM,CAAC,IAAU;IAC9B,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,wBAAS,CAAC;SACxC,kBAAkB,CAAC,WAAW,CAAC;SAC/B,MAAM,CAAC,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAAC;SAChD,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC;SACpC,KAAK,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;SACvC,OAAO,EAAE,CAAA;IAEZ,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { FieldResolver, Resolver, Root } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { templateGapOf } from './role-template.js'\nimport { taskCoverageOf } from './role-task.js'\nimport { RoleTaskCoverageListView, RoleTemplateGapView } from './role-template-types.js'\n\n/**\n * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against its base template and against\n * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;\n * this only reads the role's privileges.\n *\n * Both are fields of a role, so they are reached only through a door that already returned the role - the same\n * privilege and the same domain the role list asks for. They add no door of their own.\n */\n@Resolver(Role)\nexport class RoleTemplateGapResolver {\n @FieldResolver(type => RoleTemplateGapView, {\n nullable: true,\n description:\n \"What the role's base template grants here that the role does not hold, and what the role holds beyond it. Null when the role has no base template, or its base is not declared in this installation.\"\n })\n async templateGap(@Root() role: Role): Promise<RoleTemplateGapView | null> {\n if (!role.templateId) {\n return null\n }\n\n return templateGapOf(role.templateId, await heldBy(role))\n }\n\n @FieldResolver(type => RoleTaskCoverageListView, {\n description: \"The role's privileges laid against every registered task, and those no task names.\"\n })\n async taskCoverage(@Root() role: Role): Promise<RoleTaskCoverageListView> {\n return taskCoverageOf(await heldBy(role))\n }\n}\n\nasync function heldBy(role: Role): Promise<Set<string>> {\n const held = await getRepository(Privilege)\n .createQueryBuilder('PRIVILEGE')\n .select(['PRIVILEGE.category', 'PRIVILEGE.name'])\n .innerJoin('PRIVILEGE.roles', 'ROLE')\n .where('ROLE.id = :id', { id: role.id })\n .getMany()\n\n return new Set(held.map(privilege => `${privilege.category} ${privilege.name}`))\n}\n"]}
@@ -13,11 +13,14 @@ export declare class RoleTemplateMutation {
13
13
  * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);
14
14
  * this is the same rule, stated once.
15
15
  *
16
+ * ── One base template ─────────────────────────────────────────────────────────
17
+ * The role this makes or tops up is based on this template (`Role.templateId`, ADR-0075
18
+ * decision 1 amended 2026-10-26). A role based on another template is refused, not topped
19
+ * up: a role has one base, and the administrator changes it on the role screen first.
20
+ *
16
21
  * ── Why privileges are added and never removed ───────────────────────────────
17
- * Two applications may declare a template with the same role name on purpose, and the
18
- * privileges are then the union — to a user "quality inspector" is one seat. The union
19
- * only widens: if a template later drops a privilege it stays on roles already seeded,
20
- * because the seed runs once and the role belongs to the administrator afterwards.
22
+ * If a template later drops a privilege it stays on roles already seeded, because the seed
23
+ * runs once and the role belongs to the administrator afterwards.
21
24
  *
22
25
  * A privilege that is no longer declared anywhere is already handled elsewhere and not
23
26
  * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses
@@ -23,11 +23,14 @@ let RoleTemplateMutation = class RoleTemplateMutation {
23
23
  * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);
24
24
  * this is the same rule, stated once.
25
25
  *
26
+ * ── One base template ─────────────────────────────────────────────────────────
27
+ * The role this makes or tops up is based on this template (`Role.templateId`, ADR-0075
28
+ * decision 1 amended 2026-10-26). A role based on another template is refused, not topped
29
+ * up: a role has one base, and the administrator changes it on the role screen first.
30
+ *
26
31
  * ── Why privileges are added and never removed ───────────────────────────────
27
- * Two applications may declare a template with the same role name on purpose, and the
28
- * privileges are then the union — to a user "quality inspector" is one seat. The union
29
- * only widens: if a template later drops a privilege it stays on roles already seeded,
30
- * because the seed runs once and the role belongs to the administrator afterwards.
32
+ * If a template later drops a privilege it stays on roles already seeded, because the seed
33
+ * runs once and the role belongs to the administrator afterwards.
31
34
  *
32
35
  * A privilege that is no longer declared anywhere is already handled elsewhere and not
33
36
  * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses
@@ -59,6 +62,9 @@ let RoleTemplateMutation = class RoleTemplateMutation {
59
62
  where: { domain: { id: domain.id }, name: roleName },
60
63
  relations: ['privileges']
61
64
  });
65
+ if (role?.templateId && role.templateId !== template.id) {
66
+ throw new Error(`Role "${roleName}" is based on role template "${role.templateId}", not "${template.id}" - change its base template on the role screen first.`);
67
+ }
62
68
  if (!role) {
63
69
  /*
64
70
  * Only on the way to creating one. Topping up an existing role of ours is not a new name
@@ -70,6 +76,7 @@ let RoleTemplateMutation = class RoleTemplateMutation {
70
76
  domain,
71
77
  name: roleName,
72
78
  description: template.description,
79
+ templateId: template.id,
73
80
  creator: user,
74
81
  updater: user
75
82
  });
@@ -104,19 +111,12 @@ let RoleTemplateMutation = class RoleTemplateMutation {
104
111
  }
105
112
  }
106
113
  /*
107
- * Nothing records that this role came from a template.
108
- *
109
- * A `templateVersions` column stood here so a later change to the template could be
110
- * reported. It bought less than it looked like: what an administrator needs is "these
111
- * two privileges were added to the template", and that is the template's current
112
- * grants against the role's current privileges — computed, not stored. Telling them
113
- * apart from a privilege the administrator removed on purpose would take the seeded
114
- * content itself, not a version of it, and no screen asks for either yet.
115
- *
116
- * So the seed ends here. If reporting template drift is designed later, what to store
117
- * follows from that design rather than being guessed at now.
114
+ * The base template is recorded and nothing more. What an administrator needs later is "these two
115
+ * privileges were added to the template", and that is the template's current grants against the role's
116
+ * current privileges — computed from the base, not stored (`Role.templateGap`).
118
117
  */
119
118
  role.privileges = [...(role.privileges || []), ...added];
119
+ role.templateId = template.id;
120
120
  role.updater = user;
121
121
  return await roleRepository.save(role);
122
122
  }
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-mutation.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { refuseRoleNameIfStanding } from '../../utils/refuse-role-name.js'\nimport { Role } from '../role/role.js'\nimport { roleTemplateById, computedRoleTemplates, RegisteredRoleTemplate, templateDefinitionErrors, templateGrantsOf } from './role-template.js'\n\n/* `refuseRoleNameIfStanding` throws a `Refusal`; this is the exit that turns it into a sentence. */\n@Resolver(Role)\n@UseMiddleware(SpeakRefusals)\nexport class RoleTemplateMutation {\n /**\n * Creates a role from a template — once per name.\n *\n * ── Why an existing name is used rather than replaced ────────────────────────\n * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`\n * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the\n * first one while the administrator edits the second, and nothing would report it: the\n * screen shows two roles and the approvals go to the one nobody is looking at.\n *\n * So an existing role of that name is used as it stands. operato-plant's seed already\n * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);\n * this is the same rule, stated once.\n *\n * ── Why privileges are added and never removed ───────────────────────────────\n * Two applications may declare a template with the same role name on purpose, and the\n * privileges are then the union — to a user \"quality inspector\" is one seat. The union\n * only widens: if a template later drops a privilege it stays on roles already seeded,\n * because the seed runs once and the role belongs to the administrator afterwards.\n *\n * A privilege that is no longer declared anywhere is already handled elsewhere and not\n * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses\n * to switch it back on, and removing it is the administrator's call. Reclaiming it from\n * this side would mean overwriting, and would be unable to tell a stale grant from one\n * the administrator added on purpose.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Role, { description: 'To create a role from a role template' })\n async createRoleFromTemplate(\n @Arg('templateId') templateId: string,\n /**\n * What this plant calls the seat. Defaults to the template's name.\n *\n * The template knows which seat exists in a factory running this product; it does not\n * know what the people there call it. And because a role is also what an approval line\n * points at, the same seat is sometimes needed twice — a day shift and a night shift\n * inspector hold the same privileges and take different approvals. Without a name of\n * their own a template could be used exactly once per domain.\n */\n @Arg('name', { nullable: true }) name: string | undefined,\n @Ctx() context: ResolverContext,\n /**\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3).\n *\n * ⚠ This is where the rule bites hardest, and stopping here is the point of it. A template\n * seeds the seats a product needs, and on a tenant with a parent those names are often\n * already visible through inheritance — so the template would quietly put a second row of\n * the same name beside the inherited one, which is the state the whole rule exists to\n * prevent.\n *\n * The name is not changed to get past it. Standing up the same names in this domain is the\n * first step of moving off an inherited structure (decision 2), so this says \"yes, that is\n * what I am doing\" rather than \"call it something else\". `name` above remains for the other\n * thing — a second seat with the same privileges under a name of its own.\n */\n @Arg('separately', { nullable: true }) separately: boolean | undefined\n ): Promise<Role> {\n const { domain, user, tx } = context.state\n\n const template = findTemplate(templateId)\n\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n /*\n * A template whose tasks are not all there is refused whole, before a role is made or a privilege added\n * (ADR-0139 decision 11). Applying the rest would stand up a seat short of work its name says it does, and\n * the gap would look like the administrator's choice.\n */\n const errors = templateDefinitionErrors(template)\n if (errors.length) {\n throw new Error(`Role template \"${template.id}\" cannot be applied: ${errors.join('; ')}.`)\n }\n\n const roleName = (name || template.name).trim()\n\n if (!roleName) {\n throw new Error('A role needs a name.')\n }\n\n const roleRepository = tx.getRepository(Role)\n\n let role = await roleRepository.findOne({\n where: { domain: { id: domain.id }, name: roleName },\n relations: ['privileges']\n })\n\n if (!role) {\n /*\n * Only on the way to creating one. Topping up an existing role of ours is not a new name\n * and must not be refused — running the same template twice is how a seat gets privileges\n * the template gained since.\n */\n await refuseRoleNameIfStanding({ name: roleName, domain, separately, tx })\n\n role = await roleRepository.save({\n domain,\n name: roleName,\n description: template.description,\n creator: user,\n updater: user\n } as any)\n\n role = await roleRepository.findOne({\n where: { id: role!.id },\n relations: ['privileges']\n })\n }\n\n const held = new Set((role!.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`))\n const wanted = templateGrantsOf(template).flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })))\n\n /*\n * Privileges have no domain — the whole server shares one set and what differs per\n * domain is only which role holds them (`Privilege` has no domain column; auth-base's\n * boot creates them by category and name alone). Filtering by domain finds nothing.\n */\n const privilegeRepository = tx.getRepository(Privilege)\n const added: Privilege[] = []\n\n for (const one of wanted) {\n if (held.has(`${one.category} ${one.name}`)) {\n continue\n }\n\n const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } })\n\n /*\n * A privilege this installation does not declare is skipped, never created. Creating\n * it here would make a row no `@privilege` asks for: the role would look as if it\n * had been granted something, and open nothing. Granted-but-closed is the worst of\n * the three outcomes. `roleTemplates` reports these as `missingCount` before the\n * administrator picks the template.\n */\n if (privilege) {\n added.push(privilege)\n }\n }\n\n /*\n * Nothing records that this role came from a template.\n *\n * A `templateVersions` column stood here so a later change to the template could be\n * reported. It bought less than it looked like: what an administrator needs is \"these\n * two privileges were added to the template\", and that is the template's current\n * grants against the role's current privileges — computed, not stored. Telling them\n * apart from a privilege the administrator removed on purpose would take the seeded\n * content itself, not a version of it, and no screen asks for either yet.\n *\n * So the seed ends here. If reporting template drift is designed later, what to store\n * follows from that design rather than being guessed at now.\n */\n role!.privileges = [...(role!.privileges || []), ...added]\n role!.updater = user\n\n return await roleRepository.save(role!)\n }\n}\n\nfunction findTemplate(id: string): RegisteredRoleTemplate | undefined {\n return roleTemplateById(id) || computedRoleTemplates().find(template => template.id === id)\n}\n"]}
1
+ {"version":3,"file":"role-template-mutation.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { refuseRoleNameIfStanding } from '../../utils/refuse-role-name.js'\nimport { Role } from '../role/role.js'\nimport { roleTemplateById, computedRoleTemplates, RegisteredRoleTemplate, templateDefinitionErrors, templateGrantsOf } from './role-template.js'\n\n/* `refuseRoleNameIfStanding` throws a `Refusal`; this is the exit that turns it into a sentence. */\n@Resolver(Role)\n@UseMiddleware(SpeakRefusals)\nexport class RoleTemplateMutation {\n /**\n * Creates a role from a template — once per name.\n *\n * ── Why an existing name is used rather than replaced ────────────────────────\n * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`\n * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the\n * first one while the administrator edits the second, and nothing would report it: the\n * screen shows two roles and the approvals go to the one nobody is looking at.\n *\n * So an existing role of that name is used as it stands. operato-plant's seed already\n * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);\n * this is the same rule, stated once.\n *\n * ── One base template ─────────────────────────────────────────────────────────\n * The role this makes or tops up is based on this template (`Role.templateId`, ADR-0075\n * decision 1 amended 2026-10-26). A role based on another template is refused, not topped\n * up: a role has one base, and the administrator changes it on the role screen first.\n *\n * ── Why privileges are added and never removed ───────────────────────────────\n * If a template later drops a privilege it stays on roles already seeded, because the seed\n * runs once and the role belongs to the administrator afterwards.\n *\n * A privilege that is no longer declared anywhere is already handled elsewhere and not\n * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses\n * to switch it back on, and removing it is the administrator's call. Reclaiming it from\n * this side would mean overwriting, and would be unable to tell a stale grant from one\n * the administrator added on purpose.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Role, { description: 'To create a role from a role template' })\n async createRoleFromTemplate(\n @Arg('templateId') templateId: string,\n /**\n * What this plant calls the seat. Defaults to the template's name.\n *\n * The template knows which seat exists in a factory running this product; it does not\n * know what the people there call it. And because a role is also what an approval line\n * points at, the same seat is sometimes needed twice — a day shift and a night shift\n * inspector hold the same privileges and take different approvals. Without a name of\n * their own a template could be used exactly once per domain.\n */\n @Arg('name', { nullable: true }) name: string | undefined,\n @Ctx() context: ResolverContext,\n /**\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3).\n *\n * ⚠ This is where the rule bites hardest, and stopping here is the point of it. A template\n * seeds the seats a product needs, and on a tenant with a parent those names are often\n * already visible through inheritance — so the template would quietly put a second row of\n * the same name beside the inherited one, which is the state the whole rule exists to\n * prevent.\n *\n * The name is not changed to get past it. Standing up the same names in this domain is the\n * first step of moving off an inherited structure (decision 2), so this says \"yes, that is\n * what I am doing\" rather than \"call it something else\". `name` above remains for the other\n * thing — a second seat with the same privileges under a name of its own.\n */\n @Arg('separately', { nullable: true }) separately: boolean | undefined\n ): Promise<Role> {\n const { domain, user, tx } = context.state\n\n const template = findTemplate(templateId)\n\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n /*\n * A template whose tasks are not all there is refused whole, before a role is made or a privilege added\n * (ADR-0139 decision 11). Applying the rest would stand up a seat short of work its name says it does, and\n * the gap would look like the administrator's choice.\n */\n const errors = templateDefinitionErrors(template)\n if (errors.length) {\n throw new Error(`Role template \"${template.id}\" cannot be applied: ${errors.join('; ')}.`)\n }\n\n const roleName = (name || template.name).trim()\n\n if (!roleName) {\n throw new Error('A role needs a name.')\n }\n\n const roleRepository = tx.getRepository(Role)\n\n let role = await roleRepository.findOne({\n where: { domain: { id: domain.id }, name: roleName },\n relations: ['privileges']\n })\n\n if (role?.templateId && role.templateId !== template.id) {\n throw new Error(\n `Role \"${roleName}\" is based on role template \"${role.templateId}\", not \"${template.id}\" - change its base template on the role screen first.`\n )\n }\n\n if (!role) {\n /*\n * Only on the way to creating one. Topping up an existing role of ours is not a new name\n * and must not be refused — running the same template twice is how a seat gets privileges\n * the template gained since.\n */\n await refuseRoleNameIfStanding({ name: roleName, domain, separately, tx })\n\n role = await roleRepository.save({\n domain,\n name: roleName,\n description: template.description,\n templateId: template.id,\n creator: user,\n updater: user\n } as any)\n\n role = await roleRepository.findOne({\n where: { id: role!.id },\n relations: ['privileges']\n })\n }\n\n const held = new Set((role!.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`))\n const wanted = templateGrantsOf(template).flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })))\n\n /*\n * Privileges have no domain — the whole server shares one set and what differs per\n * domain is only which role holds them (`Privilege` has no domain column; auth-base's\n * boot creates them by category and name alone). Filtering by domain finds nothing.\n */\n const privilegeRepository = tx.getRepository(Privilege)\n const added: Privilege[] = []\n\n for (const one of wanted) {\n if (held.has(`${one.category} ${one.name}`)) {\n continue\n }\n\n const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } })\n\n /*\n * A privilege this installation does not declare is skipped, never created. Creating\n * it here would make a row no `@privilege` asks for: the role would look as if it\n * had been granted something, and open nothing. Granted-but-closed is the worst of\n * the three outcomes. `roleTemplates` reports these as `missingCount` before the\n * administrator picks the template.\n */\n if (privilege) {\n added.push(privilege)\n }\n }\n\n /*\n * The base template is recorded and nothing more. What an administrator needs later is \"these two\n * privileges were added to the template\", and that is the template's current grants against the role's\n * current privileges — computed from the base, not stored (`Role.templateGap`).\n */\n role!.privileges = [...(role!.privileges || []), ...added]\n role!.templateId = template.id\n role!.updater = user\n\n return await roleRepository.save(role!)\n }\n}\n\nfunction findTemplate(id: string): RegisteredRoleTemplate | undefined {\n return roleTemplateById(id) || computedRoleTemplates().find(template => template.id === id)\n}\n"]}
@@ -40,9 +40,9 @@ export declare class RoleTemplateView {
40
40
  * approval routed to the role nobody edits. The existing role is used as it stands and
41
41
  * the template's privileges are added to it.
42
42
  *
43
- * It says "default name" rather than "seeded" because the administrator may give the
44
- * role a name of their own, and then nothing here can tell whether this template was
45
- * used. That is deliberate: provenance is not stored.
43
+ * It says "default name" rather than "based on" because it answers the question the
44
+ * create dialog asks — will this name make a new role or top up a standing one. Which
45
+ * template a role is based on is the role's own `templateId`.
46
46
  */
47
47
  defaultNameTaken: boolean;
48
48
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-types.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\n/**\n * What a template looks like *to this installation*.\n *\n * The declared template and the offered template are not the same thing: an app declares\n * grants across modules it expects to be installed, and a given installation has a subset.\n * operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47\n * categories declared across the repository. So the answer has to say which parts of the\n * template this installation can actually honour.\n */\n@ObjectType({ description: 'One module and the axes a role template grants on it.' })\nexport class RoleTemplateGrantView {\n @Field({ description: 'The privilege category — what is guarded.' })\n category: string\n\n @Field(type => [String], { description: 'The privilege axes granted on that category.' })\n axes: string[]\n\n /**\n * Axes this installation does not declare.\n *\n * Dropping them quietly would hand the administrator a role that does not match the\n * template's name, with no way to tell. They are reported instead, and the screen says\n * \"not present in this installation\".\n */\n @Field(type => [String], { description: 'Axes named by the template that this installation does not declare.' })\n missingAxes: string[]\n}\n\n@ObjectType({ description: 'A role template offered by this installation.' })\nexport class RoleTemplateView {\n @Field({ description: 'Declaration key, namespaced by the declaring application.' })\n id: string\n\n @Field({ description: 'The role name this template creates in a domain.' })\n name: string\n\n @Field({ nullable: true, description: 'A description of the seat this template creates.' })\n description?: string\n\n @Field(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' })\n grants: RoleTemplateGrantView[]\n\n @Field(type => [String], { description: 'The tasks this template is built from, by id - each a piece of work a module or application names.' })\n tasks: string[]\n\n /**\n * What stops this template from being applied at all — a task nobody registered, or a task naming a\n * privilege this installation does not declare (ADR-0139 decision 11). Empty when it can be applied.\n */\n @Field(type => [String], { description: 'Definition errors that refuse this template when applied: unregistered tasks, or tasks naming undeclared privileges.' })\n definitionErrors: string[]\n\n @Field(type => Int, { description: 'How many privileges this template would actually grant here.' })\n privilegeCount: number\n\n @Field(type => Int, { description: 'How many privileges the template names that this installation does not have.' })\n missingCount: number\n\n /**\n * Whether a role already carries this template's default name in the current domain.\n *\n * The screen warns before creating one, because seeding onto an existing name does not\n * make a second role — approval lines hold a role's id, so a duplicate would leave the\n * approval routed to the role nobody edits. The existing role is used as it stands and\n * the template's privileges are added to it.\n *\n * It says \"default name\" rather than \"seeded\" because the administrator may give the\n * role a name of their own, and then nothing here can tell whether this template was\n * used. That is deliberate: provenance is not stored.\n */\n @Field({ description: \"Whether a role already carries this template's default name in the current domain.\" })\n defaultNameTaken: boolean\n\n /**\n * True when the template deliberately opens nothing.\n *\n * A seat that only receives approvals is legitimate — operato-twin declares seven roles\n * with no privileges at all, used as the target an approval line points at. The screen\n * must not draw that as \"0 privileges, something is wrong\", so the distinction is\n * carried here rather than inferred from an empty list.\n */\n @Field({ description: 'Whether this template grants nothing on purpose, rather than by omission.' })\n grantsNothingDeliberately: boolean\n}\n\n@ObjectType({ description: 'Privileges on one category that a role lacks against its template.' })\nexport class RoleTemplateGapGrant {\n @Field({ description: 'The privilege category.' })\n category: string\n\n @Field(type => [String], { description: 'The axes the template grants here and the role does not hold.' })\n axes: string[]\n}\n\n@ObjectType({\n description: \"What the template of a role's name grants in this installation that the role does not hold.\"\n})\nexport class RoleTemplateGapView {\n @Field({ description: 'The template the role is paired with, by name.' })\n templateId: string\n\n @Field({ description: 'The template name - the same as the role name.' })\n templateName: string\n\n @Field(type => [RoleTemplateGapGrant], { description: 'The privileges missing from the role, by category.' })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => Int, {\n description: 'How many privileges the role lacks against its template. Zero means up to date.'\n })\n missingCount: number\n\n /**\n * What the role holds beyond the template. Not a fault — a site may widen a role on purpose — so the screen\n * calls it \"wider than the template\" and nothing takes it away.\n */\n @Field(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that the template does not grant, by category.' })\n extra: RoleTemplateGapGrant[]\n\n @Field(type => Int, { description: 'How many privileges the role holds beyond the template.' })\n extraCount: number\n}\n\n@ObjectType({ description: \"One task laid against a role's privileges.\" })\nexport class RoleTaskCoverageView {\n @Field({ description: 'The task id, `<module>.<work>`. Its words are the translation keys task.<id>.name and task.<id>.description.' })\n taskId: string\n\n @Field({ description: 'The module or application that declares the task - the first part of its id.' })\n module: string\n\n @Field({\n description:\n \"all, some, none or broken. Says which of the task's privileges the role holds - not whether the work can be done. broken: the task names a privilege this installation does not declare.\"\n })\n status: string\n\n @Field(type => [RoleTemplateGapGrant], { description: \"The task's privileges the role holds.\" })\n held: RoleTemplateGapGrant[]\n\n @Field(type => [RoleTemplateGapGrant], { description: \"The task's declared privileges the role does not hold.\" })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => [RoleTemplateGapGrant], { description: 'Privileges the task names that this installation does not declare.' })\n undeclared: RoleTemplateGapGrant[]\n}\n\n@ObjectType({ description: \"A role's privileges laid against every registered task.\" })\nexport class RoleTaskCoverageListView {\n @Field(type => [RoleTaskCoverageView], { description: 'Every registered task, with what the role holds of it.' })\n tasks: RoleTaskCoverageView[]\n\n @Field(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that no task names.' })\n unclassified: RoleTemplateGapGrant[]\n}\n"]}
1
+ {"version":3,"file":"role-template-types.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\n/**\n * What a template looks like *to this installation*.\n *\n * The declared template and the offered template are not the same thing: an app declares\n * grants across modules it expects to be installed, and a given installation has a subset.\n * operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47\n * categories declared across the repository. So the answer has to say which parts of the\n * template this installation can actually honour.\n */\n@ObjectType({ description: 'One module and the axes a role template grants on it.' })\nexport class RoleTemplateGrantView {\n @Field({ description: 'The privilege category — what is guarded.' })\n category: string\n\n @Field(type => [String], { description: 'The privilege axes granted on that category.' })\n axes: string[]\n\n /**\n * Axes this installation does not declare.\n *\n * Dropping them quietly would hand the administrator a role that does not match the\n * template's name, with no way to tell. They are reported instead, and the screen says\n * \"not present in this installation\".\n */\n @Field(type => [String], { description: 'Axes named by the template that this installation does not declare.' })\n missingAxes: string[]\n}\n\n@ObjectType({ description: 'A role template offered by this installation.' })\nexport class RoleTemplateView {\n @Field({ description: 'Declaration key, namespaced by the declaring application.' })\n id: string\n\n @Field({ description: 'The role name this template creates in a domain.' })\n name: string\n\n @Field({ nullable: true, description: 'A description of the seat this template creates.' })\n description?: string\n\n @Field(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' })\n grants: RoleTemplateGrantView[]\n\n @Field(type => [String], { description: 'The tasks this template is built from, by id - each a piece of work a module or application names.' })\n tasks: string[]\n\n /**\n * What stops this template from being applied at all — a task nobody registered, or a task naming a\n * privilege this installation does not declare (ADR-0139 decision 11). Empty when it can be applied.\n */\n @Field(type => [String], { description: 'Definition errors that refuse this template when applied: unregistered tasks, or tasks naming undeclared privileges.' })\n definitionErrors: string[]\n\n @Field(type => Int, { description: 'How many privileges this template would actually grant here.' })\n privilegeCount: number\n\n @Field(type => Int, { description: 'How many privileges the template names that this installation does not have.' })\n missingCount: number\n\n /**\n * Whether a role already carries this template's default name in the current domain.\n *\n * The screen warns before creating one, because seeding onto an existing name does not\n * make a second role — approval lines hold a role's id, so a duplicate would leave the\n * approval routed to the role nobody edits. The existing role is used as it stands and\n * the template's privileges are added to it.\n *\n * It says \"default name\" rather than \"based on\" because it answers the question the\n * create dialog asks — will this name make a new role or top up a standing one. Which\n * template a role is based on is the role's own `templateId`.\n */\n @Field({ description: \"Whether a role already carries this template's default name in the current domain.\" })\n defaultNameTaken: boolean\n\n /**\n * True when the template deliberately opens nothing.\n *\n * A seat that only receives approvals is legitimate — operato-twin declares seven roles\n * with no privileges at all, used as the target an approval line points at. The screen\n * must not draw that as \"0 privileges, something is wrong\", so the distinction is\n * carried here rather than inferred from an empty list.\n */\n @Field({ description: 'Whether this template grants nothing on purpose, rather than by omission.' })\n grantsNothingDeliberately: boolean\n}\n\n@ObjectType({ description: 'Privileges on one category that a role lacks against its template.' })\nexport class RoleTemplateGapGrant {\n @Field({ description: 'The privilege category.' })\n category: string\n\n @Field(type => [String], { description: 'The axes the template grants here and the role does not hold.' })\n axes: string[]\n}\n\n@ObjectType({\n description: \"What the template of a role's name grants in this installation that the role does not hold.\"\n})\nexport class RoleTemplateGapView {\n @Field({ description: 'The template the role is paired with, by name.' })\n templateId: string\n\n @Field({ description: 'The template name - the same as the role name.' })\n templateName: string\n\n @Field(type => [RoleTemplateGapGrant], { description: 'The privileges missing from the role, by category.' })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => Int, {\n description: 'How many privileges the role lacks against its template. Zero means up to date.'\n })\n missingCount: number\n\n /**\n * What the role holds beyond the template. Not a fault — a site may widen a role on purpose — so the screen\n * calls it \"wider than the template\" and nothing takes it away.\n */\n @Field(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that the template does not grant, by category.' })\n extra: RoleTemplateGapGrant[]\n\n @Field(type => Int, { description: 'How many privileges the role holds beyond the template.' })\n extraCount: number\n}\n\n@ObjectType({ description: \"One task laid against a role's privileges.\" })\nexport class RoleTaskCoverageView {\n @Field({ description: 'The task id, `<module>.<work>`. Its words are the translation keys task.<id>.name and task.<id>.description.' })\n taskId: string\n\n @Field({ description: 'The module or application that declares the task - the first part of its id.' })\n module: string\n\n @Field({\n description:\n \"all, some, none or broken. Says which of the task's privileges the role holds - not whether the work can be done. broken: the task names a privilege this installation does not declare.\"\n })\n status: string\n\n @Field(type => [RoleTemplateGapGrant], { description: \"The task's privileges the role holds.\" })\n held: RoleTemplateGapGrant[]\n\n @Field(type => [RoleTemplateGapGrant], { description: \"The task's declared privileges the role does not hold.\" })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => [RoleTemplateGapGrant], { description: 'Privileges the task names that this installation does not declare.' })\n undeclared: RoleTemplateGapGrant[]\n}\n\n@ObjectType({ description: \"A role's privileges laid against every registered task.\" })\nexport class RoleTaskCoverageListView {\n @Field(type => [RoleTaskCoverageView], { description: 'Every registered task, with what the role holds of it.' })\n tasks: RoleTaskCoverageView[]\n\n @Field(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that no task names.' })\n unclassified: RoleTemplateGapGrant[]\n}\n"]}
@@ -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. Nothing is written
14
- * onto the role to remember where it came from — what an administrator would want from
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
- * Two apps may declare the same name on purpose — that is one seat, and the privileges
46
- * become the union. The name is also a stored identifier: approval lines hold the role's
47
- * id, so renaming here creates a second role and leaves the approval line pointing at
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 template of this role's name — the pair the role list shows unasked.
163
+ * The gap against the role's base template (`Role.templateId`) — what the role list shows unasked.
165
164
  *
166
- * Paired by name, because provenance is not stored. A role whose name no template carries — renamed by its
167
- * administrator, or named in the site's own words as operato-office's roles are — has no pair here; the role
168
- * screen lets the administrator pick the template to lay it against (`templateGapAgainst`). The role is never
169
- * renamed to make a pair: approval lines hold the role, and the name is the site's.
170
- * `null` when no template carries this name; a gap of 0 missing is a role that is up to date.
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(roleName: string, held: ReadonlySet<string>, templates?: RegisteredRoleTemplate[], declared?: Record<string, unknown>): RoleTemplateGap | null;
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 template of this role's name — the pair the role list shows unasked.
207
+ * The gap against the role's base template (`Role.templateId`) — what the role list shows unasked.
208
208
  *
209
- * Paired by name, because provenance is not stored. A role whose name no template carries — renamed by its
210
- * administrator, or named in the site's own words as operato-office's roles are — has no pair here; the role
211
- * screen lets the administrator pick the template to lay it against (`templateGapAgainst`). The role is never
212
- * renamed to make a pair: approval lines hold the role, and the name is the site's.
213
- * `null` when no template carries this name; a gap of 0 missing is a role that is up to date.
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(roleName, held, templates = allRoleTemplates(), declared = process['PRIVILEGES'] || {}) {
216
- const template = templates.find(one => one.name === roleName);
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
  /**