@happyvertical/smrt-core 0.45.1 → 0.45.2
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 +81 -289
- package/agents/build-knowledge.md +92 -0
- package/agents/memory.md +4 -0
- package/agents/object-runtime.md +83 -0
- package/agents/schema-paths.md +186 -476
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +25 -7
- package/dist/generators/mcp.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/knowledge-discovery.d.ts +26 -0
- package/dist/knowledge-discovery.d.ts.map +1 -0
- package/dist/knowledge-discovery.js +54 -0
- package/dist/knowledge-discovery.js.map +1 -0
- package/dist/knowledge.d.ts +1 -0
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +30 -12
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/registry/types.d.ts +11 -6
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +47 -35
- package/package.json +4 -4
package/AGENTS.md
CHANGED
|
@@ -1,299 +1,91 @@
|
|
|
1
1
|
# @happyvertical/smrt-core
|
|
2
2
|
|
|
3
|
-
ORM, code generation, AI integration, and
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`DispatchBus`, `GlobalInterceptors`, and `LearningMemory`; this file documents
|
|
7
|
-
their invariants and source locations, and the module docs below cover the
|
|
8
|
-
per-subsystem semantics.
|
|
3
|
+
Foundation ORM, registry, schema/code generation, AI integration, and DispatchBus.
|
|
4
|
+
Read the module for the subsystem being edited; root AGENTS covers shared model
|
|
5
|
+
and repository rules.
|
|
9
6
|
|
|
10
7
|
## Modules
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
subsystem you are editing. This file keeps what holds across all of them.
|
|
14
|
-
|
|
15
|
-
| Module | Scope | Module doc |
|
|
9
|
+
| Source | Scope | Module doc |
|
|
16
10
|
|---|---|---|
|
|
17
|
-
| `src/
|
|
18
|
-
| `src/
|
|
19
|
-
| `src/
|
|
20
|
-
| `src/
|
|
21
|
-
| `src/data-query.ts` |
|
|
22
|
-
| `src/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
await collection.list({
|
|
63
|
-
where: { status: 'active', 'price >': 10 },
|
|
64
|
-
limit: 50, offset: 0, orderBy: 'created_at DESC'
|
|
65
|
-
});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Projection, latest-related, facets, counts, and bounded read plans are
|
|
69
|
-
documented in [agents/collection-reads.md](agents/collection-reads.md).
|
|
70
|
-
|
|
71
|
-
`list()` and `query()` hydrate model instances serially in result order because
|
|
72
|
-
an `initialize()` hook may query through the same transaction-bound PostgreSQL
|
|
73
|
-
client. Keep this serialization invariant; use `select` when callers need plain
|
|
74
|
-
rows without model hydration.
|
|
75
|
-
|
|
76
|
-
Native DuckDB model hydration casts declared UUID columns to `VARCHAR` in the
|
|
77
|
-
read query because its JavaScript binding otherwise returns lossy HUGEINT
|
|
78
|
-
wrapper objects. Explicit projections apply the same cast for selected UUID
|
|
79
|
-
fields so bounded query envelopes preserve canonical row and relationship ids.
|
|
80
|
-
For STI child columns, raw `query()` SELECTs, and latest-related projections,
|
|
81
|
-
the read path describes the output types without evaluating the query, then
|
|
82
|
-
performs one data-bearing SELECT with UUID result columns cast to `VARCHAR`;
|
|
83
|
-
mutation statements are never reinterpreted or replayed.
|
|
84
|
-
|
|
85
|
-
**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`.
|
|
86
|
-
Arrays auto-detect `IN`. NULL is a value, not an operator: `{ deletedAt: null }`
|
|
87
|
-
renders `IS NULL` and `{ 'deletedAt !=': null }` renders `IS NOT NULL`.
|
|
88
|
-
|
|
89
|
-
This list is the set `@happyvertical/sql`'s `buildWhere` can execute, and
|
|
90
|
-
`convertWhereKeys` accepts nothing outside it — an operator accepted here but
|
|
91
|
-
unknown there fails inside the query builder, after the API said the query was
|
|
92
|
-
valid (#2276). Two entries were removed for that reason and now reject at the
|
|
93
|
-
API boundary: `contains` (never existed in the SQL layer; use `like` with
|
|
94
|
-
explicit wildcards) and dot-notation JSON paths such as `metadata.userId` (never
|
|
95
|
-
rewritten into an extraction expression, so they reached SQL as qualified column
|
|
96
|
-
references). Re-adding either requires the query builder to support it first;
|
|
97
|
-
`src/__tests__/issue-2276-where-contract.test.ts` executes every accepted
|
|
98
|
-
operator against a database to keep the two in step.
|
|
99
|
-
|
|
100
|
-
STI child collections auto-filter by `_meta_type`. Query bounds — `LIMIT 1` on `get()`, the `limit`/`offset` parser, the `orderBy` whitelist and sensitive/permission refusals, and the deterministic generated-list ordering (#2367) — are in [agents/query-bounds.md](agents/query-bounds.md).
|
|
101
|
-
|
|
102
|
-
## Canonical Bounded Data Queries (#2444)
|
|
103
|
-
|
|
104
|
-
The normalizers and fingerprint are the trust boundary for the
|
|
105
|
-
transport-neutral query envelope; full bounds, schema, and output rules live in
|
|
106
|
-
[agents/data-query.md](agents/data-query.md). Adapters own tenant/principal
|
|
107
|
-
access and query execution.
|
|
108
|
-
|
|
109
|
-
## Object Memory & Semantic Search
|
|
110
|
-
|
|
111
|
-
Context memory and semantic search are persistence primitives inherited by
|
|
112
|
-
`SmrtObject`/`SmrtCollection`; their storage, scope, expiry, and tenant
|
|
113
|
-
invariants are in [agents/memory.md](agents/memory.md).
|
|
114
|
-
|
|
115
|
-
## @smrt() Decorator Options
|
|
116
|
-
|
|
117
|
-
Key options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes, #2357 — see "Schema paths"), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).
|
|
118
|
-
|
|
119
|
-
Registration sets `SMRT_TABLE_NAME` static property (survives minification).
|
|
120
|
-
|
|
121
|
-
## @field() UI hints (#2046)
|
|
122
|
-
|
|
123
|
-
`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only
|
|
124
|
-
seed for the field-policy rail (epic #2045). Carried in the manifest under the
|
|
125
|
-
field's `_meta.ui` (never a top-level `FieldDefinition` key), readable at
|
|
126
|
-
runtime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with
|
|
127
|
-
`description` into generated web-collection definitions and browser MCP tool
|
|
128
|
-
schemas. No schema/persistence/security effect — `sensitive`/`readPermission`
|
|
129
|
-
stay the security rail, and `sensitive`/`transient` fields never emit to the
|
|
130
|
-
client at all.
|
|
131
|
-
|
|
132
|
-
## Domain Knowledge Artifacts
|
|
11
|
+
| `src/object.ts`, `src/collection.ts`, `src/child-accessors.ts` | Lifecycle, hydration, operators, STI, child accessors, dispatch | [agents/object-runtime.md](agents/object-runtime.md) |
|
|
12
|
+
| `src/revision-guard.ts` | Guarded writes and PostgreSQL revision precision | [agents/revision-guard.md](agents/revision-guard.md) |
|
|
13
|
+
| `src/collection.ts` | Projections, latest-related, facets, counts, read plans | [agents/collection-reads.md](agents/collection-reads.md) |
|
|
14
|
+
| `src/collection.ts` | Limits, sort whitelist, generated list order | [agents/query-bounds.md](agents/query-bounds.md) |
|
|
15
|
+
| `src/data-query.ts` | Transport-neutral bounded query normalization | [agents/data-query.md](agents/data-query.md) |
|
|
16
|
+
| `src/schema/`, `src/migrations/`, `src/cascade.ts`, `src/system/` | DDL parity, indexes, migrations, delete integrity, retention | [agents/schema-paths.md](agents/schema-paths.md) |
|
|
17
|
+
| `src/change-feed.ts` | Durable changes, cursors, table versions, retention | [agents/change-feed.md](agents/change-feed.md) |
|
|
18
|
+
| `src/change-signals.ts` | Signal bus, replica fan-out, SSE | [agents/change-signals.md](agents/change-signals.md) |
|
|
19
|
+
| `src/generators/`, `src/vite-plugin/web-collections.ts` | REST/CLI/MCP generation, manifest hashes, ETags | [agents/generators.md](agents/generators.md) |
|
|
20
|
+
| `src/vite-plugin/`, `src/consumer-plugin/`, `src/knowledge.ts` | Decorator UI hints, knowledge projection, generation snapshots | [agents/build-knowledge.md](agents/build-knowledge.md) |
|
|
21
|
+
| `src/object.ts`, `src/collection.ts`, `src/learning/memory.ts` | Context memory and semantic search | [agents/memory.md](agents/memory.md) |
|
|
22
|
+
|
|
23
|
+
## Cross-module invariants
|
|
24
|
+
|
|
25
|
+
- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.
|
|
26
|
+
|
|
27
|
+
- Production DDL uses manifest generators; `getTestDatabase()` uses registry
|
|
28
|
+
generators. Keep columns, indexes, FK actions, and runtime conflict targets in
|
|
29
|
+
parity (`src/schema/schema-path-parity.test.ts`). Every new query predicate
|
|
30
|
+
needs its index or an explicit reason none is needed.
|
|
31
|
+
- Tenant scoping covers every read and every unique/conflict key. Explicit
|
|
32
|
+
conflict columns are not rewritten; their author must include tenant scope.
|
|
33
|
+
- Persisted saves use `id` and loaded `updated_at`; new saves use natural keys.
|
|
34
|
+
Preserve revision compare-and-swap ordering through public `save()`,
|
|
35
|
+
`claimRevision()`, and transaction APIs. Embedded saves, deletes, and complete
|
|
36
|
+
`withTransaction()` callbacks share a process-local write queue.
|
|
37
|
+
- Collection model hydration is serial in result order: initialization may query
|
|
38
|
+
the same transaction-bound PostgreSQL client. Use projections for plain rows.
|
|
39
|
+
- Native DuckDB UUIDs must be cast coherently on read before identity reuse;
|
|
40
|
+
custom embedded revision paths use `getCanonicalPersistedRow()`. Never replay
|
|
41
|
+
a mutation to discover result types.
|
|
42
|
+
- `withDatabase(db, callback)` restores only database bindings (including public
|
|
43
|
+
`options.db`); `withTransaction(callback)` also restores identity/revision
|
|
44
|
+
metadata after rollback. Do not use a bound instance concurrently.
|
|
45
|
+
- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;
|
|
46
|
+
call it on a base PostgreSQL connection before caller-owned transactions.
|
|
47
|
+
Bootstrap uses an advisory lock. Application tables still require migrations;
|
|
48
|
+
runtime table verification checks existence only.
|
|
49
|
+
- Manifest generation fails closed on scanner errors, including unresolved
|
|
50
|
+
decorator spreads. Never emit partial, default-open registration.
|
|
51
|
+
- Generated registration repairs bundled class identity using the exact imported
|
|
52
|
+
constructor, explicit package, and isolated one-object manifest. Never infer
|
|
53
|
+
ownership from paths, simple names, or table names; packages can share names.
|
|
54
|
+
Consumer regression gate: `packages/bundle-gate/src/__tests__/registry-identity.spec.ts`.
|
|
133
55
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`
|
|
137
|
-
- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`
|
|
138
|
-
|
|
139
|
-
Keep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic
|
|
140
|
-
agent contract for downstream review and architecture tools.
|
|
141
|
-
|
|
142
|
-
The schema-version-1 object projection is additive and high-signal: it retains
|
|
143
|
-
normalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,
|
|
144
|
-
method signatures, and field defaults/constraints/readonly/transient flags.
|
|
145
|
-
Sensitive fields are removed before both `fields` and `relationships` are
|
|
146
|
-
derived, including legacy flags stored under `_meta`; matching field and
|
|
147
|
-
snake-case column names are also removed from projected conflict columns, and a
|
|
148
|
-
sensitive custom tenant field is omitted while retaining scope and mode.
|
|
149
|
-
Generated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;
|
|
150
|
-
the optional marker keeps schema version 1 additive while letting readers
|
|
151
|
-
identify older artifacts that require raw-manifest corroboration.
|
|
152
|
-
|
|
153
|
-
Config precedence for knowledge is defaults → top-level `knowledge` in
|
|
154
|
-
`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →
|
|
155
|
-
object-level `@smrt({ knowledge })`.
|
|
156
|
-
|
|
157
|
-
Object-level `knowledge: false` excludes an object from authored context only;
|
|
158
|
-
it must not change runtime manifest registration. Use
|
|
159
|
-
`knowledge: { tags, summary, risks }` for review-sensitive domain objects.
|
|
160
|
-
|
|
161
|
-
HTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is
|
|
162
|
-
true, generated SvelteKit routes must stay GET-only and guarded by dev mode or
|
|
163
|
-
admin auth.
|
|
164
|
-
|
|
165
|
-
## DispatchBus
|
|
166
|
-
|
|
167
|
-
- `emit(signalType, payload, metadata)` → creates persistent Dispatch record
|
|
168
|
-
- `on(pattern, handler)` → in-memory handler (immediate)
|
|
169
|
-
- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)
|
|
170
|
-
- `process(subscriberName, handler)` → process pending dispatches
|
|
171
|
-
- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)
|
|
172
|
-
- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`
|
|
173
|
-
- Status: `pending → processing → completed` (or `failed`)
|
|
174
|
-
|
|
175
|
-
## Single Table Inheritance (STI)
|
|
176
|
-
|
|
177
|
-
- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table
|
|
178
|
-
- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)
|
|
179
|
-
- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)
|
|
180
|
-
- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically
|
|
181
|
-
- Validation: fail-fast on save if `_meta_type` missing or mismatched
|
|
182
|
-
|
|
183
|
-
## Child Accessors (R10)
|
|
184
|
-
|
|
185
|
-
`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:
|
|
186
|
-
|
|
187
|
-
- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.
|
|
188
|
-
- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.
|
|
189
|
-
|
|
190
|
-
When the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).
|
|
191
|
-
|
|
192
|
-
## Vite Plugin
|
|
56
|
+
## Gotchas
|
|
193
57
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
58
|
+
- Optional filesystem support stays lazy: use `createFilesystemAdapter()` in
|
|
59
|
+
`src/filesystem-loader.ts`, not a static files-SDK import. Fully bundled apps
|
|
60
|
+
import `@happyvertical/smrt-core/filesystem` at startup. Use
|
|
61
|
+
`importOptionalDependency()` for similarly heavy optional dependencies.
|
|
62
|
+
- Database retries are transient-only, four attempts total for `get`/`upsert`.
|
|
63
|
+
Use `src/db-errors.ts` classifiers through the cause chain, never message
|
|
64
|
+
matching (SDK driver text can live in `context.originalError`). Constraints,
|
|
65
|
+
bad input, missing tables, and aborted PostgreSQL transactions fail immediately.
|
|
66
|
+
Unique/PK violations become `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL becomes
|
|
67
|
+
`VALIDATION_REQUIRED_FIELD`; other failures keep the driver error as `cause`.
|
|
68
|
+
- Property initializers precede option values; options win. Arrays/objects are
|
|
69
|
+
shallow-cloned. Collection creation caches fields; table verification caches
|
|
70
|
+
by DB URL and table. Preserve these scopes when changing initialization.
|
|
71
|
+
- The Vite plugin loads scanner/schema code from `dist/` when present. Rebuild
|
|
72
|
+
core after editing those sources before testing consumer manifest generation.
|
|
73
|
+
Vite 8 requires `oxc.decorator: { legacy: true, emitDecoratorMetadata: true }`.
|
|
74
|
+
|
|
75
|
+
## Validation
|
|
76
|
+
|
|
77
|
+
Run focused tests first, then applicable package checks:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pnpm --filter @happyvertical/smrt-core test
|
|
81
|
+
pnpm --filter @happyvertical/smrt-core typecheck
|
|
82
|
+
pnpm --filter @happyvertical/smrt-core build
|
|
83
|
+
pnpm --filter @happyvertical/smrt-core test:postgres
|
|
84
|
+
pnpm check:agents-chain
|
|
85
|
+
pnpm smrt dev:knowledge-check
|
|
204
86
|
```
|
|
205
87
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
decorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need
|
|
211
|
-
the legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,
|
|
212
|
-
emitDecoratorMetadata: true`.
|
|
213
|
-
|
|
214
|
-
For independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept
|
|
215
|
-
the same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The
|
|
216
|
-
schema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the
|
|
217
|
-
merged project/dependency manifest, portable source paths, and source-file
|
|
218
|
-
digests; each plugin selects its own view. Reuse mode fails closed on
|
|
219
|
-
byte/provenance/path/content drift, skips scans and manifest writes, and still
|
|
220
|
-
generates routes, types, registration, and virtual modules. Omit it for normal
|
|
221
|
-
local development and watch mode.
|
|
222
|
-
|
|
223
|
-
## Schema paths (#2382)
|
|
224
|
-
|
|
225
|
-
`ensureSystemTables(db, typeHint?)` is the public, idempotent provisioning
|
|
226
|
-
boundary for framework-owned `_smrt_*` tables. Call it on a base PostgreSQL
|
|
227
|
-
connection before opening a caller-owned transaction; its advisory-locked
|
|
228
|
-
bootstrap prevents missing-table probes from poisoning that transaction.
|
|
229
|
-
|
|
230
|
-
Production DDL comes from the **manifest** paths
|
|
231
|
-
(`generateSTISchemaFromManifest`/`generateCTISchemaFromManifest`, selected in
|
|
232
|
-
`src/scanner/manifest-generator.ts` → registered `schema` → `db:migrate`). The
|
|
233
|
-
**registry** paths feed `getTestDatabase()`. Manifest and registry schemas must
|
|
234
|
-
agree on same-package foreign keys as well as columns and indexes:
|
|
235
|
-
`@foreignKey` emits a named physical constraint, while `@crossPackageRef` and
|
|
236
|
-
`@tenantId` remain indexed runtime relationships without physical constraints.
|
|
237
|
-
Natural-key references default to `CASCADE`; ordinary references default to
|
|
238
|
-
immediate `NO ACTION`, matching `SmrtObject.delete()`.
|
|
239
|
-
|
|
240
|
-
Same-package archival/audit identifiers that intentionally outlive their
|
|
241
|
-
parent may use `@foreignKey(Target, { constraint: false })`. This explicit
|
|
242
|
-
exception retains relationship loading, indexing, and application-side delete
|
|
243
|
-
metadata while omitting the physical constraint, schema dependency, and
|
|
244
|
-
app-side cascade/preflight action so the stored identifier survives deletion;
|
|
245
|
-
document the retention reason at the field, and keep ordinary same-package
|
|
246
|
-
relationships constrained.
|
|
247
|
-
|
|
248
|
-
When a relationship is valid on every engine but a particular database cannot
|
|
249
|
-
faithfully enforce its physical shape, use the public, explicit allowlist
|
|
250
|
-
`@foreignKey(Target, { constraint: { engines: ['postgres', 'sqlite'] } })`.
|
|
251
|
-
Only physical DDL and schema dependency planning are engine-scoped; native UUID
|
|
252
|
-
storage, relationship loading, indexes, and application-side delete enforcement
|
|
253
|
-
remain active on every engine. Empty or unknown allowlists fail closed. Do not
|
|
254
|
-
use this option to hide an otherwise invalid schema.
|
|
255
|
-
|
|
256
|
-
- Change column/index emission on every shipping path, proven by the path-parity
|
|
257
|
-
test `src/schema/schema-path-parity.test.ts` (#2359; index rules in the module doc). A "same as migrations" comment is a claim to check.
|
|
258
|
-
- Every new query predicate ships with its index, or a reason it doesn't.
|
|
259
|
-
- Creation is dependency-planned on every entry point. PostgreSQL defers mutual
|
|
260
|
-
cycle constraints until both tables exist; SQLite keeps cycles inline;
|
|
261
|
-
DuckDB refuses unsupported cycles/actions unless the field has an explicit
|
|
262
|
-
physical-constraint engine allowlist rather than silently omitting them.
|
|
263
|
-
In particular, generated same-package constraints retain the compatibility
|
|
264
|
-
default `ON UPDATE CASCADE`; DuckDB/JSON cannot enforce that action and must
|
|
265
|
-
return an actionable refusal instead of stripping the clause.
|
|
266
|
-
PostgreSQL deferred adds are idempotent and probe the exact child/parent
|
|
267
|
-
columns for orphans before `NOT VALID` + validation. Rollback drops children
|
|
268
|
-
before parents, removes deferred PostgreSQL cycle constraints first, and
|
|
269
|
-
defers SQLite checks while dropping populated cycles. Schema aggregation that
|
|
270
|
-
deliberately filters a parent also removes the retained child's physical FK.
|
|
271
|
-
- Numeric types, uuid casts, conflict targets, timestamps, migrations: run the
|
|
272
|
-
`test:postgres` lane — SQLite affinity accepts what PostgreSQL rejects.
|
|
273
|
-
- Read `dist/manifest.json`/regenerated schemas for what a decorator produced;
|
|
274
|
-
count across all packages instead of sampling.
|
|
275
|
-
- Tenant scoping is whole-path: every unique constraint and conflict target on a
|
|
276
|
-
tenant-scoped table carries the tenant column, and every read path — not only
|
|
277
|
-
`list()` — is interceptor-aware.
|
|
278
|
-
- Rolling indexes out is part of the change: a bulk `CREATE INDEX` batch needs
|
|
279
|
-
the bounded, concurrent migrate path (#2362, Gotchas), or it takes production
|
|
280
|
-
down on deploy.
|
|
281
|
-
|
|
282
|
-
## Gotchas
|
|
283
|
-
|
|
284
|
-
- **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.
|
|
285
|
-
- **Transaction-bound instances**: `SmrtClass.withDatabase(db, callback)` temporarily binds an initialized instance (including its public `options.db`) to a caller-owned transaction database and restores only the database binding. Transaction owners persisting one object should use `SmrtObject.withTransaction(callback)`, which restores identity/revision metadata after rollback and serializes its embedded callback with ordinary writes. Bound saves re-enter that hold. Never reach into `_db`, and do not use the same instance concurrently during either callback.
|
|
286
|
-
- **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`
|
|
287
|
-
- **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)
|
|
288
|
-
- **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime verification is `tableExists()` only (`src/schema/table-verifier.ts`) — no column, type, or index check
|
|
289
|
-
- **PostgreSQL migrate batches are always time-bounded (#2362)**: `MigrationTracker.applyAll({ atomic: true })` emits `SET LOCAL lock_timeout`/`statement_timeout` before any DDL, so a batch blocked on one table cannot hold its earlier locks indefinitely. `postgresSafe: true` adds concurrent-index mode — non-index DDL commits atomically, then index DDL runs `CONCURRENTLY` on a session pinned via `db.acquireSession()` (a pooled `db.query` would not keep the `SET` and the DDL on one connection). That mode is deliberately **not atomic**: unfinished index migrations are recorded `failed`, not `running`, and their `error_message` carries a `[smrt: concurrent-index phase 1 committed]` marker so a reconciling re-run resumes at the index build instead of replaying committed DDL. INVALID indexes are found via `pg_index.indisvalid` (`pg_indexes` reports them as present) and dropped before rebuild. Operational detail: `packages/cli/AGENTS.md`.
|
|
290
|
-
- **Retry logic is transient-only (#2366)**: `db.get()`/`db.upsert()` retry 4× total (initial + 3), but `ErrorUtils.withRetry` classifies via the cause chain (`src/db-errors.ts`) and rethrows deterministic failures immediately — constraint violations, bad input syntax, missing tables, aborted PG tx (`25P02`). `@happyvertical/sql` stringifies the driver text into `context.originalError`, so **never match `error.message`**; use `classifyDatabaseError()` / `isUniqueViolationError()` / `isAbortedTransactionError()`.
|
|
291
|
-
- **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query
|
|
292
|
-
- **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)
|
|
293
|
-
- **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls
|
|
294
|
-
- **Manifest required**: build-time AST scanning creates manifest. Without vitest plugin → "No field metadata"
|
|
295
|
-
- **ManifestBuilder fails on scanner errors**: every production manifest path
|
|
296
|
-
must abort before adapting partial scan results. A syntax error or unresolved
|
|
297
|
-
`@smrt()` config spread cannot be allowed to emit a default-open manifest.
|
|
298
|
-
- **Vite plugin loads scanner from `dist/` first**: `src/vite-plugin/import-build-aware.ts` prefers `dist/` when it exists on disk; it only falls back to `src/` on fresh clones. So if you edit `src/scanner/*.ts` or `src/schema/generator.ts` and want those edits reflected in consumer manifest generation, you must rebuild (`pnpm build` or have `pnpm dev` / `pnpm build:watch` running in core). This is intentional — sniffing `.ts` vs `.js` via `import.meta.url` was non-deterministic under tsx and broke 12–13 publishes (#1139).
|
|
299
|
-
- **Bundled registry ownership**: flattened production bundles can rewrite constructor names and make decorator-time stack inference attribute provider code to the consumer. Generated registration repairs identity only from the exact imported constructor plus an explicit package and isolated one-object manifest; never infer ownership from output paths, simple names, or table names. Distinct packages may export the same simple name under qualified keys. The production-consumer gate lives in `packages/bundle-gate/src/__tests__/registry-identity.spec.ts` (#2308).
|
|
88
|
+
The PostgreSQL lane is required for numeric types, UUID casts, conflict targets,
|
|
89
|
+
timestamps, and migrations. Tests generate their manifest before Vitest; restart
|
|
90
|
+
watch mode after adding decorated classes. Documentation-only changes need
|
|
91
|
+
instruction-chain and knowledge freshness checks, not the runtime test suite.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Decorators, build integration, and knowledge
|
|
2
|
+
|
|
3
|
+
Key options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes; see [schema-paths.md](schema-paths.md)), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).
|
|
4
|
+
|
|
5
|
+
Registration sets `SMRT_TABLE_NAME` static property (survives minification).
|
|
6
|
+
|
|
7
|
+
## @field() UI hints (#2046)
|
|
8
|
+
|
|
9
|
+
`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only
|
|
10
|
+
seed for the field-policy rail (epic #2045). Carried in the manifest under the
|
|
11
|
+
field's `_meta.ui` (never a top-level `FieldDefinition` key), readable at
|
|
12
|
+
runtime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with
|
|
13
|
+
`description` into generated web-collection definitions and browser MCP tool
|
|
14
|
+
schemas. No schema/persistence/security effect — `sensitive`/`readPermission`
|
|
15
|
+
stay the security rail, and `sensitive`/`transient` fields never emit to the
|
|
16
|
+
client at all.
|
|
17
|
+
|
|
18
|
+
## Lightweight discovery
|
|
19
|
+
|
|
20
|
+
`src/knowledge-discovery.ts`, exported through `smrt-core/knowledge`, enumerates
|
|
21
|
+
installed scope directories and reads canonical AGENTS/legacy CLAUDE docs without
|
|
22
|
+
loading package code, artifacts, or scanning objects. CLI snapshots and MCP share
|
|
23
|
+
these primitives. Callers retain selection policy: snapshots resolve links and
|
|
24
|
+
fall back to `packages/*` only when no installed SMRT package loads; MCP preserves
|
|
25
|
+
node_modules paths, deduplicates realpaths, excludes authored workspace links,
|
|
26
|
+
and then enriches the selected packages. Workspace-root/glob discovery remains
|
|
27
|
+
owned by each consumer.
|
|
28
|
+
|
|
29
|
+
## Domain Knowledge Artifacts
|
|
30
|
+
|
|
31
|
+
`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:
|
|
32
|
+
|
|
33
|
+
- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`
|
|
34
|
+
- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`
|
|
35
|
+
|
|
36
|
+
Keep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic
|
|
37
|
+
agent contract for downstream review and architecture tools.
|
|
38
|
+
|
|
39
|
+
The schema-version-1 object projection is additive and high-signal: it retains
|
|
40
|
+
normalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,
|
|
41
|
+
method signatures, and field defaults/constraints/readonly/transient flags.
|
|
42
|
+
Sensitive fields are removed before both `fields` and `relationships` are
|
|
43
|
+
derived, including legacy flags stored under `_meta`; matching field and
|
|
44
|
+
snake-case column names are also removed from projected conflict columns, and a
|
|
45
|
+
sensitive custom tenant field is omitted while retaining scope and mode.
|
|
46
|
+
Generated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;
|
|
47
|
+
the optional marker keeps schema version 1 additive while letting readers
|
|
48
|
+
identify older artifacts that require raw-manifest corroboration.
|
|
49
|
+
|
|
50
|
+
Config precedence for knowledge is defaults → top-level `knowledge` in
|
|
51
|
+
`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →
|
|
52
|
+
object-level `@smrt({ knowledge })`.
|
|
53
|
+
|
|
54
|
+
Object-level `knowledge: false` excludes an object from authored context only;
|
|
55
|
+
it must not change runtime manifest registration. Use
|
|
56
|
+
`knowledge: { tags, summary, risks }` for review-sensitive domain objects.
|
|
57
|
+
|
|
58
|
+
HTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is
|
|
59
|
+
true, generated SvelteKit routes must stay GET-only and guarded by dev mode or
|
|
60
|
+
admin auth.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Vite Plugin
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)
|
|
67
|
+
export default defineConfig({
|
|
68
|
+
oxc: {
|
|
69
|
+
decorator: {
|
|
70
|
+
legacy: true,
|
|
71
|
+
emitDecoratorMetadata: true,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Under Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`
|
|
78
|
+
recipe (or tsconfig `experimentalDecorators` reached through SvelteKit's
|
|
79
|
+
`extends "./.svelte-kit/tsconfig.json"` chain), so that recipe throws
|
|
80
|
+
`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure
|
|
81
|
+
decorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need
|
|
82
|
+
the legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,
|
|
83
|
+
emitDecoratorMetadata: true`.
|
|
84
|
+
|
|
85
|
+
For independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept
|
|
86
|
+
the same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The
|
|
87
|
+
schema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the
|
|
88
|
+
merged project/dependency manifest, portable source paths, and source-file
|
|
89
|
+
digests; each plugin selects its own view. Reuse mode fails closed on
|
|
90
|
+
byte/provenance/path/content drift, skips scans and manifest writes, and still
|
|
91
|
+
generates routes, types, registration, and virtual modules. Omit it for normal
|
|
92
|
+
local development and watch mode.
|
package/agents/memory.md
CHANGED
|
@@ -14,3 +14,7 @@ by `@smrt({ embeddings })`, with native pgvector/HNSW or an in-memory fallback.
|
|
|
14
14
|
Results hydrate through `list({ 'id in': … })`, so normal tenant isolation still
|
|
15
15
|
applies. Keep injected search behind the `SmrtCollection.semanticSearch`
|
|
16
16
|
boundary.
|
|
17
|
+
|
|
18
|
+
`LearningMemory.capture()` reinforces successes and decays failures while
|
|
19
|
+
updating outcome counters. Its tenant-isolated `recall()` applies confidence,
|
|
20
|
+
expiry, time-decay, and hierarchical-scope filters and refreshes `last_used_at`.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Object and collection runtime
|
|
2
|
+
|
|
3
|
+
`constructor(options)` → `initialize()` → ready for `save()`/`delete()`/`loadFromId()`
|
|
4
|
+
|
|
5
|
+
- `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided
|
|
6
|
+
- `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)
|
|
7
|
+
- Persisted `save()` uses loaded `updated_at` in its `UPDATE`; zero rows throws
|
|
8
|
+
`RUNTIME_REVISION_CONFLICT`. Explicit `expectedUpdatedAt` binds a save or
|
|
9
|
+
delete to an earlier snapshot. Remote guarded deletes bind the same predicate
|
|
10
|
+
into the final `DELETE`; embedded adapters compare inside the shared write queue
|
|
11
|
+
before cascading. That queue serializes same-process saves, deletes, and full
|
|
12
|
+
`SmrtObject.withTransaction()` callbacks. Custom writes must preserve this
|
|
13
|
+
public CAS ordering contract. PostgreSQL predicate:
|
|
14
|
+
[revision-guard.md](revision-guard.md).
|
|
15
|
+
- Native DuckDB UUID columns are hydrated as canonical strings before model
|
|
16
|
+
initialization, natural-key lookup, and embedded revision claims. Exact
|
|
17
|
+
natural-key probes retain the interceptor-authorized filter when
|
|
18
|
+
canonicalizing a wrapped identity. Custom embedded-CAS paths that consume
|
|
19
|
+
persisted rows must use `getCanonicalPersistedRow()` so UUID identities are
|
|
20
|
+
cast in the same coherent read before reuse.
|
|
21
|
+
- `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a "content body" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)
|
|
22
|
+
- `save()` error contract (#2366): unique/PK violation → `ValidationError` `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL → `VALIDATION_REQUIRED_FIELD`, both on the first attempt on every adapter; any other database failure → `DatabaseError` with the driver error on `cause`
|
|
23
|
+
- `getSlug()`: auto-generates from name → title → label → id
|
|
24
|
+
- `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## SmrtCollection Query
|
|
28
|
+
|
|
29
|
+
Projection, latest-related, facets, counts, and bounded read plans are
|
|
30
|
+
documented in [collection-reads.md](collection-reads.md).
|
|
31
|
+
|
|
32
|
+
`list()` and `query()` hydrate model instances serially in result order because
|
|
33
|
+
an `initialize()` hook may query through the same transaction-bound PostgreSQL
|
|
34
|
+
client. Keep this serialization invariant; use `select` when callers need plain
|
|
35
|
+
rows without model hydration.
|
|
36
|
+
|
|
37
|
+
Native DuckDB model hydration casts declared UUID columns to `VARCHAR` in the
|
|
38
|
+
read query because its JavaScript binding otherwise returns lossy HUGEINT
|
|
39
|
+
wrapper objects. Explicit projections apply the same cast for selected UUID
|
|
40
|
+
fields so bounded query envelopes preserve canonical row and relationship ids.
|
|
41
|
+
For STI child columns, raw `query()` SELECTs, and latest-related projections,
|
|
42
|
+
the read path describes the output types without evaluating the query, then
|
|
43
|
+
performs one data-bearing SELECT with UUID result columns cast to `VARCHAR`;
|
|
44
|
+
mutation statements are never reinterpreted or replayed.
|
|
45
|
+
|
|
46
|
+
**WHERE operators**: `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, `like`.
|
|
47
|
+
Arrays auto-detect `IN`. NULL is a value, not an operator: `{ deletedAt: null }`
|
|
48
|
+
renders `IS NULL` and `{ 'deletedAt !=': null }` renders `IS NOT NULL`.
|
|
49
|
+
|
|
50
|
+
`convertWhereKeys` must accept only operators executable by the SQL builder.
|
|
51
|
+
`contains` and dot-notation JSON paths reject at the boundary; use `like` with
|
|
52
|
+
explicit wildcards. Adding operators requires SQL support first.
|
|
53
|
+
`src/__tests__/issue-2276-where-contract.test.ts` executes the accepted set.
|
|
54
|
+
|
|
55
|
+
STI child collections auto-filter by `_meta_type`. Query bounds — `LIMIT 1` on `get()`, the `limit`/`offset` parser, the `orderBy` whitelist and sensitive/permission refusals, and the deterministic generated-list ordering (#2367) — are in [query-bounds.md](query-bounds.md).
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## DispatchBus
|
|
59
|
+
|
|
60
|
+
- `emit(signalType, payload, metadata)` → creates persistent Dispatch record
|
|
61
|
+
- `on(pattern, handler)` → in-memory handler (immediate)
|
|
62
|
+
- `subscribe({ signalType, subscriber })` → persistent subscription (survives restarts)
|
|
63
|
+
- `process(subscriberName, handler)` → process pending dispatches
|
|
64
|
+
- Wildcards: `campaign.*` matches `campaign.completed` (single segment only)
|
|
65
|
+
- Tables: `_smrt_dispatch`, `_smrt_dispatch_subscriptions`
|
|
66
|
+
- Status: `pending → processing → completed` (or `failed`)
|
|
67
|
+
|
|
68
|
+
## Single Table Inheritance (STI)
|
|
69
|
+
|
|
70
|
+
- Base: `@smrt({ tableStrategy: 'sti' })` — children inherit, share one table
|
|
71
|
+
- Discriminator: `_meta_type` column with qualified names (`@happyvertical/smrt-content:Article`)
|
|
72
|
+
- Child fields: `@meta()` decorator → stored in `_meta_data` JSONB (not as columns)
|
|
73
|
+
- Polymorphic queries: collection loads `_meta_type`, creates correct subclass dynamically
|
|
74
|
+
- Validation: fail-fast on save if `_meta_type` missing or mismatched
|
|
75
|
+
|
|
76
|
+
## Child Accessors (R10)
|
|
77
|
+
|
|
78
|
+
`src/child-accessors.ts` installs a consistent `get<FieldName>()` instance method for every `@oneToMany` field at `@smrt()` registration time (e.g. `@oneToMany('OrderItem') items` → `order.getItems()`), delegating to `loadRelatedMany`. Two invariants:
|
|
79
|
+
|
|
80
|
+
- **Additive** — never overwrites a hand-rolled method of the same name (checks the whole prototype chain). `Profile.getMetadata()` (key-value) and `ProfileRelationship.getTerms()` are preserved.
|
|
81
|
+
- **Runtime-only** — attached to the prototype, invisible to the build-time manifest, so it never leaks into the REST/CLI/MCP surface.
|
|
82
|
+
|
|
83
|
+
When the target declares multiple FKs back to the parent, annotate `@oneToMany(Target, { foreignKey: '<inverseField>' })`; `loadRelatedMany` and the eager `include:` loader both honor it (else first-match).
|