SMRT Monorepo Package Standards
See the MCP Apps integration standard for portable embedded UI, optional OpenAI extensions, ownership and staged release gates.
This document defines the standards every package in packages/* must follow. It exists to:
- Make every package look the same so contributors can move between packages without surprise
- Catch drift early via a checkable list rather than ad-hoc review
- Provide a single canonical reference the audit epic and per-package issues link to
The standard is prescriptive. Where a package needs to deviate, it must document why in its own AGENTS.md.
The companion document docs/PROJECT_REQUIREMENTS.md defines requirements for SMRT consumer projects. This document defines requirements for packages inside this monorepo. The two overlap heavily, but the audiences are different.
Table of contents
- Repository layout
- package.json
- Build configuration
- TypeScript configuration
- Testing
- Documentation
- Code conventions
- UI packaging (Svelte)
- Triple-consumption packages
- Templates
- Forbidden artifacts
- Appendix A: audit snapshot
- Appendix B: rationale for changes
1. Repository layout
Required files
packages/<name>/
├── src/
│ ├── index.ts # public API root
│ └── __tests__/ # unit + integration tests
├── package.json
├── tsconfig.json
├── vite.config.ts # uses createPackageConfig(name, opts?)
├── vitest.config.ts # uses smrtVitestPlugin()
├── README.md
├── AGENTS.md # canonical package expert guidance
├── CLAUDE.md # one-line Claude Code shim: @AGENTS.md
└── CHANGELOG.md # changesets-managed
Conditional files
| File | When required |
|---|---|
src/__smrt-register__.ts | Package defines @smrt() classes (issue #1132 self-registration pattern) |
src/svelte/ and ambient.d.ts and tsconfig.svelte.json | Package ships Svelte UI components |
src/manifest/ | Package participates in build-time manifest generation |
tsconfig.build.json | declaration emission needs different inclusions than tsc --noEmit |
tsconfig.typecheck.json | tsc --noEmit needs different inclusions than build |
bin/ | Package exposes a CLI binary |
e2e/ and playwright.config.ts | Package has Playwright end-to-end tests |
Non-TypeScript packages
packages/smrt-mobile (Kotlin Multiplatform), packages/smrt-android
(Android/Compose), and packages/smrt-ios (SwiftUI/XcodeGen) are the monorepo's
non-TypeScript packages (ADR 0001). They keep the workspace wrapper surface
(package.json, AGENTS.md, CLAUDE.md shim, README.md) but are exempt —
via the NON_TYPESCRIPT_PACKAGES set in scripts/check-standards.mjs — from
the TypeScript-specific requirements: no vitest.config.ts and no vitest
test/test:watch scripts (their tests are Gradle/kotlin.test and Swift
XCTest, run by the .github/workflows/mobile.yml lane), no typecheck script
(the Kotlin/Swift compilers typecheck), and no dist in files (nothing
publishes to npm: private: true, Maven/SPM publishing deferred per the ADR
0001 Phase 0 decision record; all stay in the changesets fixed group so their
versions ride the release train). Their always-on structural gates are
validate:shell / validate:android / validate:ios, wired as each package's
build script.
Forbidden at any package root or src
See §11 for the full list.
2. package.json
Skeleton
{
"name": "@happyvertical/smrt-<name>",
"version": "0.X.Y",
"type": "module",
"description": "<one sentence>",
"author": "HappyVertical",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/happyvertical/smrt.git",
"directory": "packages/<name>"
},
"files": ["dist", "AGENTS.md", "CLAUDE.md"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "vite build",
"build:watch": "vite build --watch",
"dev": "vite build --watch",
"clean": "rm -rf dist .turbo",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit -p tsconfig.json",
"prepack": "node ../../scripts/prepack-package.js",
"verify:pack": "node ../../scripts/verify-pack.js"
},
"dependencies": {
"@happyvertical/smrt-core": "workspace:*",
"@happyvertical/ai": "catalog:"
},
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
}
}
Rules
type: "module"— alwaysauthor: "HappyVertical"— uniform; replace"HAVE Team","Will Griffin <willgriffin@gmail.com>", and missing fieldsrepository.directory— requiredfiles—["dist", "AGENTS.md", "CLAUDE.md"]. Add"bin"if package has a CLI. Do not addREADME.md(it is published automatically). Do not add directories that don't exist.- Exports map condition order:
{types, import}—typesmust come first (Node resolves the first matching condition; ifimportis first, types resolution silently fails). Bare-string targets are forbidden — every entry must be a conditional object. - Dependencies:
- All
@happyvertical/smrt-*references useworkspace:* - All
@happyvertical/sdkreferences (ai,sql,files,utils,cache,documents,email,encryption,geo,images,jobs,json,logger,messages,ocr,pdf,projects,repos,secrets,spider) usecatalog: @types/nodealwayscatalog:vite,vitest,typescriptcome from root devDependencies — do not redeclare per-package unless overriding- Pinning style: prefer caret (
^X.Y.Z) for third-party deps; exact pins (X.Y.Z) only for tools where minor bumps cause breakage (document why)
- All
- Coordinated releases: every publishable
@happyvertical/smrt-*package belongs to the fixed release group in.changeset/config.jsonand carries the same version as that group.scripts/check-standards.mjsenforces both invariants before publish validation. - Scripts: every package has
build,build:watch,dev,clean,test,test:watch,typecheck,prepack,verify:pack. Nolintorformatscripts — those are root-level Biome tasks only (pnpm lint/biome ci/pnpm format-check), which already gate every package on PRs;scripts/check-standards.mjsforbids per-packagelint/lint:fix/format/format-checkscripts so drift back to them is caught (S2, #1374). The presence oftypecheckis likewise enforced byscripts/check-standards.mjs; the only carve-outs are the plain-JS template wrappers (template-sveltekit,template-site-static-json), whose typecheck obligation lives in their scaffoldedtemplate/package.json(see §10). peerDependencies:- Svelte peer is one uniform floor (currently
svelte: ^5.57.0) for packages shipping UI. It is a consumer compatibility floor, not the dev version: Renovate widens it on a new major only (renovate.json), and it is raised by hand only when smrt needs a newer Svelte feature or fix - Optional peers explicitly marked in
peerDependenciesMeta - Required peers (e.g.
usersrequiresprofiles) documented inAGENTS.md
- Svelte peer is one uniform floor (currently
publishConfig.registryalwayshttps://registry.npmjs.orgfor published packages, withaccess: "public"
Copied resource exports
The packed-export verifier distinguishes executable exports from authored files copied by a scaffold. A package may explicitly declare copied directory exports:
{
"exports": { ".": "./index.js", "./template/*": "./template/*" },
"smrt": { "resourceExports": ["./template/*"] }
}
Each declaration must be a unique non-root directory pattern with one terminal
* and an identical, unconditioned export target. Directory segments start with
an ASCII letter, digit, _ or -, followed by those characters or ..
Root/global patterns, traversal, conditional targets and unknown export keys
are rejected. The declared directory must contain files in the actual tarball;
a directory entry alone is insufficient. Metadata is read from packed
package.json, never inferred from package names or file extensions.
Resource exports retain packaged-presence checks but do not enter the runtime
JavaScript/Svelte analysis merely because a copied file has a code extension.
This allows unrendered scaffold placeholders without parsing or executing
template configuration. Runtime roots, other exports (including wildcards),
and runtime imports into resource directories retain all normal checks. A
runtime entry reaching Svelte still requires an effective svelte condition.
The declaration does not change Node's exports map or scaffold copy behavior.
node --test scripts/verify-template-packed-exports.test.mjs creates both real
template tarballs with normal pnpm pack hooks and verifies them through the
root wrapper. Complete publish validation uses
node scripts/validate-publish-packages.js for every shard, then
node scripts/verify-publish-artifacts.mjs <output-directory> to check exact
publish membership, versions and digests. This covers all publishable packages,
including those without a convenience verify:pack script.
The validator also runs both manifest guards for every package it packs
(scripts/release-manifest-guards.mjs, #2853), so coverage does not depend on
a package defining prepack. Completeness (#1483) re-scans src/ in
--release mode: the scanner must load, and a package that declares @smrt()
objects without a ./manifest export fails. The packed manifest must match the
verified workspace manifest byte for byte. The exports guard (#2845) runs
against the tarball installed into a scratch consumer with only its declared
dependencies linked. Exemptions stay those of the verifiers: framework
infrastructure packages, collection companions and bundler-only root barrels.
A closed API/CLI/MCP surface does not exempt a public model export. prepack
still runs the same verifiers in their lenient workspace mode (an unbuilt
scanner or a missing ./manifest export skips, and exports are checked against
workspace dist/); only the release validator is fail-closed and bound to the
tarball. A local changeset publish therefore gets the weaker check.
3. Build configuration
Standard config
// vite.config.ts
import { createPackageConfig } from '../../vite.config.base';
export default createPackageConfig('<package-name>', {
// optional
entries: ['ui', 'playground'],
svelte: 'svelte',
});
Rules
- Always use
createPackageConfigfromvite.config.base.ts. Hand-writtenvite.config.tsfiles require an explicit comment stating why and a tracking issue. - Build target:
es2022for libraries;node20only for tools that must run server-side (CLIs);node24only when explicitly required. - Declarations:
scripts/declarations.tsruns the pinnedtsc --emitDeclarationOnlycompiler through the shared and custom Vite builds. Declarations remain separate modules; no API Extractor or declaration bundler is used. Compiler errors fail every library build. - Declaration layout: all packages emit separate declaration modules. Keep component declarations in the existing
svelte-packagestep. - Output format: ESM only (
formats: ['es']) - Sourcemaps: on
vitestpackage is exempt — it must build withtscbecause it provides the vite plugin to others. The emptyvite.config.tsshould be removed.
4. TypeScript configuration
Base configs at repo root
| Base | Purpose |
|---|---|
tsconfig.json | Default for libraries — strict, ES2022, ESNext modules, bundler resolution |
tsconfig.package-build.json | Shared declaration/source-check settings |
tsconfig.package-svelte.json | Settings for packages with .svelte source |
tsconfig.package-typecheck.json | Settings for tsc --noEmit |
tsconfig.package-ui.json | Settings for packages shipping UI (extends svelte + adds DOM lib) |
Per-package rules
- Always have
tsconfig.jsonextending the appropriate base - Add
tsconfig.svelte.jsononly if.sveltesource is present - Add
tsconfig.typecheck.jsononly if typecheck inclusions differ from build - Add
tsconfig.build.jsononly if declaration emission needs different inclusions - Maximum: 4 tsconfig files per package. If you need more, talk to maintainers first.
- Do not extend
tsconfig.kit.jsonunless the package ships a SvelteKit app (templates only)
5. Testing
vitest.config.ts
import { defineConfig } from 'vitest/config';
import { smrtVitestPlugin } from '../vitest/src/index.ts';
export default defineConfig({
plugins: [smrtVitestPlugin({ verbose: true })],
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
testTimeout: 30000,
fileParallelism: false,
pool: 'forks',
},
});
Note:
fileParallelism: falsealready pins the pool to a single worker — Vitest overrides any user-suppliedmaxWorkersin that case, so do not addmaxWorkers. ThepoolOptions.forks.singleForkoption was removed in Vitest 4, and a flattest.singleForkwas never valid at any version; a straysingleForkkey is silently ignored rather than rejected.
isolate: false is an opt-in exception, not the default
Files still run one at a time under fileParallelism: false, but isolate
defaults to true, so each test file gets a fresh fork that reloads the whole
SMRT manifest and base-class graph. Setting isolate: false reuses one fork and
loads that graph once, which is worth it for the heaviest packages —
packages/video uses it and drops from four worker forks to one.
Do not apply it by default. It shares one module registry across test files,
which makes execution order significant, and Vitest reorders files between runs
from its cached durations. Measured on this repo, 3 of 14 packages broke under
it: agents and assets raised Class '...' is not registered because the
ObjectRegistry globalThis singleton carried mutated state across files, and
content leaked module-level Svelte mocks. Before enabling it for a package,
run that suite under randomized file order
(vitest run --sequence.shuffle.files --sequence.seed=N) across several seeds
and keep per-file isolation if any of them fail.
If a package needs the memory relief but its suite depends on per-file
isolation, keep isolate: true and raise the per-fork heap instead
(NODE_OPTIONS=--max-old-space-size=... on that package's test script).
Correctness beats uniformity: never disable isolation to make a package match
its siblings.
Note: Inside this monorepo, packages import
smrtVitestPlugindirectly from the workspace source (../vitest/src/index.ts) rather than from the published@happyvertical/smrt-vitest. This avoids a circular workspace build dependency. Consumer projects (outside the monorepo) should import from the package name.
Rules
smrtVitestPlugin()is mandatory. This is non-negotiable; it is the framework's own dogfooding requirement.- Test naming (per
.claude/rules/testing.md):*.test.ts— unit*.spec.ts— integration*.optional.test.ts— requires external APIs, skipped in CI
- Build output is not a test input:
smrtVitestPlugin()excludes**/dist/**and**/.svelte-kit/**in the root and inline test projects, retaining Vitest defaults and consumer exclusions. Independent projects keep their own custom filters; root filters follow Vitest's explicitextendsinheritance. - Test location: tests live under
src/__tests__/. Colocated tests are deprecated; do not introduce new ones. - Minimum: every published package has at least one unit test. Stub packages (e.g.
gnode) document why they don't and link to the implementation issue. - Templates: at least one Playwright e2e verifying
pnpm scaffoldworks end-to-end. testTimeout: 30s default; raise per-test if a specific case needs longer; raise the package default only with documented reason (usersis at 60s for legitimate reasons).
6. Documentation
Required
README.md— ≥80 lines. Purpose, install, basic usage, link toAGENTS.md, links to other relevant package READMEs.AGENTS.md— ≥30 lines. Package-specific patterns, gotchas, integration points. Included infiles:allowlist.CLAUDE.md— exactly one line:@AGENTS.md. Included infiles:allowlist for Claude Code compatibility.CHANGELOG.md— managed by changesets. Don't hand-edit.
Discouraged
These create a "every package has its own snowflake docs" problem:
ARCHITECTURE.mdSPEC.mdMIGRATION.mdBRAINSTORM.mdAUTO_POPULATE_GUIDE.mdSECRETS_MIGRATION.mdTEMPLATE_README.md
If you have content that would go into one of those files, the right home is:
- Architectural reasoning →
docs/architecture/<topic>.md(Docusaurus site) - Migration instructions for a specific change → ephemeral migration note in the changeset
- Specs and brainstorms →
docs/rfcs/ - Detailed how-tos →
AGENTS.md
Existing files of these types should be migrated and the per-package files removed.
7. Code conventions
These are already documented in the root AGENTS.md. They are reproduced here for completeness:
@smrt()decorator on every persisted class- Never override
toJSON(). UsetransformJSON()instead. The base class handles STI discriminator and meta-field extraction.- Exception:
tenancy/interceptor.tscallsinstance.toJSON()directly to handle stub instances. This must be documented in the file with a comment.
- Exception:
- Same-package foreign keys: use
@foreignKey(Target). - Cross-package foreign keys: use
@crossPackageRef('@happyvertical/smrt-package:Class'); this avoids circular DDL constraints while preserving runtime relationship metadata. - Single-owner junction collections extend
SmrtJunctionand exposebyLeft()/byRight()plus options-objectattach()/detach(). - Hierarchical tree models extend
SmrtHierarchical; chains/DAGs use package-specific fields and methods. - Polymorphic generic/provenance links extend
SmrtPolymorphicAssociation. @TenantScoped({ mode: 'optional' })on tenant-aware models. Tenant-aware packages without the decorator (secrets,prompts,features,imagesfor some models) must document the tenant strategy inAGENTS.md.__smrt-register__.tsself-registration imported fromindex.ts(issue #1132 pattern)@meta()for STI child-specific fields (stored in_meta_data, not as columns)- STI discriminator format:
@happyvertical/smrt-<package>:<ClassName> - Numeric defaults:
count: number = 0→ INTEGER;price: number = 0.0→ DECIMAL - Composite-owner junction collections extend
SmrtJunctionBase, sharing right-side reads, ordering, positioning, and registration guards without inheriting incompatible single-owner methods.AssetAssociationCollectionkeeps its requiredmetaType, metaIdarguments. For runtime checks covering both kinds, useinstanceof SmrtJunctionBase; asset associations no longer satisfyinstanceof SmrtJunction. conflictColumnsset on junction/upsert tables- System tables prefixed
_smrt_ - JSON fields stored as strings with
getX()/setX()helpers wrapped intry/catch
Browser-safe model package roots
Every publishable package that declares @smrt() objects, plus smrt-core, must have a root entry that builds for a browser target (Vite browser resolution conditions, over published-style dist exports) without reaching Node-only modules: node: built-ins, pg, pg-pool, express, cosmiconfig, jiti, native addons, and the other modules listed in FORBIDDEN_NODE_ONLY_MODULES in packages/bundle-gate/src/browser-gate/boundary.ts. Node-only code (servers, workers, filesystem, Node-only crypto, config loaders) lives behind a subpath export or a lazy await import() that follows the bundle-boundary optional-dependency pattern (#1977). Static imports of server/worker/config code from a root entry are violations, even when guarded at runtime, because bundlers follow them.
The bundle-gate browser fixture (#3621) enforces this in CI. Known violations are ratcheted in packages/bundle-gate/src/browser-gate/expected-failures.ts, each tied to a tracking issue. The gate fails on a package that is not listed and on a listed package that now passes; the PR that fixes a package deletes its own entry. The goal is an empty list. Packages that declare @smrt() objects but are intentionally not gated (for example smrt-cli) are listed with their reason in MODEL_PACKAGE_EXCLUSIONS.
Logging (S14 / dim 9)
Shipped library code logs through @happyvertical/logger, never console.*.
console is reserved for contexts where stdout/stderr is the product, not a
diagnostic side-channel.
Use the logger — runtime diagnostics emitted by shipped library code (caught errors, recoverable warnings, operational traces):
import { createLogger } from '@happyvertical/logger';
const logger = createLogger({ level: 'info' });
logger.error('Failed to load schema', { error }); // was console.error(error)
logger.warn('Falling back to default', { id }); // was console.warn(...)
logger.debug('resolved relationship', { target }); // was a diagnostic console.log
Map by intent: a caught/operational error → logger.error; a recoverable
problem → logger.warn; developer diagnostics → logger.debug (or info for
genuinely operational milestones).
Keep console (Biome noConsole: off) where the output IS the contract:
- the
clipackage's user-facing command output (results, tables, prompts, help) — its internal diagnostics still use the logger; - standalone / dev / demo entrypoints —
server.ts,*-server.ts,bin/,scripts/,lib/server/seed-*,demo*; - build-time & codegen tooling — vite / consumer plugins, scanners, prebuild,
and the REST / CLI / MCP / manifest generators (
vite-plugin/,consumer-plugin/,prebuild/,scanner/,generators/,manifest/generator*,manifest/discover-*). Note this is the generation side only — runtime manifest loading (manifest/manifest-loader.ts,store.ts) is shipped library code and uses the logger; - test files and
*.config.*(already exempt).
Browser / Svelte code keeps console. @happyvertical/logger is a
Node/server logger; it has no place in code that runs in the browser. So .svelte
components and browser-only modules (e.g. smrt-svelte, browser-ai/ adapters,
client-side state) use console — that's the browser's diagnostic channel.
Migrate to the logger only on the Node/server side (collections, services, server
routes/hooks, ORM/runtime libraries).
Never touch console.* inside comments or JSDoc @example blocks — that is
documentation, not code.
Enforcement (ratchet). Global noConsole: "warn" in biome.json.
Keep-console contexts above get "off" overrides; a runtime module flips to
"error" once migrated — per package/file, the same incremental ratchet used by
the design-token sweeps (S1). The raw console.* count overstates the work:
most of it is the keep-console contexts above; the real target is runtime
library logging, concentrated in core.
Secret scanning (S7)
Committed credentials are blocked by gitleaks, run deterministically (tool-only, no model-assisted checks) in two places:
- lefthook pre-commit —
gitleaks git --stagedscans the staged diff before the commit lands. Local-only and best-effort: if gitleaks isn't installed it warns and skips (CI is the hard gate). Install withbrew install gitleaks. - CI (
on-pull-request) — theSecret Scan (gitleaks)job installs a pinned gitleaks and scans the PR commit range (merge-base..HEAD). A finding fails the PR.
Both runs pass --redact, so a matched secret is never printed to logs (org
secret-handling policy). Real secrets belong in Warden, never in the repo.
Config + allowlist. .gitleaks.toml at the repo root is the single source of
truth: it extends gitleaks' default rules (useDefault = true) and allowlists
justified false positives (build artifacts under dist/, the lockfile, *.test.
/*.spec. fixtures that embed deliberate dummy keys, and the legacy
initial-import commit). Add new exclusions there with a justification comment —
never disable the scan.
Dependency audit (S8)
pnpm audit runs as a CI gate on every PR (on-pull-request →
Dependency Vulnerability Audit), reading the dependency tree from
pnpm-lock.yaml (no install needed).
- Blocking threshold: high. The gate runs
pnpm audit --audit-level=high, so any high or critical advisory fails the PR. Moderate/low are reported but non-blocking. - Remediation first. Prefer fixing over ignoring: most advisories are stale
transitive deps with a published patch, fixable by a version-range-scoped entry
in
pnpm-workspace.yamlunderoverrides(e.g."undici@>=7.0.0 <7.24.0": "7.24.0"). Scope the key to the vulnerable range so unrelated majors aren't force-bumped. - Accept-with-justification. Only when an advisory can't be remediated without
breaking a pinned API, or no patched release exists at all (e.g.
braces, #3396), add its GHSA toauditConfig.ignoreGhsasinpnpm-workspace.yamlwith asuppressed: truerecord inaudit-policy.json: reachability evidence, the upstream blocker, and arecheckBydate.pnpm audit:policyenforces the records and fails once a record passes itsrecheckBydate.pnpm audit:policy:driftwarns about every livepnpm auditadvisory that has no record, and about unsuppressed records whose advisory has left the tree. It cannot do the latter for suppressed records: pnpm removes ignored GHSAs from the audit payload, so a suppressed record is revisited through itsrecheckBydate instead. Revisit baselined advisories when their blocker is removed.
8. UI packaging (Svelte)
Required exports for UI packages
{
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
"./ui": { "types": "./dist/ui.d.ts", "import": "./dist/ui.js" },
"./svelte": {
"types": "./dist/svelte/index.d.ts",
"svelte": "./dist/svelte/index.js",
"import": "./dist/svelte/index.js"
},
"./playground": { "types": "./dist/playground.d.ts", "import": "./dist/playground.js" },
"./workbench": { "types": "./dist/workbench.d.ts", "import": "./dist/workbench.js" }
}
}
Rules
- Directory layout:
src/svelte/components/(not flatsrc/svelte/) svelte/index.ts: usesModuleUIRegistry.register(...)pattern. Pure re-exports without registry are deprecated../uisubpath: exportsMODULE_METAandUI_SLOTSconstants. The vite config builds auientry; the package.json must declare the matching export. (chatcurrently has the entry without the export.)- Selector slots (#3599): a
UI_SLOTSentry may declareselects: '@scope/package:Model'(string literal) to mark its component as the one selector for that model. The scanner reads it fromsrc/**/ui.tsinto the manifest'suiSelectors; generic forms use it for@foreignKey/@crossPackageReffields targeting the model. Presentation-only: the caller supplies the data. A model whose record label lives outside its own fields ships a selector instead of declaringdisplay.label. ./playgroundsubpath: exports the package's playground module for use bysmrt-playground./workbenchsubpath: exports package-owned routes and workbench metadata for use bysmrt-workbench- Svelte peer: one uniform floor (currently
svelte: ^5.57.0), not bumped with the devDependency. Drop the^4.0.0 || ^5.0.0range. - Build script:
vite build && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json && node ../../scripts/prune-svelte-package-artifacts.js dist - Typecheck script: packages with
./svelteexports must run both TypeScript and Svelte checks via the a11y wrapper, e.g.tsc --noEmit && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json(see Accessibility enforcement below). SvelteKit-backed packages should runsvelte-kit syncbefore both the TypeScript pass and thesvelte-check-a11ywrapper pass. tsconfig.svelte.json: extendstsconfig.package-svelte.json, includesambient.d.tsand*.svelte
Accessibility enforcement (S12, #1417)
Svelte's compiler a11y warnings are promoted to errors so a regression fails
the existing (required) typecheck CI gate — no separate build job and no new
required status check.
- Mechanism:
scripts/svelte-check-a11y.mjsis a drop-insvelte-checkwrapper that appends--compiler-warningswith an explicit comma-separated list of everya11y_*code mapped to:error(svelte-check's native warning-promotion flag — there is no wildcard syntax, so each code is listed). Every UI package'stypecheckcalls the wrapper instead of baresvelte-check, reusing each package's own tsconfig/codegen flow unchanged. - Single source of truth: the canonical
a11y_*code list lives in the wrapper. A Svelte upgrade that adds a NEW a11y code surfaces it as a plain (non-gating) warning until it is added to that list. - Escape hatch:
SMRT_A11Y_ENFORCE=0runs plainsvelte-checkwithout the promotion (local debugging only; CI always enforces). - Remediation pairs with the gate: the deterministic gate catches what the
compiler can see; component-test
axeassertions (S11 harness) cover the rest. As of S12 the repo is a11y-clean repo-wide (0 compiler a11y warnings). - Waiver —
products: itstypecheckisnpm run generate && tsc(nosvelte-check, a pre-existing #1370/#1375 carve-out for its mixed app-mode entrypoints), so its.sveltefiles are not a11y-gated. They are a11y-clean today (verified via standalonesvelte-check); wiringsvelte-checkinto theproductstypecheck is tracked with the rest of #1370.
Test artifacts in Svelte packages
Every svelte-package build ends with
node ../../scripts/prune-svelte-package-artifacts.js dist (or its narrower
Svelte output directory). This removes test/spec files, their declarations and
source maps, __tests__, and test-stubs, while preserving runtime components
and public test-support helpers. The packaging tool itself does not expose a
file filter. The shared declaration emitter excludes tests and fixture trees
even when a package supplies additional custom exclusions. scripts/validate-publish-packages.js checks the actual packed
tarball and rejects test artifacts, including files outside dist selected by
a package's files allowlist.
9. Triple-consumption packages
products is the reference template for packages that need to ship as:
- npm library (consumed via import)
- Module Federation host
- Standalone SvelteKit app
This is opt-in, not the default. Most packages are library-only. To opt in, the package must:
- Add
src/{lib,app,federation}/ - Add
federation.config.ts,index.html - Add scripts
dev:standalone,dev:federation,build:app,build:federation - Use a hand-written
vite.config.ts(notcreatePackageConfig) - Document in its
AGENTS.mdwhy it needs all three modes
Don't scaffold federation/standalone for a package unless there's a real consumer. commerce, ads, affiliates, ledgers, analytics — currently library-only — should stay that way.
10. Templates
Templates ship a template/ directory that is copied wholesale by the scaffold tool.
Rules
template/package.jsonalwaysprivate: truetemplate/package.jsonandtemplate.config.jsSMRT versions match the monorepo's current major (do not pin to^0.17.0while monorepo is at0.23.x)- All files referenced in
template/package.jsonscripts must actually exist intemplate/ template/README.md≥100 lines, explaining the scaffolding flow and runtime parameters- CI verifies that
pnpm install && pnpm buildsucceeds in each scaffolded template (would have caught thetemplate-site-static-jsonmissing-caelus.tsbug) - Scaffolded SvelteKit templates must ship
typecheckas a TypeScript pass plussvelte-check; otherwise generated projects inherit the.svelteambient typing gap.
11. Forbidden artifacts
These never belong in packages/* and should be .gitignore'd at the repo root:
Local Lefthook checks enforce the highest-signal subset of this list on staged files. The same hook suite also runs deterministic SMRT knowledge freshness checks:
- pre-commit:
pnpm knowledge:check --changed --strict --format markdown - pre-push:
pnpm knowledge:check --strict --format markdown
Knowledge hooks are deterministic and local. They must not call Codex, Claude, or any other model provider; model-assisted audits stay manual and must be followed by the deterministic checker.
Domain knowledge artifacts
Downstream packages and apps use smrt-knowledge.json as the deterministic
agent/developer contract. It is separate from manifest.json, which remains
runtime-focused.
- local dev/build artifact:
.smrt/smrt-knowledge.json - package build artifact:
dist/smrt-knowledge.json - package export, when published:
"./smrt-knowledge.json": "./dist/smrt-knowledge.json"
Knowledge artifact generation is on by default in the SMRT Vite plugin. CLI and
MCP consume these artifacts through deterministic dev tooling; HTTP exposure is
off by default and must be enabled explicitly with knowledge.api.enabled: true.
Generated HTTP routes are GET-only and must require dev mode or configured admin
auth.
Use object-level @smrt({ knowledge: false }) only to exclude an object from
authored agent context while preserving runtime manifest behavior. Use
@smrt({ knowledge: { tags, summary, risks } }) for package-specific review or
architecture constraints.
If a package exports ./smrt-knowledge.json, the package files allowlist must
publish dist or dist/smrt-knowledge.json, and the deterministic checker must
be able to find a current artifact.
Default tags/risks derivation (#2863)
When @smrt({ knowledge: { tags, risks } }) is not set on an object, or
knowledge.tags/knowledge.risks is not set on the package config passed to
buildDomainKnowledgeManifest, package-level tags/risks are derived from
facts the manifest already carries rather than left empty. This is a floor,
not a substitute for authored review context — a package should still set
knowledge.tags/knowledge.risks when it has one.
tags:package.json#keywords(authored), pluscross-packagewhen the package declares a@happyvertical/smrt-*dependency.risks:sensitive-fields-excluded(generation always strips sensitive fields — seesensitiveFieldsExcludedabove),cross-package-refs:<n>whenrelationshipsV2.crossPackageRefFields > 0,sti-inheritancewhen any object usestableStrategy: 'sti', andpolymorphic-associations:<n>when `relationshipsV2.polymorphicAssociations0`.
Object-level tags/risks are never derived — only what knowledge: { tags, risks } declares on that object, so a fine-grained annotation always traces
to an authored source.
Cross-package knowledge graph
.smrt/smrt-knowledge-graph.json (schema version 1, gitignored like
.smrt/smrt-knowledge.json) merges every discoverable per-package
smrt-knowledge.json — checked at packages/<pkg>/dist/smrt-knowledge.json
first, then packages/<pkg>/.smrt/smrt-knowledge.json — into one deterministic
root artifact: packages and objects nodes, and typed edges derived
directly from fields the per-package artifacts already carry (never invented):
| Edge type | Derived from |
|---|---|
crossPackageRef | An object field with type: 'crossPackageRef'; to resolves to the target object's node id when a scanned package declares it. |
sti | An object with tableStrategy: 'sti' and extends set; to is the parent object. |
junction / hierarchical / polymorphic | relationshipFeatures containing SmrtJunction / SmrtHierarchical / SmrtPolymorphicAssociation. |
systemTable | An object whose tableName starts with _smrt_. |
Generate it with pnpm knowledge:graph (also runs after pnpm build, so a
built repo always has a current graph). pnpm knowledge:check --strict
additionally treats the graph as stale — the same sourceHashes mechanism a
single package's artifact uses — whenever any per-package artifact it was
built from has changed since generation; the check is a no-op until at least
one package has a built smrt-knowledge.json to merge. The same check runs
inside the shared freshness function, so the smrt-dev-mcp
check-knowledge-freshness / reflect-* tools and smrt dev:knowledge-check
report a missing, invalid, or stale graph too; a missing graph is only
required where the root package.json declares a knowledge:graph script,
so consumer apps that never generate one are unaffected. smrt docs:agents
exports the graph alongside its per-package snapshot when run inside a
monorepo that has generated one. Implementation:
packages/core/src/knowledge-graph.ts, scripts/generate-knowledge-graph.ts.
Artifact and context vocabulary
Use these terms consistently. Do not call every generated file or ambient input "context":
| Term | Meaning |
|---|---|
| Source model | Authored TypeScript classes, decorators, and SMRT configuration. This is the source of truth. |
| Runtime manifest | The generated intermediate representation written to .smrt/manifest.json in development and dist/manifest.json in builds, then consumed by registry, schema, route, type, CLI, and MCP tooling. It describes objects; it is not a cross-invocation provenance envelope. |
| Domain knowledge artifact | smrt-knowledge.json, the deterministic, sanitized agent/developer projection of manifests plus package knowledge. It is not loaded as the runtime manifest. |
| Merged knowledge graph | .smrt/smrt-knowledge-graph.json, the deterministic root artifact merging every discoverable per-package domain knowledge artifact into one node/edge graph with typed cross-package edges (#2863). Derived from, and only as fresh as, the domain knowledge artifacts it merges. |
| Review or architecture context | A temporary prompt bundle assembled from knowledge artifacts and documentation for a specific model-assisted task. It is derived input, not a persisted runtime contract. |
| Generation snapshot | The versioned, immutable reuse envelope implemented by generationSnapshot in smrtPlugin() and smrtConsumer(). It carries one merged runtime manifest with portable source paths, source-file digests, and caller-verified provenance; consumers verify its exact bytes and current source contents before selecting the project, dependency, or aggregate view they need. Future schema versions may add more normalized generator inputs or an output inventory without turning runtime/request state into persisted context. |
| Recipe | A declared, user-facing unit of app functionality ("Sales"), smaller than a package: a SmrtRecipe subclass naming models (class references), navigation entries, prerequisite recipe ids, and curation-only options. It is not persisted; the scanner emits it as the top-level recipes array of the runtime manifest and the domain knowledge artifact (#3590). See core's agents/recipes.md. |
| Recipe help | The user-facing Markdown a recipe's static help points to (overview and tasks, steps naming fields as {field:name} or {field:Model.name}), emitted as help: { markdown, fieldRefs } on the recipe entry; fieldRefs is derived at build time. Field text comes from @field({ description }). Hosts render it with core's renderHelp, which follows the app's effective field policies and outputs an AST of text nodes, not HTML (#3591). |
| Runtime or request context | Live dependencies and authority for an operation, such as database, tenant, principal, AI, CLI, REST, or MCP state. It must not be serialized into a generation snapshot. |
| Object context | A SmrtObject instance's context value, paired with its slug as a logical namespace. This is unrelated to prompt or generation context. |
| Learned context memory | Values stored through remember() / recall() in _smrt_contexts. This is mutable runtime data and is unrelated to generated artifacts. |
When a design needs to reuse generated state across processes, name the exact
artifact (runtime manifest, domain knowledge artifact, or generation snapshot) instead of using unqualified context. A runtime manifest alone is
not sufficient proof that its source inputs, generator configuration, or
companion outputs match.
UI metadata vocabulary
The manifest carries three additive, presentation-only facts (#3599). None changes schema, persistence, or security:
| Term | Meaning |
|---|---|
| Widget hint | @field({ ui: { widget } }), one of textarea, currency, email, url, phone; emitted under the field's _meta.ui and validated against the field type at build time. |
| Display label | @smrt({ display: { label } }) names the own, non-sensitive field that labels a record (default: first of name, title, label, code); emitted as displayLabelField and named in generated MCP/WebMCP tool descriptions so assistants can identify records. |
| Selector slot | A ModuleUISlot with selects: '<qualified model>'; emitted as the manifest's uiSelectors. Hosts resolve it with findSelectorFor from @happyvertical/smrt-core/ui-metadata. |
Diagnostics SOP
smrt doctor is the umbrella developer command for project-health diagnostics.
Add focused, composable checks beneath it rather than introducing a generic
smrt validate command; noun-scoped validators such as smrt db:validate keep
their narrower contracts. check and diagnose remain aliases for doctor.
Diagnostics report contract violations; they are not the enforcement boundary.
Every loader, plugin, or generator that consumes a prepared artifact must call
the same verifier directly, perform no scan or write in prepared mode, and fail
closed on missing, stale, incompatible, or unverifiable inputs. smrt doctor
exposes that verifier through its atomic --generation-snapshot* option set for
humans and CI, but a passing doctor run must never be required to make an unsafe
consumer reject invalid state.
Keep checks read-only by default, return actionable evidence, and support machine-readable output when a check is intended for CI. Any future repair mode must identify its exact mutations and remain separate from verification.
Model-assisted knowledge workflow
Use models as optional local reviewers, not as freshness gates:
- Ask
smrt-dev-mcpfor deterministic context withbuild-review-context,smrt-review,build-architecture-context, orsmrt-architecture. - For formal downstream reviews, fetch the portable
smrt-code-reviewprocedure with MCP toolget-agent-skilland follow it before writing findings. - Send the returned prompt bundle to Codex, Claude, or another model under the user's local plan.
- Apply only reviewed changes to source docs or package expertise.
- Re-run
pnpm knowledge:check --strict --format markdownbefore committing.
When an automation needs to consume checker output, use
pnpm knowledge:check --strict --format json. Hooks and CI must stay
token-free; prompt bundles are the boundary between deterministic SMRT tooling
and optional model assistance. Bundled agent skills are procedural wrappers
around that boundary; they must not require a specific model provider or harness.
| Pattern | Source | Action |
|---|---|---|
vite.config.ts.timestamp-*.mjs | Vite config write-cache | Add to root .gitignore; remove existing from agents, content (×2), tags |
vite.config.ts.bak | Manual backups | Remove (currently in profiles, with legacy HappyVertical namespace refs) |
temp-test-manifest-gen-*.ts | Manifest builder driver scripts | Generate to a gitignored path; remove existing from agents, users, ads, affiliates |
*.timestamp-*.mjs | Vite write-cache | Catch-all for vite |
.DS_Store | macOS finder | Add to root .gitignore |
Empty test-*.{js,mjs,ts} files | Dev throwaways | Remove (currently in products) |
woohoo.txt, ASCII-art dumps | Random | Remove |
Binary assets >100KB in src/ or package root | Misplaced | Move to top-level assets/ if needed |
Generated .agents/smrt-framework.md / .claude/smrt-framework.md | smrt docs:agents / compatibility smrt docs:claude output | Generate to consumer projects only; remove from package directories |
Per-package .changeset/ | Changesets at sub-package level | Move to repo root (currently in cli/) |
| Empty config files | Dead | Remove (currently in vitest/vite.config.ts) |
Appendix A: audit snapshot
This is a snapshot of the monorepo as of the standards audit. Numbers reflect non-compliant packages; the per-package issues track resolution.
Headline non-compliance
smrtVitestPlugin()not used in 7 packages: config, types, scanner, tags, social, secrets, voice- Stale build/dev artifacts committed in 8 packages (see §11)
- 6 packages ship with zero tests: affiliates, voice, tags, gnode, template-sveltekit, template-site-static-json
- Templates pin
@happyvertical/smrt-core: ^0.17.0while monorepo is at0.23.11 - Historical packages drifted on
AGENTS.md/CLAUDE.mdshim publishing smrt-playgroundpreviously had no package agent guidance
Drift dimensions
| Dimension | Non-compliant packages |
|---|---|
Hand-written vite.config.ts (not using createPackageConfig) | core, cli, config, types, scanner, products, secrets, content, assets, images, smrt-svelte |
Wrong exports map condition order ({import, types}) | config, cli, types |
| Bare-string export targets | scanner |
@types/node pinned to 24.10.9 (vs catalog 25.0.9) | core, affiliates, prompts, features |
Missing typecheck script | resolved (#1375) — every package ships one except products (carved out in check-standards EXEMPTIONS pending #1370) |
Missing prepack / verify:pack | secrets, sites, properties, social, video, voice, ads, affiliates, ledgers, smrt-svelte, smrt-dev-mcp |
Inconsistent author field | ~all (3 different forms in use) |
Missing repository field | ~39 of 41 |
| Build target inconsistency (mix of es2022/node20/node24/unset) | core, cli, scanner, others unset |
| Test naming convention violations | core (mixes .test/.spec inverted), social (.spec only), places (mixes both) |
Tests outside src/__tests__/ | core, products, places, voice |
| Per-package SPEC/ARCHITECTURE/BRAINSTORM/MIGRATION files | core, agents, tags, places, events, facts, profiles, assets, messages, config, tenancy |
| Triple-consumption scaffolding without need | none (only products has it; this is correct) |
| UI registry pattern not used | assets, images, chat (chat builds the entry but no export declared) |
Svelte peer range ^4.0.0 || ^5.0.0 instead of ^5.18.0 | assets, images |
Per-package open issues threaded into the audit
| Package | Open issues |
|---|---|
| core | #1003 (epic), #1009, #1010, #1011, #1024, #1127, #1139, #972, #1115 |
| cli | #1178, #1085 |
| tenancy | #1112, #1028, #1039 |
| users | #1028, #1039, #1115, #1021, #1136, #1178 |
| jobs | #1115, #1021 |
| agents | #1178, #1021 |
| profiles | #1136, #1178, #1085 |
| content | #1189, #1057 |
| assets | #1057 |
| images | #1022, #1127 |
| commerce, ledgers, ads | #1021, #1178 (cross-cutting) |
| affiliates | #997 |
| products | #1136 |
| analytics | #1021 |
| places | #1152 |
| secrets | #1112 |
| smrt-svelte | #1028, #1021, #1178, #1189 |
Issues likely closeable on this audit pass
- #1136 —
smrt-promptspackage now exists at0.23.11. Close. - #1003 (and children #1009, #1010, #1011) — verify against current state of
core/src/registry.ts. Likely partially complete. - ~13 issues with
stalelabel — triage and close or revive.
Appendix B: rationale for changes
A few of the rules above remove something that's currently in the repo. Justification:
Why kill per-package SPEC.md / ARCHITECTURE.md: They drift. core/ARCHITECTURE.md is 970 lines and has not been updated in line with the registry refactor tracked by #1003. assets/SPEC.md is 473 lines and predates the content_assets ownership migration tracked by #1057. Living architectural documentation belongs in docs/architecture/ where it gets reviewed alongside code changes, not buried in package directories where readers don't think to look.
Why force repository.directory: GitHub renders package READMEs from this field. Without it, packages on the GitHub registry show no source link and contributors cannot navigate from the registry to the code.
Why exports condition order matters: Node's resolution picks the first matching condition. If a consumer imports the package and import matches before types, TypeScript silently falls back to dist/index.js for type info, which provides no types. The bug is invisible until a downstream user complains about losing autocomplete.
Why mandate createPackageConfig: Hand-written vite configs duplicate ~80 lines of boilerplate per package and drift independently. Build targets, externals, and DTS settings that should be uniform are not. The base config is the only viable lever for a coordinated change (e.g. swapping the bundler).
Why smrtVitestPlugin() is non-negotiable: It generates the manifest at vitest startup. Without it, tests pass that should fail (because cross-package classes aren't loaded) and tests fail with "No field metadata" for reasons that look like a bug in user code. The framework's own foundation packages currently violate this; they should be the first to fix it.
Declaration packaging
TypeScript 6.0.3 emits source-shaped .d.ts and declaration maps. The build
extends each package's build config (or ordinary tsconfig), disables workspace
source aliases, and resolves dependency types through public built exports.
The shared emitter normalizes relative TypeScript module references to explicit
runtime suffixes (.js, .mjs, or .cjs) and resolves directory imports to
their index files during declaration emit. TypeScript generates declaration maps
from that transformed tree, keeping navigation aligned with the original source.
Explicit runtime suffixes, asset references such as .svelte and .json, and
bare package imports retain their specifiers. Each package must declare
dependencies used by its published types. The
compiler gets explicit Node types; path mappings use relative ./ targets
without deprecated baseUrl.
createPackageConfig creates type re-export entry files for named JS entries
whose source path differs. Svelte components still use svelte-package;
application-only routes and generated application registration are excluded
from library declaration inputs. Ambient declarations remain compiler inputs;
global augmentations stay in their original modules with the imports they need.
Validate packaging with node scripts/verify-package-types-exports.js <package>
and real packed-consumer TypeScript checks with skipLibCheck: false. The
emitter regression tests run in pnpm test:ci-scripts; packed consumers must
exercise both documented ESNext/Bundler and NodeNext resolution, including
inherited public methods and assertions that reject accidental any types.
After pnpm build, run node scripts/verify-packed-declarations.mjs to compile
real core, profiles, inventory, and their workspace dependency tarballs in both
consumer modes with declaration checking enabled. The fixture installs those
tarballs without links to producer dependencies or undeclared development types.
Packing runs normal lifecycle hooks in a separate temporary producer workspace,
so a prepack rebuild cannot delete live build outputs used by parallel tests.
Selected workspace dependency links resolve to copied producers; installed external tools
are reused without permitting pnpm to relink the live installation. The consumer
remains separate, with no ancestor node_modules or producer dependency links.
Set SMRT_PACKED_EVIDENCE_DIR to retain prepack logs, install metadata and tarball evidence.
SDK declaration portability requires the repair released in SDK v0.102.1
(SDK #1285), which the workspace
now selects. The isolated consumer preserves the project's public registry
routing without copying credentials. SMRT_PACKED_OVERRIDES may name a JSON file
of explicit candidate SDK tarball overrides for integration testing; that proof
does not establish support for the default published dependency graph. Passing
emitter fixtures alone is also insufficient.