@happyvertical/smrt-human-resources 0.55.6 → 0.55.8
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/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +22 -1
- package/dist/smrt-knowledge.json +25 -4
- package/package.json +8 -8
package/dist/manifest.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
5
|
-
"packageVersion": "0.55.
|
|
5
|
+
"packageVersion": "0.55.8",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-human-resources:Employment": {
|
|
8
8
|
"name": "employment",
|
|
9
9
|
"className": "Employment",
|
|
10
10
|
"qualifiedName": "@happyvertical/smrt-human-resources:Employment",
|
|
11
11
|
"collection": "employments",
|
|
12
|
+
"description": "One person's employment with a tenant: the stable record applications and payroll key on. There is one per tenant and profile; a rehire adds an EmploymentTerm to the same row.",
|
|
12
13
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/employment/models.ts",
|
|
13
14
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
14
15
|
"fields": {
|
|
@@ -281,6 +282,7 @@
|
|
|
281
282
|
"className": "EmploymentTerm",
|
|
282
283
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentTerm",
|
|
283
284
|
"collection": "employmentterms",
|
|
285
|
+
"description": "One continuous period of employment. endedOn is the last day employed (inclusive); null means the term is open. \"Employed on a date\" is answered from terms, never from Employment.status.",
|
|
284
286
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/employment/models.ts",
|
|
285
287
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
286
288
|
"fields": {
|
|
@@ -483,6 +485,7 @@
|
|
|
483
485
|
"className": "EmploymentChange",
|
|
484
486
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentChange",
|
|
485
487
|
"collection": "employmentchanges",
|
|
488
|
+
"description": "Append-only dated change to an employment, attributed to an actor.",
|
|
486
489
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/employment/models.ts",
|
|
487
490
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
488
491
|
"fields": {
|
|
@@ -738,6 +741,7 @@
|
|
|
738
741
|
"className": "EmploymentCollection",
|
|
739
742
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentCollection",
|
|
740
743
|
"collection": "employments",
|
|
744
|
+
"description": "Scoped persistence collection; mutations must use EmploymentService.",
|
|
741
745
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/employment/models.ts",
|
|
742
746
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
743
747
|
"fields": {},
|
|
@@ -897,6 +901,7 @@
|
|
|
897
901
|
"className": "EmploymentTermCollection",
|
|
898
902
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentTermCollection",
|
|
899
903
|
"collection": "employmentterms",
|
|
904
|
+
"description": "Scoped term collection; closed like its model.",
|
|
900
905
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/employment/models.ts",
|
|
901
906
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
902
907
|
"fields": {},
|
|
@@ -1019,6 +1024,7 @@
|
|
|
1019
1024
|
"className": "EmploymentChangeCollection",
|
|
1020
1025
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentChangeCollection",
|
|
1021
1026
|
"collection": "employmentchanges",
|
|
1027
|
+
"description": "Scoped append-only change collection; closed like its model.",
|
|
1022
1028
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/employment/models.ts",
|
|
1023
1029
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
1024
1030
|
"fields": {},
|
|
@@ -1163,6 +1169,7 @@
|
|
|
1163
1169
|
"className": "Qualification",
|
|
1164
1170
|
"qualifiedName": "@happyvertical/smrt-human-resources:Qualification",
|
|
1165
1171
|
"collection": "qualifications",
|
|
1172
|
+
"description": "A qualification definition: a managed list per tenant. Other packages gate work by referencing its id (@happyvertical/smrt-human-resources:Qualification).",
|
|
1166
1173
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/qualifications/models.ts",
|
|
1167
1174
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
1168
1175
|
"fields": {
|
|
@@ -1400,6 +1407,7 @@
|
|
|
1400
1407
|
"className": "HeldQualification",
|
|
1401
1408
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualification",
|
|
1402
1409
|
"collection": "heldqualifications",
|
|
1410
|
+
"description": "A person holds a qualification. employmentId is set when the definition's scope is employment; such a row is revoked when that employment ends.",
|
|
1403
1411
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/qualifications/models.ts",
|
|
1404
1412
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
1405
1413
|
"fields": {
|
|
@@ -1758,6 +1766,7 @@
|
|
|
1758
1766
|
"className": "HeldQualificationChange",
|
|
1759
1767
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualificationChange",
|
|
1760
1768
|
"collection": "heldqualificationchanges",
|
|
1769
|
+
"description": "Append-only dated change to a held qualification, attributed to an actor.",
|
|
1761
1770
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/qualifications/models.ts",
|
|
1762
1771
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
1763
1772
|
"fields": {
|
|
@@ -1986,6 +1995,7 @@
|
|
|
1986
1995
|
"className": "QualificationCollection",
|
|
1987
1996
|
"qualifiedName": "@happyvertical/smrt-human-resources:QualificationCollection",
|
|
1988
1997
|
"collection": "qualifications",
|
|
1998
|
+
"description": "Scoped definition collection; mutations must use QualificationService.",
|
|
1989
1999
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/qualifications/models.ts",
|
|
1990
2000
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
1991
2001
|
"fields": {},
|
|
@@ -2116,6 +2126,7 @@
|
|
|
2116
2126
|
"className": "HeldQualificationCollection",
|
|
2117
2127
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualificationCollection",
|
|
2118
2128
|
"collection": "heldqualifications",
|
|
2129
|
+
"description": "Scoped held-qualification collection; closed like its model.",
|
|
2119
2130
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/qualifications/models.ts",
|
|
2120
2131
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
2121
2132
|
"fields": {},
|
|
@@ -2328,6 +2339,7 @@
|
|
|
2328
2339
|
"className": "HeldQualificationChangeCollection",
|
|
2329
2340
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualificationChangeCollection",
|
|
2330
2341
|
"collection": "heldqualificationchanges",
|
|
2342
|
+
"description": "Scoped append-only change collection; closed like its model.",
|
|
2331
2343
|
"filePath": "/home/runner/work/smrt/smrt/packages/human-resources/src/qualifications/models.ts",
|
|
2332
2344
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
2333
2345
|
"fields": {},
|
|
@@ -2463,5 +2475,14 @@
|
|
|
2463
2475
|
"@happyvertical/smrt-core",
|
|
2464
2476
|
"@happyvertical/smrt-tenancy"
|
|
2465
2477
|
],
|
|
2478
|
+
"browser": {
|
|
2479
|
+
"status": "server-only",
|
|
2480
|
+
"issues": [
|
|
2481
|
+
"#3624"
|
|
2482
|
+
],
|
|
2483
|
+
"via": [
|
|
2484
|
+
"@happyvertical/smrt-tenancy"
|
|
2485
|
+
]
|
|
2486
|
+
},
|
|
2466
2487
|
"stampsConstructors": true
|
|
2467
2488
|
}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.8",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "fc8ba6fa556830c4590ef903e629221e8631996666caf09d454640d21efe15e2",
|
|
11
|
+
"packageJson": "de986ac1788427bed2fc7479779de37f342ba392cca73354a56f5c7f3e6fd5ca",
|
|
12
12
|
"agents": "441cce6da32d348df8bb6300d526f118d70efa5680c80615d373cc8ef98e95f4"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"name": "Employment",
|
|
65
65
|
"qualifiedName": "@happyvertical/smrt-human-resources:Employment",
|
|
66
66
|
"collection": "employments",
|
|
67
|
+
"description": "One person's employment with a tenant: the stable record applications and payroll key on. There is one per tenant and profile; a rehire adds an EmploymentTerm to the same row.",
|
|
67
68
|
"tableName": "employments",
|
|
68
69
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
69
70
|
"extends": "SmrtObject",
|
|
@@ -189,6 +190,7 @@
|
|
|
189
190
|
"name": "EmploymentTerm",
|
|
190
191
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentTerm",
|
|
191
192
|
"collection": "employmentterms",
|
|
193
|
+
"description": "One continuous period of employment. endedOn is the last day employed (inclusive); null means the term is open. \"Employed on a date\" is answered from terms, never from Employment.status.",
|
|
192
194
|
"tableName": "employment_terms",
|
|
193
195
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
194
196
|
"extends": "SmrtObject",
|
|
@@ -281,6 +283,7 @@
|
|
|
281
283
|
"name": "EmploymentChange",
|
|
282
284
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentChange",
|
|
283
285
|
"collection": "employmentchanges",
|
|
286
|
+
"description": "Append-only dated change to an employment, attributed to an actor.",
|
|
284
287
|
"tableName": "employment_changes",
|
|
285
288
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
286
289
|
"extends": "SmrtObject",
|
|
@@ -400,6 +403,7 @@
|
|
|
400
403
|
"name": "EmploymentCollection",
|
|
401
404
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentCollection",
|
|
402
405
|
"collection": "employments",
|
|
406
|
+
"description": "Scoped persistence collection; mutations must use EmploymentService.",
|
|
403
407
|
"tableName": "employments",
|
|
404
408
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
405
409
|
"extends": "SmrtCollection",
|
|
@@ -417,6 +421,7 @@
|
|
|
417
421
|
"name": "EmploymentTermCollection",
|
|
418
422
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentTermCollection",
|
|
419
423
|
"collection": "employmentterms",
|
|
424
|
+
"description": "Scoped term collection; closed like its model.",
|
|
420
425
|
"tableName": "employment_terms",
|
|
421
426
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
422
427
|
"extends": "SmrtCollection",
|
|
@@ -434,6 +439,7 @@
|
|
|
434
439
|
"name": "EmploymentChangeCollection",
|
|
435
440
|
"qualifiedName": "@happyvertical/smrt-human-resources:EmploymentChangeCollection",
|
|
436
441
|
"collection": "employmentchanges",
|
|
442
|
+
"description": "Scoped append-only change collection; closed like its model.",
|
|
437
443
|
"tableName": "employment_changes",
|
|
438
444
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
439
445
|
"extends": "SmrtCollection",
|
|
@@ -452,6 +458,7 @@
|
|
|
452
458
|
"qualifiedName": "@happyvertical/smrt-human-resources:Qualification",
|
|
453
459
|
"displayLabelField": "name",
|
|
454
460
|
"collection": "qualifications",
|
|
461
|
+
"description": "A qualification definition: a managed list per tenant. Other packages gate work by referencing its id (@happyvertical/smrt-human-resources:Qualification).",
|
|
455
462
|
"tableName": "qualifications",
|
|
456
463
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
457
464
|
"extends": "SmrtObject",
|
|
@@ -559,6 +566,7 @@
|
|
|
559
566
|
"name": "HeldQualification",
|
|
560
567
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualification",
|
|
561
568
|
"collection": "heldqualifications",
|
|
569
|
+
"description": "A person holds a qualification. employmentId is set when the definition's scope is employment; such a row is revoked when that employment ends.",
|
|
562
570
|
"tableName": "held_qualifications",
|
|
563
571
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
564
572
|
"extends": "SmrtObject",
|
|
@@ -734,6 +742,7 @@
|
|
|
734
742
|
"name": "HeldQualificationChange",
|
|
735
743
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualificationChange",
|
|
736
744
|
"collection": "heldqualificationchanges",
|
|
745
|
+
"description": "Append-only dated change to a held qualification, attributed to an actor.",
|
|
737
746
|
"tableName": "held_qualification_changes",
|
|
738
747
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
739
748
|
"extends": "SmrtObject",
|
|
@@ -841,6 +850,7 @@
|
|
|
841
850
|
"name": "QualificationCollection",
|
|
842
851
|
"qualifiedName": "@happyvertical/smrt-human-resources:QualificationCollection",
|
|
843
852
|
"collection": "qualifications",
|
|
853
|
+
"description": "Scoped definition collection; mutations must use QualificationService.",
|
|
844
854
|
"tableName": "qualifications",
|
|
845
855
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
846
856
|
"extends": "SmrtCollection",
|
|
@@ -858,6 +868,7 @@
|
|
|
858
868
|
"name": "HeldQualificationCollection",
|
|
859
869
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualificationCollection",
|
|
860
870
|
"collection": "heldqualifications",
|
|
871
|
+
"description": "Scoped held-qualification collection; closed like its model.",
|
|
861
872
|
"tableName": "held_qualifications",
|
|
862
873
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
863
874
|
"extends": "SmrtCollection",
|
|
@@ -875,6 +886,7 @@
|
|
|
875
886
|
"name": "HeldQualificationChangeCollection",
|
|
876
887
|
"qualifiedName": "@happyvertical/smrt-human-resources:HeldQualificationChangeCollection",
|
|
877
888
|
"collection": "heldqualificationchanges",
|
|
889
|
+
"description": "Scoped append-only change collection; closed like its model.",
|
|
878
890
|
"tableName": "held_qualification_changes",
|
|
879
891
|
"packageName": "@happyvertical/smrt-human-resources",
|
|
880
892
|
"extends": "SmrtCollection",
|
|
@@ -899,5 +911,14 @@
|
|
|
899
911
|
"polymorphicAssociations": 0,
|
|
900
912
|
"uuidColumns": 52
|
|
901
913
|
},
|
|
902
|
-
"agentDoc": "# smrt-human-resources\n\nHR records: who works for an organization, as what, and with what standing\n(#3435). It sits between identity (`smrt-profiles`) and pay (payroll, #3292):\npayroll → human-resources → profiles. It holds no pay, no time records, no\nlogins or permissions, and no identity or contact details.\n\n## Models\n\n| Model | Table | Notes |\n|---|---|---|\n| `Employment` | `employments` | One per tenant and profile. `profileId`, optional `userId` and `employerProfileId`, `employeeNumber` (unique per tenant), `workerType`, `position`, `status` (`active` \\| `on-leave` \\| `ended`) |\n| `EmploymentTerm` | `employment_terms` | One continuous period: `startedOn`, `endedOn` (last day, inclusive; null is open), `endReason`. A rehire adds a term |\n| `EmploymentChange` | `employment_changes` | Append-only dated change: `kind`, `effectiveOn`, `fromValue`, `toValue`, `actorProfileId` |\n| `Qualification` | `qualifications` | Definition, `key` unique per tenant: `kind`, `issuingBody`, `expires`, `validityMonths`, `scope` (`person` \\| `employment`) |\n| `HeldQualification` | `held_qualifications` | A person holds a qualification: `issuedOn`, `expiresOn`, `certificateNumber`, `status`, verifier, optional `documentAssetId`, `renewalOfId` |\n| `HeldQualificationChange` | `held_qualification_changes` | Append-only dated change: `kind`, `effectiveOn`, `reason`, `actorProfileId` |\n\n`EmploymentService` and `QualificationService` are the only writers (an\napplication subclass may save its own added fields; see \"Extending\nEmployment\"). Both are\nconstructed from a trusted `{ tenantId, profileId }` actor the application has\nalready authorized, and both take an optional `onEvent` hook. Construct them\nwith the root database handle, never one that is already inside a transaction\n(see \"Events need the root handle\").\n\n## Invariants\n\n- **Sensitive and closed.** Every model is `@TenantScoped({ mode: 'required' })`,\n `sensitive: true`, with `api` / `mcp` / `cli` set to `{ include: [] }`.\n `save()` throws outside a service (`src/write.ts`) and `delete()` always\n throws. Services insert rows only through `insertHr()` in `src/write.ts`,\n which uses the model's public constructor, `initialize()` and\n `requireInsertOnSave()`; never reach into a collection's protected\n `createUnsaved()`. A subclass must restate the closed surface: `@smrt` exposure is not\n inherited, and an empty `@smrt()` exposes everything.\n- **Collections are closed too.** Collection classes are registered like\n models, so each of the six carries\n `@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })` as an\n inline literal (the scanner cannot resolve an imported constant). An\n undecorated collection publishes its methods as MCP tools. A collection's\n config merges over its model's, so a model's effective `cli` reads `false`.\n `src/__tests__/closed-surface.test.ts` asserts no generated tool exists.\n- **The tenant is the employer.** `employerProfileId` is informational and\n never part of uniqueness. There is one `Employment` per tenant and profile,\n so (tenant, profile, date) resolves to at most one employment.\n- **The employment id is stable.** Rehire adds a term to the same row; other\n packages and applications key on `Employment.id`.\n- **Dates are calendar dates.** `YYYY-MM-DD` text, compared as strings, with\n no timezone. The application converts an instant to its local date first.\n `verifiedAt` is the one instant.\n- **\"Employed on a date\" comes from terms**, never from `status`. `on-leave`\n still counts as employed.\n- **`ended` means an end has been recorded**, not \"no longer employed today\".\n `end` accepts an `endedOn` ahead of the day it is called (a notice period)\n and stores `ended` at once; the package has no clock and never flips a\n status when a date passes. For \"on a date\" use `check`, `employedOn`, `asOf`\n or `findByUser(userId, on)`.\n- **Dated changes belong to the latest term.** `changePosition`,\n `changeWorkerType`, `placeOnLeave` and `returnFromLeave` are accepted when\n `effectiveOn` is inside the latest term: the open one, or after an end is\n recorded the final one up to and including its `endedOn`. On an ended\n employment the stored status stays `ended`, the row's `position` /\n `workerType` update as usual, leave is validated from the replayed history\n as of `effectiveOn` (not from `status`), and the usual event is raised. A\n date after the last day is `HR_STATUS_TRANSITION`; before the term started,\n `HR_INVALID`.\n- **History is append-only.** Changes are new rows. A term is closed once. A\n renewal is a new `HeldQualification` pointing at the old one through\n `renewalOfId`. A row stored `revoked` is never saved again, except for its\n verification fields (`HeldQualification.save()` allows a save that changes\n only `verifiedByProfileId`, `verifiedAt` and `documentAssetId`); a later\n suspension, reinstatement or earlier revocation of one is a further change\n row, never an edit.\n- **A stored qualification status is \"the last change recorded\", not\n standing, and no rule decides standing from it.** `revoke` and `suspend`\n store their status at once even when `effectiveOn` is ahead, and `reinstate` stores `valid`\n even when it takes effect later (`expired` if the sweep has recorded the\n row's lapse).\n The one sticky value is `revoked`: once stored it stays, whatever is\n recorded afterwards. Every rule decides from the dated history, as `check`\n does: the one-live-grant rule, renewal validation, `expiringWithin`, the\n employment-end cutoff, and the transitions themselves.\n- **Transitions are validated by replay as of `effectiveOn`.** On the latest\n row of a chain, from the chain's whole history: `suspend` needs the chain\n neither suspended nor revoked on `effectiveOn`; `reinstate` needs it\n suspended (and not revoked) on `effectiveOn`; both are `HR_INVALID` when\n dated before the chain's latest suspension or reinstatement, so those two\n are recorded in date order. `revoke` is exempt from that ordering (it is\n terminal and the earliest one wins in replay) and is refused only when a\n revocation effective on or before `effectiveOn` is already recorded\n (`HR_STATUS_TRANSITION`). So a revocation recorded for a later date (a\n scheduled one, an employment end with a last day ahead, a grant against a\n closed term) freezes nothing: until it takes effect the qualification can\n be suspended, reinstated, renewed, or revoked from an earlier day. A stored\n `expired` blocks none of them.\n- **A term holds its own changes.** `end` rejects an `endedOn` earlier than a\n position, worker-type or leave change recorded in the open term, and\n `rehire` a start before one, so replay (`check().onLeave`, `asOf`) never\n picks up a change dated outside its term. Login links are not replayed and\n are not part of this rule.\n- **Login links are immediate.** `linkLogin` / `unlinkLogin` write the stored\n `userId` at once; `effectiveOn` is only recorded on the change row. `asOf`\n does not replay them and `findByUser(userId, on)` reads the stored `userId`\n (only the terms are dated).\n- **A login is exclusive to the employment that stores it.** `hire`, `rehire`\n and `linkLogin` reject (`HR_INVALID`) a `userId` stored on any other\n employment in the tenant, whatever that employment's status: an ended\n employment keeps its login through its notice period and after, until\n `unlinkLogin` (which works on an ended employment) clears it. So two\n employments never share a login through the service, and neither lookup\n can be ambiguous; `findByUser` still fails closed on an ambiguous match as\n a backstop for rows written some other way. `findByUser(userId)` goes by\n the stored status; `findByUser(userId, on)` goes by terms covering `on`,\n whatever the status, and is what a host that records end dates ahead of\n time must call.\n- **A renewal chain shares its standing.** `check`, `holders` and\n `listForProfile` treat a row as not good on a date when a later row in its\n chain is suspended or revoked on that date. `suspend`, `reinstate` and\n `revoke` act on the latest row only (`HR_INVALID` on a row that was\n renewed), and the change belongs to the chain: `effectiveOn` may be any\n date from the chain's first `issuedOn` onward (see \"Transitions are\n validated by replay\" for the ordering rule). It may precede the latest\n row's own `issuedOn`. That is how a ticket in force today is withdrawn\n after its renewal was recorded ahead of time: the earlier row stops passing from\n `effectiveOn`, and the renewal does not restore standing when its issue\n date arrives (revoked from issue, or suspended from issue until\n reinstated). `listForProfile` reports the status of the latest row issued\n by the date, so it agrees with `check` instead of answering\n `not-yet-issued`.\n- **A renewal is validated by date.** `renew` rejects an `issuedOn` earlier\n than the row's own, or earlier than its latest suspension or reinstatement\n (`HR_INVALID`: the renewal would answer for dates the history says were\n suspended); a row suspended on `issuedOn` by replay\n (`HR_STATUS_TRANSITION`: reinstate first); a row revoked on or before\n `issuedOn` (`HR_INVALID`); an expiry earlier than the row's own, or any\n expiry on a renewal of a row that has none (`HR_INVALID`: the renewed row\n keeps answering for its own period, so a shorter renewal would lapse while\n `check` still passed; to replace a qualification with a shorter one,\n revoke it from that day and grant a new one); and a person who holds\n another chain that is not\n revoked on or before `issuedOn` and overlaps the renewal\n (`HR_ALREADY_HELD`). A chain that only starts on or after the day the\n renewal is cut off (its carried revocation) does not overlap, so a lapsed\n ticket can be renewed up to its scheduled revocation even when the next\n grant, dated from that day, was recorded first.\n- **A renewal carries a pending cutoff forward.** A row whose revocation\n takes effect after `issuedOn` is still in force and is renewed normally;\n replay only propagates standing from later rows to earlier ones, so the\n renewal records the cutoff on itself: a `revoked` change at the earlier of\n the old row's pending revocation (with its reason) and, inside a closed\n employment term, `endedOn + 1` (`employment-ended`; it wins a tie). The\n renewal is stored `revoked` and raises `renewed` then `revoked`. Each\n remaining latest row therefore holds its chain's revocation, which\n `expiringWithin`, the one-live-grant rule and `renew` rely on.\n- **One live grant, by date.** `grant` is refused (`HR_ALREADY_HELD`) unless\n every chain the person already holds of that qualification is revoked\n effective on or before the new `issuedOn`. A revocation recorded for a\n later date does not free an earlier issue date (the message says to renew\n the existing one), and a lapsed ticket, swept or not, is renewed instead of\n granted again.\n- **Verification is not a status.** `verify` sets `verifiedByProfileId` and\n `verifiedAt` (and optionally the document) on any row, including one stored\n `revoked`; it writes no change row and raises no event.\n- **Ids are validated.** A blank or missing id is `HR_INVALID` in both\n services (`HrService.id`); the `check` methods answer `not-employed` /\n `not-held` instead.\n- **Qualification scope.** A `person`-scoped held qualification survives a\n change of employment. An `employment`-scoped one carries `employmentId` and\n is revoked, with reason `employment-ended`, in the transaction that ends the\n employment. The revocation is effective the day after `endedOn`, so the\n qualification is still good on the last day employed. The cutoff is the\n earliest one, decided by date and per renewal chain\n (`revokeEmploymentQualifications`): the latest row of each chain of the\n employment gets it, with one `held-qualification.revoked` event, unless a\n revocation effective on or before it is already recorded anywhere in the\n chain. Rows that were renewed are not written; replay cuts them off through\n the chain. A latest row already stored `revoked` for a later date gets the\n change row only and the event carries the earlier date; replay takes the\n earliest revocation. A grant or renewal dated inside a term whose end is\n already recorded (granted during notice, or backdated) gets the same\n revocation in its own transaction and is stored `revoked` from the start.\n- **Expiry is computed.** A check on a date reads `expiresOn`; the stored\n `expired` status is written by `QualificationService.sweepExpired()`,\n which the application calls from its own scheduler (and by reinstating a\n row whose lapse the sweep has recorded); it is bookkeeping and\n no rule reads standing from it. The sweep decides which rows lapsed by\n date: every row whose last day is before `today`, that has no `expired`\n change yet and was not revoked on or before the day after `expiresOn`,\n gets an `expired` change and (unless it was renewed) the event. That\n includes a ticket that runs out while suspended, or while a revocation is\n recorded for a later day (a notice period). Only a row stored `valid` has\n its stored status set to `expired`; a stored `suspended` or `revoked`\n stays. Those rows are re-read on every run, so a scheduled sweep should\n pass `{ since }` (a day on or before the last day its previous run\n covered) to bound the work. `expiringWithin(days, today,\n { employedOnly })` lists rows that are good on `today` by the same dated\n evaluation as `check` (issued by `today`, not suspended or revoked on it,\n not renewed) and can keep only people employed on `today`.\n- **Events need the root handle.** A mutation begins its own transaction\n with `db.beginTransaction()`, commits it, then delivers its events.\n `@happyvertical/sql` has no after-commit hook and no marker for \"inside a\n transaction\" (happyvertical/sdk#1249), so on a handle that is already\n inside one (the `tx` of `db.transaction()`, a savepoint scope, or a\n `beginTransaction()` handle) the commit would be the caller's and an outer\n rollback could follow a delivered event. `HrService.transact` therefore\n refuses a mutation on such a handle with `HR_TRANSACTION_UNSUPPORTED`\n before writing anything. It recognises one by asking it to begin: a\n transaction-scoped handle either has no `beginTransaction` (libsql SQLite,\n PostgreSQL, DuckDB, JSON) or exposes one that throws\n `NestedTransactionError` without touching the connection (the\n native-capabilities SQLite adapter, opened for `vector` /\n `notifications`). Never go back to `db.transaction()` /\n `withEmbeddedWriteTransaction` here: on a transaction-scoped handle those\n re-enter the caller's transaction under a savepoint, or with\n `reuseUnsupportedNested` run on the caller's handle, and both fail open.\n Reads work on any handle. Tests that isolate with\n `createIsolatedTestDb*()` must hand the services `baseDb`, not the\n transaction handle.\n- **Tested on SQLite and PostgreSQL.** `HrService.identity` normalizes ids\n only on PostgreSQL. On any other adapter (DuckDB included) nothing is\n normalized, so callers must pass canonical lowercase UUIDs.\n- **Not a permission system.** A qualification says what a person may do in\n the real world. Who may use the software stays with `smrt-users`.\n- **People are `smrt-profiles:Profile` ids** in fields named `profileId` or\n `<role>ProfileId`, as cross-package refs. This is the id `smrt-timesheets`\n holds.\n\n## Extending Employment\n\nDeclare a same-named subclass over the `employments` table in the application,\nrestating the fields and the closed surface, and add application fields there\n(teamworks-os keeps succession rank and acting-role fields this way). Only one\ninheritance family may share a table; the deepest same-named subclass wins.\n\n`Employment.save()` lets such a subclass write its own fields: without the\nservice's write capability a save is allowed only for an existing row whose\npackage-owned columns (`tenantId`, `profileId`, `userId`, `employerProfileId`,\n`employeeNumber`, `workerType`, `position`, `status`) all still hold their\nstored values. A save that changes one of them, and any insert, throws\n`HR_WRITE_FORBIDDEN`; `delete()` always throws. Add a column to\n`PACKAGE_OWNED_COLUMNS` in `src/employment/models.ts` when the package gains\none.\n\n## Svelte UI\n\n`@happyvertical/smrt-human-resources/svelte` ships four components, each\nregistered as a slot in `HUMAN_RESOURCES_UI_SLOTS` (`src/ui.ts`):\n\n| Component | Slot | Shows |\n|---|---|---|\n| `EmployeeList` | `employee-list` | Name, employee number, position, worker type, status (plus \"Last day {date}\" when `endsOn` is set), start date |\n| `EmployeeForm` | `employee-form` | Employee number, worker type, position, start date (new hire only), effective date (edit only), optional login |\n| `PersonQualifications` | `person-qualifications` | One person's qualifications with their expiry state |\n| `ExpiringQualificationsList` | `expiring-qualifications-list` | Qualifications expiring soon across people, soonest first |\n\n- **Props-driven.** Components never load or save. The host calls the\n services, adapts rows with the helpers in `src/svelte/types.ts`\n (`toEmployeeView`, `toPersonQualificationView`,\n `toExpiringQualificationView`) and handles `onselect` / `onsubmit`.\n- **No clock in components.** `EmployeeList` shows the stored status badge\n and, when the host passes `endsOn` (the latest term's `endedOn`, fourth\n argument of `toEmployeeView`), the text \"Last day {date}\". It never compares\n a date with today to decide a label.\n- **Names come from the host.** HR holds no identity data, so every view\n carries a `displayName` the host supplies.\n- **`EmployeeForm` is hand-built** from smrt-ui form primitives (owner\n decision); the package does not depend on `smrt-fields` or `smrt-users`.\n- **`EmployeeForm` values are not a service input.** A hire returns\n `startedOn`; an edit returns a required `effectiveOn` (prefilled from\n `today`) that the host passes to `changePosition` / `changeWorkerType` /\n `linkLogin` / `unlinkLogin` for each value that changed. A login link or\n unlink takes effect at once whatever the date, so a future-dated edit that\n also clears the login unlinks it immediately; a host that wants it kept\n until then calls `unlinkLogin` on that day. The employee's\n current login stays an option even when the host's `logins` omit it, so a\n save never unlinks it by accident. Each validation message is tied to its\n field with `aria-describedby`.\n- **Day arithmetic is UTC** (`daysUntil`); never build a local-time `Date`\n from a calendar date.\n- **Expiry state is text plus a badge**, never colour alone. \"Expiring soon\"\n is a valid qualification whose last day is within `expiringSoonDays`\n (default 30), inclusive.\n- **Strings** go through `useI18n()` and the catalog in `src/svelte/i18n.ts`,\n keyed `human_resources.<component>.<key>`. Only smrt-ui primitives; no raw\n `<input>` / `<button>` / `<select>`.\n\n## Validation commands\n\n```bash\npnpm --filter @happyvertical/smrt-human-resources build\npnpm --filter @happyvertical/smrt-human-resources test\npnpm --filter @happyvertical/smrt-human-resources typecheck\n```\n\nBuild before `test`: `src/__tests__/employment-extension.test.ts` reads\n`dist/manifest.json`.\n"
|
|
914
|
+
"agentDoc": "# smrt-human-resources\n\nHR records: who works for an organization, as what, and with what standing\n(#3435). It sits between identity (`smrt-profiles`) and pay (payroll, #3292):\npayroll → human-resources → profiles. It holds no pay, no time records, no\nlogins or permissions, and no identity or contact details.\n\n## Models\n\n| Model | Table | Notes |\n|---|---|---|\n| `Employment` | `employments` | One per tenant and profile. `profileId`, optional `userId` and `employerProfileId`, `employeeNumber` (unique per tenant), `workerType`, `position`, `status` (`active` \\| `on-leave` \\| `ended`) |\n| `EmploymentTerm` | `employment_terms` | One continuous period: `startedOn`, `endedOn` (last day, inclusive; null is open), `endReason`. A rehire adds a term |\n| `EmploymentChange` | `employment_changes` | Append-only dated change: `kind`, `effectiveOn`, `fromValue`, `toValue`, `actorProfileId` |\n| `Qualification` | `qualifications` | Definition, `key` unique per tenant: `kind`, `issuingBody`, `expires`, `validityMonths`, `scope` (`person` \\| `employment`) |\n| `HeldQualification` | `held_qualifications` | A person holds a qualification: `issuedOn`, `expiresOn`, `certificateNumber`, `status`, verifier, optional `documentAssetId`, `renewalOfId` |\n| `HeldQualificationChange` | `held_qualification_changes` | Append-only dated change: `kind`, `effectiveOn`, `reason`, `actorProfileId` |\n\n`EmploymentService` and `QualificationService` are the only writers (an\napplication subclass may save its own added fields; see \"Extending\nEmployment\"). Both are\nconstructed from a trusted `{ tenantId, profileId }` actor the application has\nalready authorized, and both take an optional `onEvent` hook. Construct them\nwith the root database handle, never one that is already inside a transaction\n(see \"Events need the root handle\").\n\n## Invariants\n\n- **Sensitive and closed.** Every model is `@TenantScoped({ mode: 'required' })`,\n `sensitive: true`, with `api` / `mcp` / `cli` set to `{ include: [] }`.\n `save()` throws outside a service (`src/write.ts`) and `delete()` always\n throws. Services insert rows only through `insertHr()` in `src/write.ts`,\n which uses the model's public constructor, `initialize()` and\n `requireInsertOnSave()`; never reach into a collection's protected\n `createUnsaved()`. A subclass must restate the closed surface: `@smrt` exposure is not\n inherited, and an empty `@smrt()` exposes everything.\n- **Collections are closed too.** Collection classes are registered like\n models, so each of the six carries\n `@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })` as an\n inline literal (the scanner cannot resolve an imported constant). An\n undecorated collection publishes its methods as MCP tools. A collection's\n config merges over its model's, so a model's effective `cli` reads `false`.\n `src/__tests__/closed-surface.test.ts` asserts no generated tool exists.\n- **The tenant is the employer.** `employerProfileId` is informational and\n never part of uniqueness. There is one `Employment` per tenant and profile,\n so (tenant, profile, date) resolves to at most one employment.\n- **The employment id is stable.** Rehire adds a term to the same row; other\n packages and applications key on `Employment.id`.\n- **Dates are calendar dates.** `YYYY-MM-DD` text, compared as strings, with\n no timezone. The application converts an instant to its local date first.\n `verifiedAt` is the one instant.\n- **\"Employed on a date\" comes from terms**, never from `status`. `on-leave`\n still counts as employed.\n- **`ended` means an end has been recorded**, not \"no longer employed today\".\n `end` accepts an `endedOn` ahead of the day it is called (a notice period)\n and stores `ended` at once; the package has no clock and never flips a\n status when a date passes. For \"on a date\" use `check`, `employedOn`, `asOf`\n or `findByUser(userId, on)`.\n- **Dated changes belong to the latest term.** `changePosition`,\n `changeWorkerType`, `placeOnLeave` and `returnFromLeave` are accepted when\n `effectiveOn` is inside the latest term: the open one, or after an end is\n recorded the final one up to and including its `endedOn`. On an ended\n employment the stored status stays `ended`, the row's `position` /\n `workerType` update as usual, leave is validated from the replayed history\n as of `effectiveOn` (not from `status`), and the usual event is raised. A\n date after the last day is `HR_STATUS_TRANSITION`; before the term started,\n `HR_INVALID`.\n- **History is append-only.** Changes are new rows. A term is closed once. A\n renewal is a new `HeldQualification` pointing at the old one through\n `renewalOfId`. A row stored `revoked` is never saved again, except for its\n verification fields (`HeldQualification.save()` allows a save that changes\n only `verifiedByProfileId`, `verifiedAt` and `documentAssetId`); a later\n suspension, reinstatement or earlier revocation of one is a further change\n row, never an edit.\n- **A stored qualification status is \"the last change recorded\", not\n standing, and no rule decides standing from it.** `revoke` and `suspend`\n store their status at once even when `effectiveOn` is ahead, and `reinstate` stores `valid`\n even when it takes effect later (`expired` if the sweep has recorded the\n row's lapse).\n The one sticky value is `revoked`: once stored it stays, whatever is\n recorded afterwards. Every rule decides from the dated history, as `check`\n does: the one-live-grant rule, renewal validation, `expiringWithin`, the\n employment-end cutoff, and the transitions themselves.\n- **Transitions are validated by replay as of `effectiveOn`.** On the latest\n row of a chain, from the chain's whole history: `suspend` needs the chain\n neither suspended nor revoked on `effectiveOn`; `reinstate` needs it\n suspended (and not revoked) on `effectiveOn`; both are `HR_INVALID` when\n dated before the chain's latest suspension or reinstatement, so those two\n are recorded in date order. `revoke` is exempt from that ordering (it is\n terminal and the earliest one wins in replay) and is refused only when a\n revocation effective on or before `effectiveOn` is already recorded\n (`HR_STATUS_TRANSITION`). So a revocation recorded for a later date (a\n scheduled one, an employment end with a last day ahead, a grant against a\n closed term) freezes nothing: until it takes effect the qualification can\n be suspended, reinstated, renewed, or revoked from an earlier day. A stored\n `expired` blocks none of them.\n- **A term holds its own changes.** `end` rejects an `endedOn` earlier than a\n position, worker-type or leave change recorded in the open term, and\n `rehire` a start before one, so replay (`check().onLeave`, `asOf`) never\n picks up a change dated outside its term. Login links are not replayed and\n are not part of this rule.\n- **Login links are immediate.** `linkLogin` / `unlinkLogin` write the stored\n `userId` at once; `effectiveOn` is only recorded on the change row. `asOf`\n does not replay them and `findByUser(userId, on)` reads the stored `userId`\n (only the terms are dated).\n- **A login is exclusive to the employment that stores it.** `hire`, `rehire`\n and `linkLogin` reject (`HR_INVALID`) a `userId` stored on any other\n employment in the tenant, whatever that employment's status: an ended\n employment keeps its login through its notice period and after, until\n `unlinkLogin` (which works on an ended employment) clears it. So two\n employments never share a login through the service, and neither lookup\n can be ambiguous; `findByUser` still fails closed on an ambiguous match as\n a backstop for rows written some other way. `findByUser(userId)` goes by\n the stored status; `findByUser(userId, on)` goes by terms covering `on`,\n whatever the status, and is what a host that records end dates ahead of\n time must call.\n- **A renewal chain shares its standing.** `check`, `holders` and\n `listForProfile` treat a row as not good on a date when a later row in its\n chain is suspended or revoked on that date. `suspend`, `reinstate` and\n `revoke` act on the latest row only (`HR_INVALID` on a row that was\n renewed), and the change belongs to the chain: `effectiveOn` may be any\n date from the chain's first `issuedOn` onward (see \"Transitions are\n validated by replay\" for the ordering rule). It may precede the latest\n row's own `issuedOn`. That is how a ticket in force today is withdrawn\n after its renewal was recorded ahead of time: the earlier row stops passing from\n `effectiveOn`, and the renewal does not restore standing when its issue\n date arrives (revoked from issue, or suspended from issue until\n reinstated). `listForProfile` reports the status of the latest row issued\n by the date, so it agrees with `check` instead of answering\n `not-yet-issued`.\n- **A renewal is validated by date.** `renew` rejects an `issuedOn` earlier\n than the row's own, or earlier than its latest suspension or reinstatement\n (`HR_INVALID`: the renewal would answer for dates the history says were\n suspended); a row suspended on `issuedOn` by replay\n (`HR_STATUS_TRANSITION`: reinstate first); a row revoked on or before\n `issuedOn` (`HR_INVALID`); an expiry earlier than the row's own, or any\n expiry on a renewal of a row that has none (`HR_INVALID`: the renewed row\n keeps answering for its own period, so a shorter renewal would lapse while\n `check` still passed; to replace a qualification with a shorter one,\n revoke it from that day and grant a new one); and a person who holds\n another chain that is not\n revoked on or before `issuedOn` and overlaps the renewal\n (`HR_ALREADY_HELD`). A chain that only starts on or after the day the\n renewal is cut off (its carried revocation) does not overlap, so a lapsed\n ticket can be renewed up to its scheduled revocation even when the next\n grant, dated from that day, was recorded first.\n- **A renewal carries a pending cutoff forward.** A row whose revocation\n takes effect after `issuedOn` is still in force and is renewed normally;\n replay only propagates standing from later rows to earlier ones, so the\n renewal records the cutoff on itself: a `revoked` change at the earlier of\n the old row's pending revocation (with its reason) and, inside a closed\n employment term, `endedOn + 1` (`employment-ended`; it wins a tie). The\n renewal is stored `revoked` and raises `renewed` then `revoked`. Each\n remaining latest row therefore holds its chain's revocation, which\n `expiringWithin`, the one-live-grant rule and `renew` rely on.\n- **One live grant, by date.** `grant` is refused (`HR_ALREADY_HELD`) unless\n every chain the person already holds of that qualification is revoked\n effective on or before the new `issuedOn`. A revocation recorded for a\n later date does not free an earlier issue date (the message says to renew\n the existing one), and a lapsed ticket, swept or not, is renewed instead of\n granted again.\n- **Verification is not a status.** `verify` sets `verifiedByProfileId` and\n `verifiedAt` (and optionally the document) on any row, including one stored\n `revoked`; it writes no change row and raises no event.\n- **Ids are validated.** A blank or missing id is `HR_INVALID` in both\n services (`HrService.id`); the `check` methods answer `not-employed` /\n `not-held` instead.\n- **Qualification scope.** A `person`-scoped held qualification survives a\n change of employment. An `employment`-scoped one carries `employmentId` and\n is revoked, with reason `employment-ended`, in the transaction that ends the\n employment. The revocation is effective the day after `endedOn`, so the\n qualification is still good on the last day employed. The cutoff is the\n earliest one, decided by date and per renewal chain\n (`revokeEmploymentQualifications`): the latest row of each chain of the\n employment gets it, with one `held-qualification.revoked` event, unless a\n revocation effective on or before it is already recorded anywhere in the\n chain. Rows that were renewed are not written; replay cuts them off through\n the chain. A latest row already stored `revoked` for a later date gets the\n change row only and the event carries the earlier date; replay takes the\n earliest revocation. A grant or renewal dated inside a term whose end is\n already recorded (granted during notice, or backdated) gets the same\n revocation in its own transaction and is stored `revoked` from the start.\n- **Expiry is computed.** A check on a date reads `expiresOn`; the stored\n `expired` status is written by `QualificationService.sweepExpired()`,\n which the application calls from its own scheduler (and by reinstating a\n row whose lapse the sweep has recorded); it is bookkeeping and\n no rule reads standing from it. The sweep decides which rows lapsed by\n date: every row whose last day is before `today`, that has no `expired`\n change yet and was not revoked on or before the day after `expiresOn`,\n gets an `expired` change and (unless it was renewed) the event. That\n includes a ticket that runs out while suspended, or while a revocation is\n recorded for a later day (a notice period). Only a row stored `valid` has\n its stored status set to `expired`; a stored `suspended` or `revoked`\n stays. Those rows are re-read on every run, so a scheduled sweep should\n pass `{ since }` (a day on or before the last day its previous run\n covered) to bound the work. `expiringWithin(days, today,\n { employedOnly })` lists rows that are good on `today` by the same dated\n evaluation as `check` (issued by `today`, not suspended or revoked on it,\n not renewed) and can keep only people employed on `today`.\n- **Events need the root handle.** A mutation begins its own transaction\n with `db.beginTransaction()`, commits it, then delivers its events.\n `@happyvertical/sql` has no after-commit hook and no marker for \"inside a\n transaction\" (happyvertical/sdk#1249), so on a handle that is already\n inside one (the `tx` of `db.transaction()`, a savepoint scope, or a\n `beginTransaction()` handle) the commit would be the caller's and an outer\n rollback could follow a delivered event. `HrService.transact` therefore\n refuses a mutation on such a handle with `HR_TRANSACTION_UNSUPPORTED`\n before writing anything. It recognises one by asking it to begin: a\n transaction-scoped handle either has no `beginTransaction` (libsql SQLite,\n PostgreSQL, DuckDB, JSON) or exposes one that throws\n `NestedTransactionError` without touching the connection (the\n native-capabilities SQLite adapter, opened for `vector` /\n `notifications`). Never go back to `db.transaction()` /\n `withEmbeddedWriteTransaction` here: on a transaction-scoped handle those\n re-enter the caller's transaction under a savepoint, or with\n `reuseUnsupportedNested` run on the caller's handle, and both fail open.\n Reads work on any handle. Tests that isolate with\n `createIsolatedTestDb*()` must hand the services `baseDb`, not the\n transaction handle.\n- **Tested on SQLite and PostgreSQL.** `HrService.identity` normalizes ids\n only on PostgreSQL. On any other adapter (DuckDB included) nothing is\n normalized, so callers must pass canonical lowercase UUIDs.\n- **Not a permission system.** A qualification says what a person may do in\n the real world. Who may use the software stays with `smrt-users`.\n- **People are `smrt-profiles:Profile` ids** in fields named `profileId` or\n `<role>ProfileId`, as cross-package refs. This is the id `smrt-timesheets`\n holds.\n\n## Extending Employment\n\nDeclare a same-named subclass over the `employments` table in the application,\nrestating the fields and the closed surface, and add application fields there\n(teamworks-os keeps succession rank and acting-role fields this way). Only one\ninheritance family may share a table; the deepest same-named subclass wins.\n\n`Employment.save()` lets such a subclass write its own fields: without the\nservice's write capability a save is allowed only for an existing row whose\npackage-owned columns (`tenantId`, `profileId`, `userId`, `employerProfileId`,\n`employeeNumber`, `workerType`, `position`, `status`) all still hold their\nstored values. A save that changes one of them, and any insert, throws\n`HR_WRITE_FORBIDDEN`; `delete()` always throws. Add a column to\n`PACKAGE_OWNED_COLUMNS` in `src/employment/models.ts` when the package gains\none.\n\n## Svelte UI\n\n`@happyvertical/smrt-human-resources/svelte` ships four components, each\nregistered as a slot in `HUMAN_RESOURCES_UI_SLOTS` (`src/ui.ts`):\n\n| Component | Slot | Shows |\n|---|---|---|\n| `EmployeeList` | `employee-list` | Name, employee number, position, worker type, status (plus \"Last day {date}\" when `endsOn` is set), start date |\n| `EmployeeForm` | `employee-form` | Employee number, worker type, position, start date (new hire only), effective date (edit only), optional login |\n| `PersonQualifications` | `person-qualifications` | One person's qualifications with their expiry state |\n| `ExpiringQualificationsList` | `expiring-qualifications-list` | Qualifications expiring soon across people, soonest first |\n\n- **Props-driven.** Components never load or save. The host calls the\n services, adapts rows with the helpers in `src/svelte/types.ts`\n (`toEmployeeView`, `toPersonQualificationView`,\n `toExpiringQualificationView`) and handles `onselect` / `onsubmit`.\n- **No clock in components.** `EmployeeList` shows the stored status badge\n and, when the host passes `endsOn` (the latest term's `endedOn`, fourth\n argument of `toEmployeeView`), the text \"Last day {date}\". It never compares\n a date with today to decide a label.\n- **Names come from the host.** HR holds no identity data, so every view\n carries a `displayName` the host supplies.\n- **`EmployeeForm` is hand-built** from smrt-ui form primitives (owner\n decision); the package does not depend on `smrt-fields` or `smrt-users`.\n- **`EmployeeForm` values are not a service input.** A hire returns\n `startedOn`; an edit returns a required `effectiveOn` (prefilled from\n `today`) that the host passes to `changePosition` / `changeWorkerType` /\n `linkLogin` / `unlinkLogin` for each value that changed. A login link or\n unlink takes effect at once whatever the date, so a future-dated edit that\n also clears the login unlinks it immediately; a host that wants it kept\n until then calls `unlinkLogin` on that day. The employee's\n current login stays an option even when the host's `logins` omit it, so a\n save never unlinks it by accident. Each validation message is tied to its\n field with `aria-describedby`.\n- **Day arithmetic is UTC** (`daysUntil`); never build a local-time `Date`\n from a calendar date.\n- **Expiry state is text plus a badge**, never colour alone. \"Expiring soon\"\n is a valid qualification whose last day is within `expiringSoonDays`\n (default 30), inclusive.\n- **Strings** go through `useI18n()` and the catalog in `src/svelte/i18n.ts`,\n keyed `human_resources.<component>.<key>`. Only smrt-ui primitives; no raw\n `<input>` / `<button>` / `<select>`.\n\n## Validation commands\n\n```bash\npnpm --filter @happyvertical/smrt-human-resources build\npnpm --filter @happyvertical/smrt-human-resources test\npnpm --filter @happyvertical/smrt-human-resources typecheck\n```\n\nBuild before `test`: `src/__tests__/employment-extension.test.ts` reads\n`dist/manifest.json`.\n",
|
|
915
|
+
"browser": {
|
|
916
|
+
"status": "server-only",
|
|
917
|
+
"issues": [
|
|
918
|
+
"#3624"
|
|
919
|
+
],
|
|
920
|
+
"via": [
|
|
921
|
+
"@happyvertical/smrt-tenancy"
|
|
922
|
+
]
|
|
923
|
+
}
|
|
903
924
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-human-resources",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.8",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "HR records for the SMRT framework: who works for an organization, as what, and with what standing — employment with dated terms, and qualifications with expiry",
|
|
6
6
|
"type": "module",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"./manifest.json": "./dist/manifest.json"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@happyvertical/logger": "^0.
|
|
34
|
-
"@happyvertical/smrt-core": "0.55.
|
|
35
|
-
"@happyvertical/smrt-tenancy": "0.55.
|
|
36
|
-
"@happyvertical/smrt-types": "0.55.
|
|
37
|
-
"@happyvertical/smrt-ui": "0.55.
|
|
38
|
-
"@happyvertical/sql": "^0.
|
|
33
|
+
"@happyvertical/logger": "^0.103.1",
|
|
34
|
+
"@happyvertical/smrt-core": "0.55.8",
|
|
35
|
+
"@happyvertical/smrt-tenancy": "0.55.8",
|
|
36
|
+
"@happyvertical/smrt-types": "0.55.8",
|
|
37
|
+
"@happyvertical/smrt-ui": "0.55.8",
|
|
38
|
+
"@happyvertical/sql": "^0.103.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"svelte": "^5.57.0"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@happyvertical/smrt-vitest": "0.55.
|
|
49
|
+
"@happyvertical/smrt-vitest": "0.55.8",
|
|
50
50
|
"@sveltejs/package": "^2.5.8",
|
|
51
51
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
52
52
|
"@types/node": "24.13.6",
|