@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.
Files changed (47) hide show
  1. package/AGENTS.md +17 -53
  2. package/agents/db-drop-framework-base-tables.md +49 -0
  3. package/agents/db-migrate-concurrency.md +11 -0
  4. package/dist/{AuditLogCollection-BBbvr8Qc-CoDs0BaX.js → AuditLogCollection-BBbvr8Qc-DU-QXhH5.js} +1 -1
  5. package/dist/{OidcIdentity-COeMidoC-B-cVovG3.js → OidcIdentity-COeMidoC-BDr8b6iW.js} +2 -2
  6. package/dist/{OidcIdentityCollection-D4DIZv0K-CexIL7No.js → OidcIdentityCollection-D4DIZv0K-BvsaDTZY.js} +1 -1
  7. package/dist/{PasswordCredentialService-DHJNKEeJ-DQlR62e-.js → PasswordCredentialService-BSSP_kyT-DlxlWy6b.js} +40 -22
  8. package/dist/{ProfileAssetCollection-D5-tG9Mm-Bmn4-Urc.js → ProfileAssetCollection-D5-tG9Mm-Be7hQJJq.js} +3 -3
  9. package/dist/{ProfileCollection-CmRaiE9U-52qjwbhJ.js → ProfileCollection-CmRaiE9U-n8apgXIh.js} +17 -17
  10. package/dist/{ProfileLinkCollection-pvPTsysg-ocN3hj_M.js → ProfileLinkCollection-pvPTsysg-DCmnAd7m.js} +2 -2
  11. package/dist/{ProfileMetadataCollection-CAgJyjze-FX20yKjY.js → ProfileMetadataCollection-CAgJyjze-BKpcvrnf.js} +1 -1
  12. package/dist/{ProfileMetafieldCollection-19ae6Lpf-qg7eMcJk.js → ProfileMetafieldCollection-19ae6Lpf-BuTJKzmz.js} +1 -1
  13. package/dist/{ProfileRelationshipCollection-BFSlTQ7J-DlDILj3m.js → ProfileRelationshipCollection-BFSlTQ7J-DBbNdauk.js} +1 -1
  14. package/dist/{ProfileTypeCollection-BRLK8cHS-QVwjfqe8.js → ProfileTypeCollection-BRLK8cHS-COCy6yqF.js} +2 -2
  15. package/dist/commands/docs-claude.d.ts +1 -0
  16. package/dist/commands/docs-claude.d.ts.map +1 -1
  17. package/dist/commands/docs-knowledge-bundle.d.ts +87 -0
  18. package/dist/commands/docs-knowledge-bundle.d.ts.map +1 -0
  19. package/dist/{commands-DNssH5Dv.js → commands-CGfM14Uj.js} +265 -6
  20. package/dist/{dist-BJABhykg.js → dist-B00KCyNE.js} +2 -2
  21. package/dist/{dist-D25iWIXX.js → dist-BGEMcSs-.js} +5 -5
  22. package/dist/dist-D7vejSNK.js +1854 -0
  23. package/dist/dist-DEKj_L2B.js +1908 -0
  24. package/dist/index.js +11 -11
  25. package/dist/knowledge/intents/audit-trail.md +63 -0
  26. package/dist/knowledge/intents/auth-and-users.md +67 -0
  27. package/dist/knowledge/intents/background-work.md +72 -0
  28. package/dist/knowledge/intents/feature-flags-and-entitlements.md +60 -0
  29. package/dist/knowledge/intents/file-and-asset-storage.md +71 -0
  30. package/dist/knowledge/intents/human-approvals.md +56 -0
  31. package/dist/knowledge/intents/index.md +41 -0
  32. package/dist/knowledge/intents/money-and-rates.md +65 -0
  33. package/dist/knowledge/intents/outbound-messages.md +67 -0
  34. package/dist/knowledge/intents/prompts-and-ai-operations.md +72 -0
  35. package/dist/knowledge/intents/relationships-and-trees.md +56 -0
  36. package/dist/knowledge/intents/scheduled-runs.md +61 -0
  37. package/dist/knowledge/intents/search-facts-knowledge.md +68 -0
  38. package/dist/knowledge/intents/secrets-and-config.md +66 -0
  39. package/dist/knowledge/intents/tenant-scoping.md +63 -0
  40. package/dist/knowledge/intents/webhooks-and-events.md +71 -0
  41. package/dist/{oidcProvisioningPrimitives-KpIiLmG4-CnRTofeV.js → oidcProvisioningPrimitives-KpIiLmG4-Csnh1ENp.js} +3 -3
  42. package/dist/{operator-fwao3Y1j.js → operator-DCwooIiI.js} +2 -2
  43. package/dist/{resolveIdentity-j2WFuKDT-C9XtigVZ.js → resolveIdentity-j2WFuKDT-DZTwcb-u.js} +14 -14
  44. package/dist/smrt-knowledge.okf.md +57 -0
  45. package/package.json +15 -15
  46. package/dist/dist-B3gpMpBj.js +0 -1908
  47. 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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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-DNssH5Dv.js");
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.