@happyvertical/smrt-cli 0.60.9 → 0.60.10
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/AGENTS.md +17 -53
- package/agents/db-drop-framework-base-tables.md +49 -0
- package/agents/db-migrate-concurrency.md +11 -0
- package/dist/{AuditLogCollection-BBbvr8Qc-CoDs0BaX.js → AuditLogCollection-BBbvr8Qc-DU-QXhH5.js} +1 -1
- package/dist/{OidcIdentity-COeMidoC-B-cVovG3.js → OidcIdentity-COeMidoC-BDr8b6iW.js} +2 -2
- package/dist/{OidcIdentityCollection-D4DIZv0K-CexIL7No.js → OidcIdentityCollection-D4DIZv0K-BvsaDTZY.js} +1 -1
- package/dist/{PasswordCredentialService-DHJNKEeJ-DQlR62e-.js → PasswordCredentialService-BSSP_kyT-DlxlWy6b.js} +40 -22
- package/dist/{ProfileAssetCollection-D5-tG9Mm-Bmn4-Urc.js → ProfileAssetCollection-D5-tG9Mm-Be7hQJJq.js} +3 -3
- package/dist/{ProfileCollection-CmRaiE9U-52qjwbhJ.js → ProfileCollection-CmRaiE9U-n8apgXIh.js} +17 -17
- package/dist/{ProfileLinkCollection-pvPTsysg-ocN3hj_M.js → ProfileLinkCollection-pvPTsysg-DCmnAd7m.js} +2 -2
- package/dist/{ProfileMetadataCollection-CAgJyjze-FX20yKjY.js → ProfileMetadataCollection-CAgJyjze-BKpcvrnf.js} +1 -1
- package/dist/{ProfileMetafieldCollection-19ae6Lpf-qg7eMcJk.js → ProfileMetafieldCollection-19ae6Lpf-BuTJKzmz.js} +1 -1
- package/dist/{ProfileRelationshipCollection-BFSlTQ7J-DlDILj3m.js → ProfileRelationshipCollection-BFSlTQ7J-DBbNdauk.js} +1 -1
- package/dist/{ProfileTypeCollection-BRLK8cHS-QVwjfqe8.js → ProfileTypeCollection-BRLK8cHS-COCy6yqF.js} +2 -2
- package/dist/commands/docs-claude.d.ts +1 -0
- package/dist/commands/docs-claude.d.ts.map +1 -1
- package/dist/commands/docs-knowledge-bundle.d.ts +87 -0
- package/dist/commands/docs-knowledge-bundle.d.ts.map +1 -0
- package/dist/{commands-DNssH5Dv.js → commands-CGfM14Uj.js} +265 -6
- package/dist/{dist-BJABhykg.js → dist-B00KCyNE.js} +2 -2
- package/dist/{dist-D25iWIXX.js → dist-BGEMcSs-.js} +5 -5
- package/dist/dist-D7vejSNK.js +1854 -0
- package/dist/dist-DEKj_L2B.js +1908 -0
- package/dist/index.js +11 -11
- package/dist/knowledge/intents/audit-trail.md +63 -0
- package/dist/knowledge/intents/auth-and-users.md +67 -0
- package/dist/knowledge/intents/background-work.md +72 -0
- package/dist/knowledge/intents/feature-flags-and-entitlements.md +60 -0
- package/dist/knowledge/intents/file-and-asset-storage.md +71 -0
- package/dist/knowledge/intents/human-approvals.md +56 -0
- package/dist/knowledge/intents/index.md +41 -0
- package/dist/knowledge/intents/money-and-rates.md +65 -0
- package/dist/knowledge/intents/outbound-messages.md +67 -0
- package/dist/knowledge/intents/prompts-and-ai-operations.md +72 -0
- package/dist/knowledge/intents/relationships-and-trees.md +56 -0
- package/dist/knowledge/intents/scheduled-runs.md +61 -0
- package/dist/knowledge/intents/search-facts-knowledge.md +68 -0
- package/dist/knowledge/intents/secrets-and-config.md +66 -0
- package/dist/knowledge/intents/tenant-scoping.md +63 -0
- package/dist/knowledge/intents/webhooks-and-events.md +71 -0
- package/dist/{oidcProvisioningPrimitives-KpIiLmG4-CnRTofeV.js → oidcProvisioningPrimitives-KpIiLmG4-Csnh1ENp.js} +3 -3
- package/dist/{operator-fwao3Y1j.js → operator-DCwooIiI.js} +2 -2
- package/dist/{resolveIdentity-j2WFuKDT-C9XtigVZ.js → resolveIdentity-j2WFuKDT-DZTwcb-u.js} +14 -14
- package/dist/smrt-knowledge.okf.md +57 -0
- package/package.json +15 -15
- package/dist/dist-B3gpMpBj.js +0 -1908
- package/dist/dist-BQI8s_yA.js +0 -1854
package/dist/index.js
CHANGED
|
@@ -51,77 +51,77 @@ var _cookbookCommands = null;
|
|
|
51
51
|
var _kitchenCommands = null;
|
|
52
52
|
async function getGnodeCommands() {
|
|
53
53
|
if (!_gnodeCommands) {
|
|
54
|
-
const { gnodeCommands } = await import("./commands-
|
|
54
|
+
const { gnodeCommands } = await import("./commands-CGfM14Uj.js");
|
|
55
55
|
_gnodeCommands = gnodeCommands;
|
|
56
56
|
}
|
|
57
57
|
return _gnodeCommands;
|
|
58
58
|
}
|
|
59
59
|
async function getGitCommands() {
|
|
60
60
|
if (!_gitCommands) {
|
|
61
|
-
const { gitCommands } = await import("./commands-
|
|
61
|
+
const { gitCommands } = await import("./commands-CGfM14Uj.js");
|
|
62
62
|
_gitCommands = gitCommands;
|
|
63
63
|
}
|
|
64
64
|
return _gitCommands;
|
|
65
65
|
}
|
|
66
66
|
async function getGenerateCommands() {
|
|
67
67
|
if (!_generateCommands) {
|
|
68
|
-
const { generateCommands } = await import("./commands-
|
|
68
|
+
const { generateCommands } = await import("./commands-CGfM14Uj.js");
|
|
69
69
|
_generateCommands = generateCommands;
|
|
70
70
|
}
|
|
71
71
|
return _generateCommands;
|
|
72
72
|
}
|
|
73
73
|
async function getInitCommands() {
|
|
74
74
|
if (!_initCommands) {
|
|
75
|
-
const { initCommands } = await import("./commands-
|
|
75
|
+
const { initCommands } = await import("./commands-CGfM14Uj.js");
|
|
76
76
|
_initCommands = initCommands;
|
|
77
77
|
}
|
|
78
78
|
return _initCommands;
|
|
79
79
|
}
|
|
80
80
|
async function getUtilityCommands() {
|
|
81
81
|
if (!_utilityCommands) {
|
|
82
|
-
const { utilityCommands } = await import("./commands-
|
|
82
|
+
const { utilityCommands } = await import("./commands-CGfM14Uj.js");
|
|
83
83
|
_utilityCommands = utilityCommands;
|
|
84
84
|
}
|
|
85
85
|
return _utilityCommands;
|
|
86
86
|
}
|
|
87
87
|
async function getDispatchCommands() {
|
|
88
88
|
if (!_dispatchCommands) {
|
|
89
|
-
const { dispatchCommands } = await import("./commands-
|
|
89
|
+
const { dispatchCommands } = await import("./commands-CGfM14Uj.js");
|
|
90
90
|
_dispatchCommands = dispatchCommands;
|
|
91
91
|
}
|
|
92
92
|
return _dispatchCommands;
|
|
93
93
|
}
|
|
94
94
|
async function getDocsCommands() {
|
|
95
95
|
if (!_docsCommands) {
|
|
96
|
-
const { docsCommands } = await import("./commands-
|
|
96
|
+
const { docsCommands } = await import("./commands-CGfM14Uj.js");
|
|
97
97
|
_docsCommands = docsCommands;
|
|
98
98
|
}
|
|
99
99
|
return _docsCommands;
|
|
100
100
|
}
|
|
101
101
|
async function getPlaygroundCommands() {
|
|
102
102
|
if (!_playgroundCommands) {
|
|
103
|
-
const { playgroundCommands } = await import("./commands-
|
|
103
|
+
const { playgroundCommands } = await import("./commands-CGfM14Uj.js");
|
|
104
104
|
_playgroundCommands = playgroundCommands;
|
|
105
105
|
}
|
|
106
106
|
return _playgroundCommands;
|
|
107
107
|
}
|
|
108
108
|
async function getCookbookCommands() {
|
|
109
109
|
if (!_cookbookCommands) {
|
|
110
|
-
const { cookbookCommands } = await import("./commands-
|
|
110
|
+
const { cookbookCommands } = await import("./commands-CGfM14Uj.js");
|
|
111
111
|
_cookbookCommands = cookbookCommands;
|
|
112
112
|
}
|
|
113
113
|
return _cookbookCommands;
|
|
114
114
|
}
|
|
115
115
|
async function getKitchenCommands() {
|
|
116
116
|
if (!_kitchenCommands) {
|
|
117
|
-
const { kitchenCommands } = await import("./commands-
|
|
117
|
+
const { kitchenCommands } = await import("./commands-CGfM14Uj.js");
|
|
118
118
|
_kitchenCommands = kitchenCommands;
|
|
119
119
|
}
|
|
120
120
|
return _kitchenCommands;
|
|
121
121
|
}
|
|
122
122
|
async function getWorkbenchCommands() {
|
|
123
123
|
if (!_workbenchCommands) {
|
|
124
|
-
const { workbenchCommands } = await import("./commands-
|
|
124
|
+
const { workbenchCommands } = await import("./commands-CGfM14Uj.js");
|
|
125
125
|
_workbenchCommands = workbenchCommands;
|
|
126
126
|
}
|
|
127
127
|
return _workbenchCommands;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "Audit trail and change feed"
|
|
4
|
+
description: "Use @smrt({ audit }) for who-changed-what records and the _smrt_changes feed for observing writes, instead of history tables or triggers."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/core/src/audit.ts
|
|
19
|
+
path: packages/core/src/audit.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/core/src/audit.ts (requireAuditOptions, auditSnapshot, writeAudit, AuditWriter)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/core/src/change-feed.ts
|
|
24
|
+
path: packages/core/src/change-feed.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/core/src/change-feed.ts (appendChange, getChangesSince, getTenantScopedChangesSince)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/core/src/change-feed-sensitivity.ts
|
|
29
|
+
path: packages/core/src/change-feed-sensitivity.ts
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/core/src/change-feed-sensitivity.ts (sensitive tables)"
|
|
32
|
+
- id: s4
|
|
33
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/smrt-dev-mcp/src/tool-catalog.ts
|
|
34
|
+
path: packages/smrt-dev-mcp/src/tool-catalog.ts
|
|
35
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
36
|
+
title: "packages/smrt-dev-mcp/src/tool-catalog.ts (recent-changes tool)"
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Audit trail and change feed
|
|
40
|
+
|
|
41
|
+
# Entry point
|
|
42
|
+
|
|
43
|
+
Two different things. Audit (who changed which fields): put `audit: true` in the model's `@smrt()` config and bind identity with `withAuditContext({ actorId, writer, reason, source }, fn)` from `@happyvertical/smrt-core`; the `writer` receives an `AuditEntry` with per-field before/after. Change feed (what changed, in order): every framework `save()`/`delete()` appends a row to `_smrt_changes`; read it with `getChangesSince(db, { since, tables, limit })` or, for request handlers, `getTenantScopedChangesSince()`. For a deployed database, the `recent-changes` tool of `smrt-dev-mcp` tails the same table read-only.[^s4]
|
|
44
|
+
|
|
45
|
+
# Why not hand-roll it
|
|
46
|
+
|
|
47
|
+
`requireAuditOptions()` in `audit.ts` throws when an audited mutation has no writer or no authenticated `actorId`, `auditSnapshot()` records the public projection so credential and permission-gated fields never enter the audit row, `writeAudit()` refuses an entity with ambiguous tenant ownership, and the writer contract requires using the same transaction handle and propagating failures.[^s1] The change feed in `change-feed.ts` allocates `seq` as `MAX(seq)+1` inside the append rather than a native identity column, so a polling cursor cannot skip a change under concurrent writers on SQLite, PostgreSQL or DuckDB; deletes are tombstones, and `getTenantScopedChangesSince()` fails closed to global rows when tenancy is on and no tenant is active.[^s2] Credential-bearing tables are refused by the feed (`change-feed-sensitivity.ts`) because a row id such as a session id is itself a secret.[^s3]
|
|
48
|
+
|
|
49
|
+
# Component pages
|
|
50
|
+
|
|
51
|
+
- [smrt-core](/components/smrt-core.md)
|
|
52
|
+
- [smrt-dev-mcp](/components/smrt-dev-mcp.md)
|
|
53
|
+
|
|
54
|
+
# Detection signals
|
|
55
|
+
|
|
56
|
+
- `regex:audit_?log`
|
|
57
|
+
- `regex:audit_?trail`
|
|
58
|
+
- `regex:CREATE TABLE\s+\w*audit`
|
|
59
|
+
|
|
60
|
+
[^s1]: `packages/core/src/audit.ts` at `478ae36900`: requireAuditOptions, auditSnapshot, writeAudit, AuditWriter.
|
|
61
|
+
[^s2]: `packages/core/src/change-feed.ts` at `478ae36900`: appendChange, getChangesSince, getTenantScopedChangesSince.
|
|
62
|
+
[^s3]: `packages/core/src/change-feed-sensitivity.ts` at `478ae36900`: sensitive tables.
|
|
63
|
+
[^s4]: `packages/smrt-dev-mcp/src/tool-catalog.ts` at `478ae36900`: recent-changes tool.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "Auth, users and sessions"
|
|
4
|
+
description: "Reach for smrt-users before writing password hashing, sessions, login throttling or role checks."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/users/src/services/credential-hash.ts
|
|
19
|
+
path: packages/users/src/services/credential-hash.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/users/src/services/credential-hash.ts (scrypt hash, timingSafeEqual, scryptHashNeedsUpgrade)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/users/src/services/LoginAttemptLimiter.ts
|
|
24
|
+
path: packages/users/src/services/LoginAttemptLimiter.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/users/src/services/LoginAttemptLimiter.ts (LoginAttemptLimiter.reserve, DEFAULT_LOGIN_*)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/users/src/services/SessionService.ts
|
|
29
|
+
path: packages/users/src/services/SessionService.ts
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/users/src/services/SessionService.ts (SessionService.createSession)"
|
|
32
|
+
- id: s4
|
|
33
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/users/AGENTS.md
|
|
34
|
+
path: packages/users/AGENTS.md
|
|
35
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
36
|
+
title: "packages/users/AGENTS.md (models and authority)"
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Auth, users and sessions
|
|
40
|
+
|
|
41
|
+
# Entry point
|
|
42
|
+
|
|
43
|
+
Import from `@happyvertical/smrt-users` (SvelteKit handlers from `@happyvertical/smrt-users/sveltekit`). Identity is `User` (global) plus `Membership` per `Tenant` with `Role` permissions resolved by `PermissionResolver`; sessions by `SessionService.createSession(userId, tenantId)`; email-and-password sign-in by `PasswordCredentialService`, with passkey, device PIN, OIDC and mobile services alongside. Request-scoped tenant context comes from smrt-tenancy adapters (`createSvelteKitHandle`, `createExpressMiddleware`).[^s3]
|
|
44
|
+
|
|
45
|
+
# Why not hand-roll it
|
|
46
|
+
|
|
47
|
+
`credential-hash.ts` stores a self-describing `scrypt$N$r$p$salt$hash` string, compares with `timingSafeEqual`, and exposes `scryptHashNeedsUpgrade()` so stronger parameters roll out at the next sign-in.[^s1] `LoginAttemptLimiter` draws attempts from two independent budgets, the submitted subject and the source, stores both as hashes, locks out with exponential backoff (defaults: 5 attempts per 5 minutes, 60 s base, doubling, capped at one hour), and its contract says the credential check always runs so a response never reveals whether an identifier maps to an account.[^s2] The package invariants also make users global with tenant access only through `Membership`, and derive tenant hierarchy paths on save rather than trusting callers.[^s4] A hand-rolled login with bcrypt or a JWT library has none of the throttling, non-enumeration or tenant model.
|
|
48
|
+
|
|
49
|
+
# Component pages
|
|
50
|
+
|
|
51
|
+
- [smrt-users](/components/smrt-users.md)
|
|
52
|
+
- [smrt-tenancy](/components/smrt-tenancy.md)
|
|
53
|
+
|
|
54
|
+
# Detection signals
|
|
55
|
+
|
|
56
|
+
- `import:bcrypt`
|
|
57
|
+
- `import:bcryptjs`
|
|
58
|
+
- `import:argon2`
|
|
59
|
+
- `import:jsonwebtoken`
|
|
60
|
+
- `import:passport`
|
|
61
|
+
- `import:express-session`
|
|
62
|
+
- `import:lucia`
|
|
63
|
+
|
|
64
|
+
[^s1]: `packages/users/src/services/credential-hash.ts` at `478ae36900`: scrypt hash, timingSafeEqual, scryptHashNeedsUpgrade.
|
|
65
|
+
[^s2]: `packages/users/src/services/LoginAttemptLimiter.ts` at `478ae36900`: LoginAttemptLimiter.reserve, DEFAULT_LOGIN_*.
|
|
66
|
+
[^s3]: `packages/users/src/services/SessionService.ts` at `478ae36900`: SessionService.createSession.
|
|
67
|
+
[^s4]: `packages/users/AGENTS.md` at `478ae36900`: models and authority.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "Background work and retries"
|
|
4
|
+
description: "Reach for smrt-jobs before writing a queue, worker loop, retry counter or job table."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/jobs/src/smrt-job.ts
|
|
19
|
+
path: packages/jobs/src/smrt-job.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/jobs/src/smrt-job.ts (SmrtJobCollection.claimReady)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/jobs/src/background-policy.ts
|
|
24
|
+
path: packages/jobs/src/background-policy.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/jobs/src/background-policy.ts (MAX_JOB_RETRIES, DEFAULT_TENANT_JOB_CAP, clampRetries)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/jobs/src/stale-recovery.ts
|
|
29
|
+
path: packages/jobs/src/stale-recovery.ts
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/jobs/src/stale-recovery.ts (DEFAULT_LEASE_TTL_MS, getEffectiveLeaseTtlMs)"
|
|
32
|
+
- id: s4
|
|
33
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/jobs/src/object-extension.ts
|
|
34
|
+
path: packages/jobs/src/object-extension.ts
|
|
35
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
36
|
+
title: "packages/jobs/src/object-extension.ts (withBackgroundJobs, BackgroundCapable, BgOptions)"
|
|
37
|
+
- id: s5
|
|
38
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/jobs/src/index.ts
|
|
39
|
+
path: packages/jobs/src/index.ts
|
|
40
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
41
|
+
title: "packages/jobs/src/index.ts (public exports)"
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Background work and retries
|
|
45
|
+
|
|
46
|
+
# Entry point
|
|
47
|
+
|
|
48
|
+
Import from `@happyvertical/smrt-jobs`. `withBackgroundJobs(Class)` adds `bg(method, args, { delay, retries, priority, queue, timeout })` and a fluent `background(method, args).delay('5m').retries(5).priority('high').enqueue()` to a SmrtObject class; both return a `JobHandle` and persist a `SmrtJob` row. Workers are a `TaskRunner` (`createTaskRunner()`); job progress and events go through `JobContext` and `SmrtJobEvent`. Mark which methods a persisted job may invoke with `backgroundEligible`/`markBackgroundEligible`.[^s4][^s5]
|
|
49
|
+
|
|
50
|
+
# Why not hand-roll it
|
|
51
|
+
|
|
52
|
+
A quick queue over a table usually claims work with a SELECT followed by an UPDATE, which lets two workers take the same row. `SmrtJobCollection.claimReady()` in `smrt-job.ts` claims in one conditional UPDATE so concurrent workers cannot receive the same pending row and never claim more than `limit` rows; on PostgreSQL it selects candidates in a `MATERIALIZED` CTE because the earlier `IN (SELECT ... FOR UPDATE SKIP LOCKED)` form let a single claim take the whole backlog (#3145).[^s1] `background-policy.ts` clamps a requested retry count to `MAX_JOB_RETRIES` (25) so a poison job cannot pin a worker, and `assertWithinTenantCreationCap` bounds a tenant to `DEFAULT_TENANT_JOB_CAP` (10,000) non-terminal jobs so one tenant cannot starve the shared pool.[^s2] A crashed worker's `running` jobs are recovered by worker liveness leases (`DEFAULT_LEASE_TTL_MS` 30 s, never fewer than three renewal ticks), not by per-job heartbeats.[^s3]
|
|
53
|
+
|
|
54
|
+
# Component pages
|
|
55
|
+
|
|
56
|
+
- [smrt-jobs](/components/smrt-jobs.md)
|
|
57
|
+
- [smrt-core](/components/smrt-core.md)
|
|
58
|
+
|
|
59
|
+
# Detection signals
|
|
60
|
+
|
|
61
|
+
- `import:bullmq`
|
|
62
|
+
- `import:bull`
|
|
63
|
+
- `import:bee-queue`
|
|
64
|
+
- `import:pg-boss`
|
|
65
|
+
- `import:kue`
|
|
66
|
+
- `regex:FOR UPDATE SKIP LOCKED`
|
|
67
|
+
|
|
68
|
+
[^s1]: `packages/jobs/src/smrt-job.ts` at `478ae36900`: SmrtJobCollection.claimReady.
|
|
69
|
+
[^s2]: `packages/jobs/src/background-policy.ts` at `478ae36900`: MAX_JOB_RETRIES, DEFAULT_TENANT_JOB_CAP, clampRetries.
|
|
70
|
+
[^s3]: `packages/jobs/src/stale-recovery.ts` at `478ae36900`: DEFAULT_LEASE_TTL_MS, getEffectiveLeaseTtlMs.
|
|
71
|
+
[^s4]: `packages/jobs/src/object-extension.ts` at `478ae36900`: withBackgroundJobs, BackgroundCapable, BgOptions.
|
|
72
|
+
[^s5]: `packages/jobs/src/index.ts` at `478ae36900`: public exports.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "Feature flags and entitlements"
|
|
4
|
+
description: "Reach for smrt-features for tenant-aware flags and smrt-subscriptions for plan entitlements and usage thresholds."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/features/src/feature-resolver.ts
|
|
19
|
+
path: packages/features/src/feature-resolver.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/features/src/feature-resolver.ts (FeatureResolver.isEnabled, resolveBaseState)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/subscriptions/src/services/subscription-resolver.ts
|
|
24
|
+
path: packages/subscriptions/src/services/subscription-resolver.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/subscriptions/src/services/subscription-resolver.ts (SubscriptionResolver)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/subscriptions/src/index.ts
|
|
29
|
+
path: packages/subscriptions/src/index.ts
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/subscriptions/src/index.ts (public exports)"
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Feature flags and entitlements
|
|
35
|
+
|
|
36
|
+
# Entry point
|
|
37
|
+
|
|
38
|
+
Flags: import `FeatureResolver` from `@happyvertical/smrt-features` and call `isEnabled(featureKey, { tenantId })` or `isEnabledFor(Class, localId, ctx)`. Defaults are declared in code and synced with `FeatureSyncService`; changes are written through `FeatureOverrideService` as global or tenant `FeatureOverride` rows. Entitlements: `SubscriptionResolver` (resolves a subscriber's plan terms and thresholds into an `EntitlementResolution`) and `TenantUsageMeter` (`record()`/`summarize()`) from `@happyvertical/smrt-subscriptions`.[^s3]
|
|
39
|
+
|
|
40
|
+
# Why not hand-roll it
|
|
41
|
+
|
|
42
|
+
`FeatureResolver.isEnabled()` in `feature-resolver.ts` resolves the registry default, then the global override, then walks the tenant's ancestor chain applying each tenant override, inheriting down the chain only where the parent has `cascadePermissions` and the child `inheritPermissions`, and it throws for a key that is neither registered nor synced instead of returning false.[^s1] It also strips host list bounds from its reads, because a configured default list limit would drop an ancestor override and silently change the resolved value (#3056).[^s1] `SubscriptionResolver` resolves terms from the plan version frozen on the subscription, fails closed when a pinned version cannot be found, and rejects a plan whose tenant does not match the subscription.[^s2] An environment variable or a column on the tenant row has no inheritance, audit or pinned-version semantics.
|
|
43
|
+
|
|
44
|
+
# Component pages
|
|
45
|
+
|
|
46
|
+
- [smrt-features](/components/smrt-features.md)
|
|
47
|
+
- [smrt-subscriptions](/components/smrt-subscriptions.md)
|
|
48
|
+
|
|
49
|
+
# Detection signals
|
|
50
|
+
|
|
51
|
+
- `import:launchdarkly-node-server-sdk`
|
|
52
|
+
- `import:unleash-client`
|
|
53
|
+
- `import:flagsmith-nodejs`
|
|
54
|
+
- `import:@growthbook/growthbook`
|
|
55
|
+
- `regex:FEATURE_[A-Z_]+`
|
|
56
|
+
- `regex:plan\s*===`
|
|
57
|
+
|
|
58
|
+
[^s1]: `packages/features/src/feature-resolver.ts` at `478ae36900`: FeatureResolver.isEnabled, resolveBaseState.
|
|
59
|
+
[^s2]: `packages/subscriptions/src/services/subscription-resolver.ts` at `478ae36900`: SubscriptionResolver.
|
|
60
|
+
[^s3]: `packages/subscriptions/src/index.ts` at `478ae36900`: public exports.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "File and asset upload and storage"
|
|
4
|
+
description: "Reach for smrt-assets AssetStore and the serving contract before writing an upload handler or file-serving route."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/assets/src/asset-serving.ts
|
|
19
|
+
path: packages/assets/src/asset-serving.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/assets/src/asset-serving.ts (resolveAssetForServing, AssetServeError, remoteMode)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/assets/src/owned-asset-helpers.ts
|
|
24
|
+
path: packages/assets/src/owned-asset-helpers.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/assets/src/owned-asset-helpers.ts (assertAssetLinkable)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/assets/src/asset-store.ts
|
|
29
|
+
path: packages/assets/src/asset-store.ts
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/assets/src/asset-store.ts (AssetStore.store)"
|
|
32
|
+
- id: s4
|
|
33
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/assets-local/src/index.ts
|
|
34
|
+
path: packages/assets-local/src/index.ts
|
|
35
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
36
|
+
title: "packages/assets-local/src/index.ts (createLocalAssetProcessor)"
|
|
37
|
+
- id: s5
|
|
38
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/assets-ergot/src/index.ts
|
|
39
|
+
path: packages/assets-ergot/src/index.ts
|
|
40
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
41
|
+
title: "packages/assets-ergot/src/index.ts (createErgotAssetProcessor)"
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# File and asset upload and storage
|
|
45
|
+
|
|
46
|
+
# Entry point
|
|
47
|
+
|
|
48
|
+
Import from `@happyvertical/smrt-assets`. `AssetStore` (`store()`, `read()`, `remove()`, `storeVersion()`) writes bytes through the @happyvertical/files provider layer (loaded lazily, so the storage backend is a configuration choice) and creates the matching `Asset` row. `createAssetRuntime()` bundles the store and collection; `serveAsset()` and `resolveAssetForServing()` are the serving contract; `addOwnedAssetFromCollection()` attaches assets to an owner through noun-specific joins. Correction to the expected home: `smrt-assets-local` and `smrt-assets-ergot` are not storage backends. They are `AssetCapabilityProvider` processors: `createLocalAssetProcessor()` (sharp image variants and metadata) and `createErgotAssetProcessor()` (Ergot workflow and external-source sync).[^s3][^s4][^s5]
|
|
49
|
+
|
|
50
|
+
# Why not hand-roll it
|
|
51
|
+
|
|
52
|
+
`resolveAssetForServing()` in `asset-serving.ts` returns 403 when an asset's `tenantId` is set and differs from the caller's, runs the host's `canAccess` hook, and defaults `remoteMode` to `'error'` because `sourceUri` is attacker-settable and proxying it is an SSRF vector (the proxy mode requires a public-IP host, re-validates redirects and caps bytes) (S5 #1396).[^s1] `assertAssetLinkable()` in `owned-asset-helpers.ts` refuses to associate an asset with an owner of another tenant, and refuses a tenant's asset on a shared owner.[^s2] A hand-rolled upload route typically serves `sourceUri` or a path directly and checks neither.
|
|
53
|
+
|
|
54
|
+
# Component pages
|
|
55
|
+
|
|
56
|
+
- [smrt-assets](/components/smrt-assets.md)
|
|
57
|
+
- [smrt-assets-local](/components/smrt-assets-local.md)
|
|
58
|
+
- [smrt-assets-ergot](/components/smrt-assets-ergot.md)
|
|
59
|
+
|
|
60
|
+
# Detection signals
|
|
61
|
+
|
|
62
|
+
- `import:multer`
|
|
63
|
+
- `import:busboy`
|
|
64
|
+
- `import:formidable`
|
|
65
|
+
- `import:@aws-sdk/client-s3`
|
|
66
|
+
|
|
67
|
+
[^s1]: `packages/assets/src/asset-serving.ts` at `478ae36900`: resolveAssetForServing, AssetServeError, remoteMode.
|
|
68
|
+
[^s2]: `packages/assets/src/owned-asset-helpers.ts` at `478ae36900`: assertAssetLinkable.
|
|
69
|
+
[^s3]: `packages/assets/src/asset-store.ts` at `478ae36900`: AssetStore.store.
|
|
70
|
+
[^s4]: `packages/assets-local/src/index.ts` at `478ae36900`: createLocalAssetProcessor.
|
|
71
|
+
[^s5]: `packages/assets-ergot/src/index.ts` at `478ae36900`: createErgotAssetProcessor.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "Human approval steps"
|
|
4
|
+
description: "Reach for smrt-approvals ApprovalService before writing an approved flag, a status column or an approval table."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/approvals/src/service.ts
|
|
19
|
+
path: packages/approvals/src/service.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/approvals/src/service.ts (ApprovalService.decide, requestApproval, consume)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/approvals/src/kinds.ts
|
|
24
|
+
path: packages/approvals/src/kinds.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/approvals/src/kinds.ts (defineApprovalKind, assertPolicyTightens)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/approvals/AGENTS.md
|
|
29
|
+
path: packages/approvals/AGENTS.md
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/approvals/AGENTS.md (invariants)"
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Human approval steps
|
|
35
|
+
|
|
36
|
+
# Entry point
|
|
37
|
+
|
|
38
|
+
Import from `@happyvertical/smrt-approvals`. Register a kind with `defineApprovalKind({ ... })` (default quorum, TTL, required permissions, optional self-approval permission), construct an `ApprovalService`, and call `requestApproval(principal, input)` when an agent or service needs a human gate, `decide(principal, requestId, { decision, reason })` for the human outcome, and `consume()` to spend an approved request exactly where the gated action happens. Tenants may tighten a kind through `ApprovalPolicy`. This is a decision gate and ledger, not a workflow engine.
|
|
39
|
+
|
|
40
|
+
# Why not hand-roll it
|
|
41
|
+
|
|
42
|
+
`ApprovalService.decide()` in `service.ts` refuses any principal whose type is not `human` (`not_human`), refuses the requester unless the kind grants a self-approval permission (`self_approval`), checks every permission the request snapshot requires, refuses a non-pending or expired request, and allows one decision per actor.[^s1] Per the package invariants every transition is one guarded UPDATE on tenant, status, version and expiry plus an append-only `ApprovalEvent` in one transaction, so two concurrent approvers cannot both win, and `assertPolicyTightens()` rejects a policy that lowers quorum or lengthens expiry.[^s2][^s3] An `approved` boolean cannot tell who approved, whether they were the requester, or whether the approval was already used.
|
|
43
|
+
|
|
44
|
+
# Component pages
|
|
45
|
+
|
|
46
|
+
- [smrt-approvals](/components/smrt-approvals.md)
|
|
47
|
+
|
|
48
|
+
# Detection signals
|
|
49
|
+
|
|
50
|
+
- `regex:approved_by`
|
|
51
|
+
- `regex:status\s*=\s*['"]approved['"]`
|
|
52
|
+
- `regex:require_?[aA]pproval`
|
|
53
|
+
|
|
54
|
+
[^s1]: `packages/approvals/src/service.ts` at `478ae36900`: ApprovalService.decide, requestApproval, consume.
|
|
55
|
+
[^s2]: `packages/approvals/src/kinds.ts` at `478ae36900`: defineApprovalKind, assertPolicyTightens.
|
|
56
|
+
[^s3]: `packages/approvals/AGENTS.md` at `478ae36900`: invariants.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Index
|
|
3
|
+
title: "Developer-intent index"
|
|
4
|
+
description: "What to reach for in smrt before hand-rolling infrastructure, keyed by what you are about to write."
|
|
5
|
+
status: draft
|
|
6
|
+
generated:
|
|
7
|
+
by: claude/smrt-4687
|
|
8
|
+
at: 2026-11-05T00:00:00Z
|
|
9
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
10
|
+
audience: internal
|
|
11
|
+
evidence_level: source-inspected
|
|
12
|
+
package_version: 0.60.8
|
|
13
|
+
sources:
|
|
14
|
+
- id: s1
|
|
15
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/AGENTS.md
|
|
16
|
+
path: AGENTS.md
|
|
17
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
18
|
+
title: "AGENTS.md"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Developer-intent index
|
|
22
|
+
|
|
23
|
+
Read this before writing queues, schedulers, mailers, uploaders, tenant filters, money fields, flags, audit tables, secrets handling, approvals, webhooks, search, auth or prompts. Each page names the smrt package and entry point, one verified reason not to hand-roll it, and the generated component page. The last column lists import, call and regex signals that suggest a hand-rolled equivalent.
|
|
24
|
+
|
|
25
|
+
| Intent | Page | Home package(s) | Entry point | Hand-rolled signals |
|
|
26
|
+
| --- | --- | --- | --- | --- |
|
|
27
|
+
| Background work and retries | [background-work](/intents/background-work.md) | [smrt-jobs](/components/smrt-jobs.md), [smrt-core](/components/smrt-core.md) | `withBackgroundJobs` (`bg()`/`background()`), `SmrtJob`, `TaskRunner` | `import:bullmq`; `import:bull`; `import:bee-queue`; `import:pg-boss`; `import:kue`; `regex:FOR UPDATE SKIP LOCKED` |
|
|
28
|
+
| Scheduled and recurring runs | [scheduled-runs](/intents/scheduled-runs.md) | [smrt-jobs](/components/smrt-jobs.md), [smrt-agents](/components/smrt-agents.md) | `AgentSchedule` + `createScheduleRunner()`/`ScheduleRunner` | `import:node-cron`; `import:cron`; `import:node-schedule`; `import:agenda`; `import:croner`; `call:setInterval`; `regex:cron\.schedule\(` |
|
|
29
|
+
| Outbound email, SMS, chat messages | [outbound-messages](/intents/outbound-messages.md) | [smrt-messages](/components/smrt-messages.md), [smrt-tenancy](/components/smrt-tenancy.md) | `PersonaMessagingService.send()`, `EmailSender`/`SlackSender`/`TelegramSender`/`ZulipSender`, `UserNotificationService` | `import:nodemailer`; `import:@sendgrid/mail`; `import:twilio`; `import:@slack/web-api`; `import:resend`; `import:postmark`; `import:mailgun.js` |
|
|
30
|
+
| File and asset upload, storage backends | [file-and-asset-storage](/intents/file-and-asset-storage.md) | [smrt-assets](/components/smrt-assets.md), [smrt-assets-local](/components/smrt-assets-local.md), [smrt-assets-ergot](/components/smrt-assets-ergot.md) | `AssetStore`, `createAssetRuntime()`, `serveAsset()`/`resolveAssetForServing()` | `import:multer`; `import:busboy`; `import:formidable`; `import:@aws-sdk/client-s3` |
|
|
31
|
+
| Tenant scoping and cross-tenant reads | [tenant-scoping](/intents/tenant-scoping.md) | [smrt-tenancy](/components/smrt-tenancy.md), [smrt-core](/components/smrt-core.md) | `@TenantScoped` + `@tenantId`, `withTenant()`, `enableTenancy()`, `allowCrossTenant` | `regex:WHERE\s+tenant_id`; `regex:tenant_?[iI]d\s*=\s*\?`; `regex:tenantId\s*[!=]==` |
|
|
32
|
+
| Money, rates, confidence fields | [money-and-rates](/intents/money-and-rates.md) | [smrt-core](/components/smrt-core.md), [smrt-subscriptions](/components/smrt-subscriptions.md) | `amount = 0` (integer minor units), `rate = 0.0` (decimal), `@field({ ui: { format } })` | `regex:\.toFixed\(2\)`; `regex:price\w*\s*=\s*0\.0`; `regex:DECIMAL\(\d+,\s*2\)`; `decimal-field:*_cents`; `decimal-field:*_amount` |
|
|
33
|
+
| Feature flags and entitlements | [feature-flags-and-entitlements](/intents/feature-flags-and-entitlements.md) | [smrt-features](/components/smrt-features.md), [smrt-subscriptions](/components/smrt-subscriptions.md) | `FeatureResolver.isEnabled()`, `SubscriptionResolver`, `TenantUsageMeter` | `import:launchdarkly-node-server-sdk`; `import:unleash-client`; `import:flagsmith-nodejs`; `import:@growthbook/growthbook`; `regex:FEATURE_[A-Z_]+`; `regex:plan\s*===` |
|
|
34
|
+
| Audit trail and change feed | [audit-trail](/intents/audit-trail.md) | [smrt-core](/components/smrt-core.md), [smrt-dev-mcp](/components/smrt-dev-mcp.md) | `@smrt({ audit: true })` + `withAuditContext()`, `getChangesSince()`, `recent-changes` tool | `regex:audit_?log`; `regex:audit_?trail`; `regex:CREATE TABLE\s+\w*audit` |
|
|
35
|
+
| Secrets and config | [secrets-and-config](/intents/secrets-and-config.md) | [smrt-secrets](/components/smrt-secrets.md), [smrt-config](/components/smrt-config.md) | `SecretService.create()` + `store()`, `loadConfig()`, `getModuleConfig()`, `defineConfig()` | `import:dotenv`; `import:node-vault`; `import:cosmiconfig`; `import:convict`; `regex:process\.env\.\w*SECRET`; `regex:createCipheriv\(` |
|
|
36
|
+
| Human approval steps | [human-approvals](/intents/human-approvals.md) | [smrt-approvals](/components/smrt-approvals.md) | `ApprovalService` (`requestApproval()`, `decide()`, `consume()`), `defineApprovalKind()` | `regex:approved_by`; `regex:status\s*=\s*['"]approved['"]`; `regex:require_?[aA]pproval` |
|
|
37
|
+
| Inbound and outbound webhooks | [webhooks-and-events](/intents/webhooks-and-events.md) | [smrt-webhooks](/components/smrt-webhooks.md), [smrt-core](/components/smrt-core.md), [smrt-jobs](/components/smrt-jobs.md), [smrt-events](/components/smrt-events.md) | `WebhookDispatcher`, `WebhookAdminService`, `registerWebhookRuntime()`; core `createDispatchBus()` | `import:svix`; `regex:createHmac\(`; `regex:x-[a-z-]*signature` |
|
|
38
|
+
| Search, facts, extracted knowledge | [search-facts-knowledge](/intents/search-facts-knowledge.md) | [smrt-facts](/components/smrt-facts.md), [smrt-content](/components/smrt-content.md), [smrt-ingestion](/components/smrt-ingestion.md), [smrt-core](/components/smrt-core.md) | `FactCollection.reconcile()`, `collection.semanticSearch()`, smrt-ingestion models and `/server` services | `import:@pinecone-database/pinecone`; `import:pgvector`; `import:minisearch`; `import:flexsearch`; `import:lunr`; `regex:cosine_?[sS]imilarity` |
|
|
39
|
+
| Auth, users, sessions | [auth-and-users](/intents/auth-and-users.md) | [smrt-users](/components/smrt-users.md), [smrt-tenancy](/components/smrt-tenancy.md) | `SessionService`, `PasswordCredentialService`, `PermissionResolver`, `LoginAttemptLimiter`, `/sveltekit` handlers | `import:bcrypt`; `import:bcryptjs`; `import:argon2`; `import:jsonwebtoken`; `import:passport`; `import:express-session`; `import:lucia` |
|
|
40
|
+
| Prompts and AI operations | [prompts-and-ai-operations](/intents/prompts-and-ai-operations.md) | [smrt-prompts](/components/smrt-prompts.md), [smrt-core](/components/smrt-core.md), [smrt-agents](/components/smrt-agents.md) | `definePrompt()` + `resolvePrompt()`, `PromptOverrideService`, `@smrt({ ai: { callable } })`, `SmrtObject.is()/do()/describe()` | `import:openai`; `import:@anthropic-ai/sdk`; `import:langchain`; `regex:You are an? [a-z ]+`; `regex:role:\s*['"]system['"]` |
|
|
41
|
+
| Polymorphic links, junctions, trees | [relationships-and-trees](/intents/relationships-and-trees.md) | [smrt-core](/components/smrt-core.md) | `SmrtJunction`/`SmrtJunctionBase`, `SmrtHierarchical`, `SmrtPolymorphicAssociation` | `regex:parent_?[iI]d\s*[:=]`; `regex:WITH RECURSIVE`; `regex:entity_?type` |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: "Money, rates and confidence fields"
|
|
4
|
+
description: "Declare money as integer minor units and rates as decimals through the core numeric-default rule instead of floats or custom decimal columns."
|
|
5
|
+
tags:
|
|
6
|
+
- smrt
|
|
7
|
+
- intent
|
|
8
|
+
status: draft
|
|
9
|
+
generated:
|
|
10
|
+
by: claude/smrt-4687
|
|
11
|
+
at: 2026-11-05T00:00:00Z
|
|
12
|
+
stale_after: 2027-02-03T00:00:00Z
|
|
13
|
+
audience: internal
|
|
14
|
+
evidence_level: source-inspected
|
|
15
|
+
package_version: 0.60.8
|
|
16
|
+
sources:
|
|
17
|
+
- id: s1
|
|
18
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/core/src/ui-metadata.ts
|
|
19
|
+
path: packages/core/src/ui-metadata.ts
|
|
20
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
21
|
+
title: "packages/core/src/ui-metadata.ts (FIELD_UI_FORMAT_TYPES)"
|
|
22
|
+
- id: s2
|
|
23
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/core/src/scanner/manifest-generator.ts
|
|
24
|
+
path: packages/core/src/scanner/manifest-generator.ts
|
|
25
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
26
|
+
title: "packages/core/src/scanner/manifest-generator.ts (ui.format validation against field type)"
|
|
27
|
+
- id: s3
|
|
28
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/core/src/migrations/minor-units.ts
|
|
29
|
+
path: packages/core/src/migrations/minor-units.ts
|
|
30
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
31
|
+
title: "packages/core/src/migrations/minor-units.ts (preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits)"
|
|
32
|
+
- id: s4
|
|
33
|
+
resource: https://github.com/happyvertical/smrt/blob/478ae3690052b825b79b28235369ee7a5f6d2f20/packages/subscriptions/src/index.ts
|
|
34
|
+
path: packages/subscriptions/src/index.ts
|
|
35
|
+
source_revision: 478ae3690052b825b79b28235369ee7a5f6d2f20
|
|
36
|
+
title: "packages/subscriptions/src/index.ts (migrateSubscriptionsMoneyToMinorUnits)"
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Money, rates and confidence fields
|
|
40
|
+
|
|
41
|
+
# Entry point
|
|
42
|
+
|
|
43
|
+
The rule is in the field default: `price = 0` is an integer column holding minor units (`1999` for $19.99), `rate = 0.0` is a decimal for rates and confidence. Mark presentation with `@field({ ui: { format: 'money' } })` (integer only) or `'percent'` (decimal or integer); a currency code is a separate text field. To convert a legacy floating-point money column, use `preflightMinorUnitsRescale()` then `rescaleMoneyColumnsToMinorUnits()` from `@happyvertical/smrt-core/migrations` (per-package wrappers such as `migrateSubscriptionsMoneyToMinorUnits` exist).[^s4]
|
|
44
|
+
|
|
45
|
+
# Why not hand-roll it
|
|
46
|
+
|
|
47
|
+
`FIELD_UI_FORMAT_TYPES` in `ui-metadata.ts` lists the only field type each presentation format may sit on (`money: ['integer']`), and the manifest generator fails the build when a `money` format is put on a non-integer field.[^s1][^s2] Converting an existing float column is deliberately not an automatic type change, because `ALTER ... TYPE BIGINT` would truncate `19.99` to `19`; `minor-units.ts` instead provides a read-only preflight that lists rows that would lose information (half-cents, values beyond the JavaScript safe-integer range once scaled) and a rescale guarded by a `BackfillTracker` marker so a re-run can never multiply a table by 100 twice.[^s3] Float money fields and `toFixed(2)` rounding lose exactly these cases silently.
|
|
48
|
+
|
|
49
|
+
# Component pages
|
|
50
|
+
|
|
51
|
+
- [smrt-core](/components/smrt-core.md)
|
|
52
|
+
- [smrt-subscriptions](/components/smrt-subscriptions.md)
|
|
53
|
+
|
|
54
|
+
# Detection signals
|
|
55
|
+
|
|
56
|
+
- `regex:\.toFixed\(2\)`
|
|
57
|
+
- `regex:price\w*\s*=\s*0\.0`
|
|
58
|
+
- `regex:DECIMAL\(\d+,\s*2\)`
|
|
59
|
+
- `decimal-field:*_cents`
|
|
60
|
+
- `decimal-field:*_amount`
|
|
61
|
+
|
|
62
|
+
[^s1]: `packages/core/src/ui-metadata.ts` at `478ae36900`: FIELD_UI_FORMAT_TYPES.
|
|
63
|
+
[^s2]: `packages/core/src/scanner/manifest-generator.ts` at `478ae36900`: ui.format validation against field type.
|
|
64
|
+
[^s3]: `packages/core/src/migrations/minor-units.ts` at `478ae36900`: preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits.
|
|
65
|
+
[^s4]: `packages/subscriptions/src/index.ts` at `478ae36900`: migrateSubscriptionsMoneyToMinorUnits.
|