Skip to main content

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

  1. Repository layout
  2. package.json
  3. Build configuration
  4. TypeScript configuration
  5. Testing
  6. Documentation
  7. Code conventions
  8. UI packaging (Svelte)
  9. Triple-consumption packages
  10. Templates
  11. Forbidden artifacts
  12. Appendix A: audit snapshot
  13. 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

FileWhen required
src/__smrt-register__.tsPackage defines @smrt() classes (issue #1132 self-registration pattern)
src/svelte/ and ambient.d.ts and tsconfig.svelte.jsonPackage ships Svelte UI components
src/manifest/Package participates in build-time manifest generation
tsconfig.build.jsonvite-plugin-dts needs different inclusions than tsc --noEmit
tsconfig.typecheck.jsontsc --noEmit needs different inclusions than build
bin/Package exposes a CLI binary
e2e/ and playwright.config.tsPackage 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" — always
  • author: "HappyVertical" — uniform; replace "HAVE Team", "Will Griffin <willgriffin@gmail.com>", and missing fields
  • repository.directory — required
  • files["dist", "AGENTS.md", "CLAUDE.md"]. Add "bin" if package has a CLI. Do not add README.md (it is published automatically). Do not add directories that don't exist.
  • Exports map condition order: {types, import}types must come first (Node resolves the first matching condition; if import is first, types resolution silently fails). Bare-string targets are forbidden — every entry must be a conditional object.
  • Dependencies:
    • All @happyvertical/smrt-* references use workspace:*
    • All @happyvertical/sdk references (ai, sql, files, utils, cache, documents, email, encryption, geo, images, jobs, json, logger, messages, ocr, pdf, projects, repos, secrets, spider) use catalog:
    • @types/node always catalog:
    • vite, vitest, vite-plugin-dts, typescript come 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)
  • Coordinated releases: every publishable @happyvertical/smrt-* package belongs to the fixed release group in .changeset/config.json and carries the same version as that group. scripts/check-standards.mjs enforces both invariants before publish validation.
  • Scripts: every package has build, build:watch, dev, clean, test, test:watch, typecheck, prepack, verify:pack. No lint or format scripts — those are root-level Biome tasks only (pnpm lint / biome ci / pnpm format-check), which already gate every package on PRs; scripts/check-standards.mjs forbids per-package lint/lint:fix/format/format-check scripts so drift back to them is caught (S2, #1374). The presence of typecheck is likewise enforced by scripts/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 scaffolded template/package.json (see §10).
  • peerDependencies:
    • Svelte peer always svelte: ^5.18.0 for packages shipping UI
    • Optional peers explicitly marked in peerDependenciesMeta
    • Required peers (e.g. users requires profiles) documented in AGENTS.md
  • publishConfig.registry always https://registry.npmjs.org for published packages, with access: "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 createPackageConfig from vite.config.base.ts. Hand-written vite.config.ts files require an explicit comment stating why and a tracking issue.
  • Build target: es2022 for libraries; node20 only for tools that must run server-side (CLIs); node24 only when explicitly required.
  • vite-plugin-dts: comes via vite.config.base.ts; do not add to per-package devDependencies.
  • DTS bundling:
    • rollupTypes: false for foundation packages (core, cli) — many internal types
    • rollupTypes: true for narrow public APIs
  • Output format: ESM only (formats: ['es'])
  • Sourcemaps: on
  • vitest package is exempt — it must build with tsc because it provides the vite plugin to others. The empty vite.config.ts should be removed.

4. TypeScript configuration

Base configs at repo root

BasePurpose
tsconfig.jsonDefault for libraries — strict, ES2022, ESNext modules, bundler resolution
tsconfig.package-build.jsonSettings for vite-plugin-dts build path
tsconfig.package-svelte.jsonSettings for packages with .svelte source
tsconfig.package-typecheck.jsonSettings for tsc --noEmit
tsconfig.package-ui.jsonSettings for packages shipping UI (extends svelte + adds DOM lib)

Per-package rules

  • Always have tsconfig.json extending the appropriate base
  • Add tsconfig.svelte.json only if .svelte source is present
  • Add tsconfig.typecheck.json only if typecheck inclusions differ from build
  • Add tsconfig.build.json only if vite-plugin-dts needs different inclusions
  • Maximum: 4 tsconfig files per package. If you need more, talk to maintainers first.
  • Do not extend tsconfig.kit.json unless 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: false already pins the pool to a single worker — Vitest overrides any user-supplied maxWorkers in that case, so do not add maxWorkers. The poolOptions.forks.singleFork option was removed in Vitest 4, and a flat test.singleFork was never valid at any version; a stray singleFork key 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 smrtVitestPlugin directly 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 scaffold works end-to-end.
  • testTimeout: 30s default; raise per-test if a specific case needs longer; raise the package default only with documented reason (users is at 60s for legitimate reasons).

6. Documentation

Required

  • README.md — ≥80 lines. Purpose, install, basic usage, link to AGENTS.md, links to other relevant package READMEs.
  • AGENTS.md — ≥30 lines. Package-specific patterns, gotchas, integration points. Included in files: allowlist.
  • CLAUDE.md — exactly one line: @AGENTS.md. Included in files: 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.md
  • SPEC.md
  • MIGRATION.md
  • BRAINSTORM.md
  • AUTO_POPULATE_GUIDE.md
  • SECRETS_MIGRATION.md
  • TEMPLATE_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(). Use transformJSON() instead. The base class handles STI discriminator and meta-field extraction.
    • Exception: tenancy/interceptor.ts calls instance.toJSON() directly to handle stub instances. This must be documented in the file with a comment.
  • 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 SmrtJunction and expose byLeft() / byRight() plus options-object attach() / 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, images for some models) must document the tenant strategy in AGENTS.md.
  • __smrt-register__.ts self-registration imported from index.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
  • conflictColumns set on junction/upsert tables
  • System tables prefixed _smrt_
  • JSON fields stored as strings with getX()/setX() helpers wrapped in try/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 cli package'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-commitgitleaks git --staged scans 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 with brew install gitleaks.
  • CI (on-pull-request) — the Secret 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-requestDependency 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.yaml under overrides (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. protobufjs 6.x held by onnx-proto under the deprecated @xenova/transformers v2 fallback) add its GHSA to auditConfig.ignoreGhsas in pnpm-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 flat src/svelte/)
  • svelte/index.ts: uses ModuleUIRegistry.register(...) pattern. Pure re-exports without registry are deprecated.
  • ./ui subpath: exports MODULE_META and UI_SLOTS constants. The vite config builds a ui entry; the package.json must declare the matching export. (chat currently has the entry without the export.)
  • ./playground subpath: exports the package's playground module for use by smrt-playground
  • ./workbench subpath: exports package-owned routes and workbench metadata for use by smrt-workbench
  • Svelte peer: svelte: ^5.18.0 (uniform). Drop the ^4.0.0 || ^5.0.0 range.
  • Build script: vite build && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json
  • Typecheck script: packages with ./svelte exports 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 run svelte-kit sync before both the TypeScript pass and the svelte-check-a11y wrapper pass.
  • tsconfig.svelte.json: extends tsconfig.package-svelte.json, includes ambient.d.ts and *.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.mjs is a drop-in svelte-check wrapper that appends --compiler-warnings with an explicit comma-separated list of every a11y_* code mapped to :error (svelte-check's native warning-promotion flag — there is no wildcard syntax, so each code is listed). Every UI package's typecheck calls the wrapper instead of bare svelte-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=0 runs plain svelte-check without the promotion (local debugging only; CI always enforces).
  • Remediation pairs with the gate: the deterministic gate catches what the compiler can see; component-test axe assertions (S11 harness) cover the rest. As of S12 the repo is a11y-clean repo-wide (0 compiler a11y warnings).
  • Waiver — products: its typecheck is npm run generate && tsc (no svelte-check, a pre-existing #1370/#1375 carve-out for its mixed app-mode entrypoints), so its .svelte files are not a11y-gated. They are a11y-clean today (verified via standalone svelte-check); wiring svelte-check into the products typecheck is tracked with the rest of #1370.

9. Triple-consumption packages

products is the reference template for packages that need to ship as:

  1. npm library (consumed via import)
  2. Module Federation host
  3. 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 (not createPackageConfig)
  • Document in its AGENTS.md why 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.json always private: true
  • template/package.json and template.config.js SMRT versions match the monorepo's current major (do not pin to ^0.17.0 while monorepo is at 0.23.x)
  • All files referenced in template/package.json scripts must actually exist in template/
  • template/README.md ≥100 lines, explaining the scaffolding flow and runtime parameters
  • CI verifies that pnpm install && pnpm build succeeds in each scaffolded template (would have caught the template-site-static-json missing-caelus.ts bug)
  • Scaffolded SvelteKit templates must ship typecheck as a TypeScript pass plus svelte-check; otherwise generated projects inherit the .svelte ambient 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), plus cross-package when the package declares a @happyvertical/smrt-* dependency.
  • risks: sensitive-fields-excluded (generation always strips sensitive fields — see sensitiveFieldsExcluded above), cross-package-refs:<n> when relationshipsV2.crossPackageRefFields > 0, sti-inheritance when any object uses tableStrategy: 'sti', and polymorphic-associations:<n> when `relationshipsV2.polymorphicAssociations

    0`.

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 typeDerived from
crossPackageRefAn object field with type: 'crossPackageRef'; to resolves to the target object's node id when a scanned package declares it.
stiAn object with tableStrategy: 'sti' and extends set; to is the parent object.
junction / hierarchical / polymorphicrelationshipFeatures containing SmrtJunction / SmrtHierarchical / SmrtPolymorphicAssociation.
systemTableAn 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":

TermMeaning
Source modelAuthored TypeScript classes, decorators, and SMRT configuration. This is the source of truth.
Runtime manifestThe 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 artifactsmrt-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 contextA 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 snapshotThe 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 contextLive 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 contextA SmrtObject instance's context value, paired with its slug as a logical namespace. This is unrelated to prompt or generation context.
Learned context memoryValues 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:

  1. Ask smrt-dev-mcp for deterministic context with build-review-context, smrt-review, build-architecture-context, or smrt-architecture.
  2. For formal downstream reviews, fetch the portable smrt-code-review procedure with MCP tool get-agent-skill and follow it before writing findings.
  3. Send the returned prompt bundle to Codex, Claude, or another model under the user's local plan.
  4. Apply only reviewed changes to source docs or package expertise.
  5. Re-run pnpm knowledge:check --strict --format markdown before 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.

PatternSourceAction
vite.config.ts.timestamp-*.mjsVite config write-cacheAdd to root .gitignore; remove existing from agents, content (×2), tags
vite.config.ts.bakManual backupsRemove (currently in profiles, with legacy HappyVertical namespace refs)
temp-test-manifest-gen-*.tsManifest builder driver scriptsGenerate to a gitignored path; remove existing from agents, users, ads, affiliates
*.timestamp-*.mjsVite write-cacheCatch-all for vite
.DS_StoremacOS finderAdd to root .gitignore
Empty test-*.{js,mjs,ts} filesDev throwawaysRemove (currently in products)
woohoo.txt, ASCII-art dumpsRandomRemove
Binary assets >100KB in src/ or package rootMisplacedMove to top-level assets/ if needed
Generated .agents/smrt-framework.md / .claude/smrt-framework.mdsmrt docs:agents / compatibility smrt docs:claude outputGenerate to consumer projects only; remove from package directories
Per-package .changeset/Changesets at sub-package levelMove to repo root (currently in cli/)
Empty config filesDeadRemove (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

  1. smrtVitestPlugin() not used in 7 packages: config, types, scanner, tags, social, secrets, voice
  2. Stale build/dev artifacts committed in 8 packages (see §11)
  3. 6 packages ship with zero tests: affiliates, voice, tags, gnode, template-sveltekit, template-site-static-json
  4. Templates pin @happyvertical/smrt-core: ^0.17.0 while monorepo is at 0.23.11
  5. Historical packages drifted on AGENTS.md / CLAUDE.md shim publishing
  6. smrt-playground previously had no package agent guidance

Drift dimensions

DimensionNon-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 targetsscanner
@types/node pinned to 24.10.9 (vs catalog 25.0.9)core, affiliates, prompts, features
Missing typecheck scriptresolved (#1375) — every package ships one except products (carved out in check-standards EXEMPTIONS pending #1370)
Missing prepack / verify:packsecrets, 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 violationscore (mixes .test/.spec inverted), social (.spec only), places (mixes both)
Tests outside src/__tests__/core, products, places, voice
Per-package SPEC/ARCHITECTURE/BRAINSTORM/MIGRATION filescore, agents, tags, places, events, facts, profiles, assets, messages, config, tenancy
Triple-consumption scaffolding without neednone (only products has it; this is correct)
UI registry pattern not usedassets, images, chat (chat builds the entry but no export declared)
Svelte peer range ^4.0.0 || ^5.0.0 instead of ^5.18.0assets, images

Per-package open issues threaded into the audit

PackageOpen 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-prompts package now exists at 0.23.11. Close.
  • #1003 (and children #1009, #1010, #1011) — verify against current state of core/src/registry.ts. Likely partially complete.
  • ~13 issues with stale label — 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.