SMRT Monorepo Package Standards
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 | vite-plugin-dts 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,vite-plugin-dts,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 always
svelte: ^5.18.0for packages shipping UI - Optional peers explicitly marked in
peerDependenciesMeta - Required peers (e.g.
usersrequiresprofiles) documented inAGENTS.md
- Svelte peer always
publishConfig.registryalwayshttps://registry.npmjs.orgfor published packages, withaccess: "public"
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. vite-plugin-dts: comes viavite.config.base.ts; do not add to per-package devDependencies.- DTS bundling:
rollupTypes: falsefor foundation packages (core,cli) — many internal typesrollupTypes: truefor narrow public APIs
- 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 | Settings for vite-plugin-dts build path |
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 ifvite-plugin-dtsneeds 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
- 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. - 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 conflictColumnsset on junction/upsert tables- System tables prefixed
_smrt_ - JSON fields stored as strings with
getX()/setX()helpers wrapped intry/catch
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 (e.g.
protobufjs6.x held byonnx-protounder the deprecated@xenova/transformersv2 fallback) add its GHSA toauditConfig.ignoreGhsasinpnpm-workspace.yaml— with a justification recorded in the PR. 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.)./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:
svelte: ^5.18.0(uniform). Drop the^4.0.0 || ^5.0.0range. - Build script:
vite build && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json - 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.
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. 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. |
| 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.
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.