@aventara/cli 1.0.0-rc.0 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,8 @@ import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
3
3
  import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
4
4
  import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
5
5
  import { APP_MODULE, } from "../project/project.inspector.js";
6
- import { SCAFFOLD_ENTRYPOINT, } from "../templates/template.registry.js";
6
+ import { planAgentSkill } from "../skill/agent-skill.planner.js";
7
+ import { SCAFFOLD_API, } from "../templates/template.registry.js";
7
8
  export const PREPARE_SCRIPT = "aventara:prepare";
8
9
  const PREPARED_SCRIPTS = [
9
10
  "build",
@@ -113,9 +114,7 @@ export function planInit(input) {
113
114
  keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
114
115
  replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
115
116
  const spec = input.read(E2E_SPEC);
116
- const tested = spec === undefined
117
- ? undefined
118
- : spliceContractTest(spec, SCAFFOLD_ENTRYPOINT);
117
+ const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
119
118
  if (tested !== undefined) {
120
119
  keeping.push(...writeOf(E2E_SPEC, spec, tested));
121
120
  replacing.push(...writeOf(E2E_SPEC, spec, tested));
@@ -195,6 +194,14 @@ export function planInit(input) {
195
194
  const workspace = input.read("pnpm-workspace.yaml");
196
195
  both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
197
196
  }
197
+ const skill = planAgentSkill({
198
+ read: input.read,
199
+ coreVersion: input.aventaraVersion,
200
+ force: false,
201
+ });
202
+ keeping.push(...skill.writes);
203
+ replacing.push(...skill.writes);
204
+ warnings.push(...skill.warnings);
198
205
  const run = packageManager === "pnpm" ? "pnpm" : "npm";
199
206
  return {
200
207
  keeping,
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { lowestMajorOf } from "../catalog/range.reader.js";
4
- export class ProjectRefusedError extends Error {
5
- name = "ProjectRefusedError";
4
+ export class AvProjectRefusedError extends Error {
5
+ name = "AvProjectRefusedError";
6
6
  }
7
7
  const NEST_MAJOR = 12;
8
8
  const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
@@ -23,32 +23,32 @@ function matchesFamily(pattern, name) {
23
23
  export function inspectProject(directory, catalog) {
24
24
  const manifestPath = path.join(directory, "package.json");
25
25
  if (!existsSync(manifestPath)) {
26
- throw new ProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and there is no package.json in ${directory}`);
26
+ throw new AvProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and there is no package.json in ${directory}`);
27
27
  }
28
28
  const manifestText = readFileSync(manifestPath, "utf8");
29
29
  const manifest = JSON.parse(manifestText);
30
30
  const nest = dependencyRange(manifest, "@nestjs/core");
31
31
  if (nest === undefined) {
32
- throw new ProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and this package.json has no @nestjs/core dependency`);
32
+ throw new AvProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and this package.json has no @nestjs/core dependency`);
33
33
  }
34
34
  if (lowestMajorOf(nest) !== NEST_MAJOR) {
35
- throw new ProjectRefusedError(`aventara init supports NestJS ${NEST_MAJOR} only, and this project depends on @nestjs/core ${nest}`);
35
+ throw new AvProjectRefusedError(`aventara init supports NestJS ${NEST_MAJOR} only, and this project depends on @nestjs/core ${nest}`);
36
36
  }
37
37
  const typescript = dependencyRange(manifest, "typescript");
38
38
  const typescriptMajor = typescript === undefined ? undefined : lowestMajorOf(typescript);
39
39
  if (typescriptMajor !== undefined &&
40
40
  typescriptMajor >= FIRST_UNSUPPORTED_TYPESCRIPT) {
41
- throw new ProjectRefusedError(`this project declares typescript ${typescript}, and TypeScript ${FIRST_UNSUPPORTED_TYPESCRIPT} has no classic compiler API for Aventara's generate steps to read types with; use TypeScript 6 or 5`);
41
+ throw new AvProjectRefusedError(`this project declares typescript ${typescript}, and TypeScript ${FIRST_UNSUPPORTED_TYPESCRIPT} has no classic compiler API for Aventara's generate steps to read types with; use TypeScript 5.7 to 6`);
42
42
  }
43
43
  const lockfiles = LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
44
44
  const known = lockfiles.filter((manager) => manager !== "yarn");
45
45
  if (lockfiles.includes("yarn") && known.length === 0) {
46
- throw new ProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
46
+ throw new AvProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
47
47
  }
48
48
  const appModule = path.join(directory, APP_MODULE);
49
49
  if (existsSync(appModule) &&
50
50
  /\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
51
- throw new ProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
51
+ throw new AvProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
52
52
  }
53
53
  const dependencies = [
54
54
  ...Object.keys(manifest.dependencies ?? {}),
@@ -4,7 +4,7 @@ import { confirmConflicts } from "../apply/conflict.confirmer.js";
4
4
  import { matchInstalledOrm, matchOrmSetup, } from "../catalog/catalog.matcher.js";
5
5
  import { planInit } from "../plan/project.planner.js";
6
6
  import { detectPackageManager } from "../project/package-manager.detector.js";
7
- import { inspectProject, ProjectRefusedError, } from "../project/project.inspector.js";
7
+ import { AvProjectRefusedError, inspectProject, } from "../project/project.inspector.js";
8
8
  import { scanServices } from "../project/service.detector.js";
9
9
  import { templatesFor, } from "../templates/template.registry.js";
10
10
  import { resolveAnswers } from "../wizard/answer.resolver.js";
@@ -13,8 +13,8 @@ import { createStepReporter, } from "./step.reporter.js";
13
13
  export function printNextSteps(stdout, steps) {
14
14
  stdout(`Next:\n${steps.map((step, index) => ` ${index + 1}. ${step}`).join("\n")}\n`);
15
15
  }
16
- export class InstallFailedError extends Error {
17
- name = "InstallFailedError";
16
+ export class AvInstallFailedError extends Error {
17
+ name = "AvInstallFailedError";
18
18
  }
19
19
  export async function runInit(command, io) {
20
20
  const { progress, nextSteps } = await initProject(command, io);
@@ -88,7 +88,7 @@ export async function initProject(command, io) {
88
88
  onOutput: progress.output,
89
89
  });
90
90
  if (installed.code !== 0) {
91
- throw new InstallFailedError(`\`${packageManager} install\` exited with code ${installed.code} after the files were written; fix what it reports and run \`${packageManager} install\` again:\n${installed.stderr.trimEnd()}`);
91
+ throw new AvInstallFailedError(`\`${packageManager} install\` exited with code ${installed.code} after the files were written; fix what it reports and run \`${packageManager} install\` again:\n${installed.stderr.trimEnd()}`);
92
92
  }
93
93
  });
94
94
  }
@@ -144,25 +144,25 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
144
144
  installCommand: `${detectedPackageManager} install`,
145
145
  });
146
146
  if (verdict.kind === "refused") {
147
- throw new ProjectRefusedError(verdict.sentence);
147
+ throw new AvProjectRefusedError(verdict.sentence);
148
148
  }
149
149
  const { entry } = verdict;
150
150
  const templates = templatesFor(entry);
151
151
  const facts = templates.readProject(io.directory, inspection.manifest);
152
152
  const problem = matchOrmSetup(entry, facts);
153
153
  if (problem !== undefined) {
154
- throw new ProjectRefusedError(problem);
154
+ throw new AvProjectRefusedError(problem);
155
155
  }
156
156
  const provider = facts.provider;
157
157
  if (facts.clientOutput === undefined) {
158
- throw new ProjectRefusedError(`this project's ${facts.generator} generator block in ${facts.schema} has no output, and the generate step reads the client from it; give it one, such as ../src/generated/${entry.orm.family}`);
158
+ throw new AvProjectRefusedError(`this project's ${facts.generator} generator block in ${facts.schema} has no output, and the generate step reads the client from it; give it one, such as ../src/generated/${entry.orm.family}`);
159
159
  }
160
160
  for (const [flag, given, found] of [
161
161
  ["--orm", command.given.orm, entry.alias],
162
162
  ["--db", command.given.db, provider],
163
163
  ]) {
164
164
  if (given !== undefined && given.trim().toLowerCase() !== found) {
165
- throw new ProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
165
+ throw new AvProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
166
166
  }
167
167
  }
168
168
  const scan = scanServices(io.directory, templates.clientShape(facts));
@@ -2,10 +2,10 @@ import { createHash } from "node:crypto";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { detectPackageManager } from "../project/package-manager.detector.js";
5
- import { ProjectRefusedError } from "../project/project.inspector.js";
5
+ import { AvProjectRefusedError } from "../project/project.inspector.js";
6
6
  import { resolveAnswers } from "../wizard/answer.resolver.js";
7
7
  import { NEW_QUESTIONS } from "../wizard/wizard.questions.js";
8
- import { InstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
8
+ import { AvInstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
9
9
  import { commitScaffold, gitSurroundings } from "./scaffold.committer.js";
10
10
  import { createStepReporter } from "./step.reporter.js";
11
11
  export const NEST_CLI = "@nestjs/cli@12.0.8";
@@ -34,7 +34,7 @@ export async function runNew(command, io) {
34
34
  }
35
35
  const directory = path.join(io.cwd, name);
36
36
  if (existsSync(directory) && readdirSync(directory).length > 0) {
37
- throw new ProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
37
+ throw new AvProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
38
38
  }
39
39
  const surroundings = command.skipGit
40
40
  ? undefined
@@ -63,7 +63,7 @@ export async function runNew(command, io) {
63
63
  onOutput: progress.output,
64
64
  });
65
65
  if (created.code !== 0) {
66
- throw new InstallFailedError(`\`${runner} ${args.join(" ")}\` exited with code ${created.code}, and nothing of Aventara's was written; fix what it reports and run aventara new again:\n${created.stderr.trimEnd()}`);
66
+ throw new AvInstallFailedError(`\`${runner} ${args.join(" ")}\` exited with code ${created.code}, and nothing of Aventara's was written; fix what it reports and run aventara new again:\n${created.stderr.trimEnd()}`);
67
67
  }
68
68
  });
69
69
  const appModule = path.join(directory, "src", "app.module.ts");
@@ -71,7 +71,7 @@ export async function runNew(command, io) {
71
71
  ? createHash("sha256").update(readFileSync(appModule)).digest("hex")
72
72
  : "(absent)";
73
73
  if (digest !== NEST_APP_MODULE_SHA256) {
74
- throw new InstallFailedError(`nest new's output changed: ${appModule} is not what ${NEST_CLI} writes (sha256 ${digest}), so nothing of Aventara's was written over it; run aventara init inside ${directory} to add Aventara to it`);
74
+ throw new AvInstallFailedError(`nest new's output changed: ${appModule} is not what ${NEST_CLI} writes (sha256 ${digest}), so nothing of Aventara's was written over it; run aventara init inside ${directory} to add Aventara to it`);
75
75
  }
76
76
  const { nextSteps } = await initProject({
77
77
  command: "init",
@@ -0,0 +1,54 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { planAgentSkill } from "../skill/agent-skill.planner.js";
4
+ export class AvSkillRefusedError extends Error {
5
+ name = "AvSkillRefusedError";
6
+ }
7
+ function readText(file) {
8
+ try {
9
+ return readFileSync(file, "utf8");
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ export function runSkill(command, io) {
16
+ if (readText(path.join(io.directory, "package.json")) === undefined) {
17
+ throw new AvSkillRefusedError(`there is no package.json in ${io.directory}: run aventara skill in your project's root`);
18
+ }
19
+ const core = path.join(io.directory, "node_modules", "@aventara", "core");
20
+ const manifest = readText(path.join(core, "package.json"));
21
+ if (manifest === undefined) {
22
+ throw new AvSkillRefusedError("@aventara/core is not installed here: install the project's dependencies first, so the skill matches its version");
23
+ }
24
+ const coreVersion = JSON.parse(manifest)
25
+ .version;
26
+ const errors = readText(path.join(core, "ERRORS.md"));
27
+ const warnings = [];
28
+ if (coreVersion !== io.cliVersion) {
29
+ warnings.push(`this is @aventara/cli ${io.cliVersion} and the project has @aventara/core ${coreVersion}; for guidance written for that version run \`npx @aventara/cli@${coreVersion} skill\``);
30
+ }
31
+ if (errors === undefined) {
32
+ warnings.push(`@aventara/core ${coreVersion} ships no ERRORS.md, so reference/errors.md is this CLI's (${io.cliVersion})`);
33
+ }
34
+ const plan = planAgentSkill({
35
+ read: (file) => readText(path.join(io.directory, file)),
36
+ coreVersion,
37
+ ...(errors === undefined ? {} : { errors }),
38
+ force: command.force,
39
+ });
40
+ for (const write of plan.writes) {
41
+ const target = path.join(io.directory, write.path);
42
+ mkdirSync(path.dirname(target), { recursive: true });
43
+ writeFileSync(target, write.content);
44
+ }
45
+ for (const warning of [...warnings, ...plan.warnings]) {
46
+ io.stderr(`aventara: warning: ${warning}.\n`);
47
+ }
48
+ io.stdout(plan.writes.length === 0
49
+ ? `aventara: the agent skill for @aventara/core ${coreVersion} is up to date${plan.warnings.length === 0 ? "" : ", apart from what the warnings above name"}.\n`
50
+ : `aventara: wrote the agent skill for @aventara/core ${coreVersion}: ${plan.writes
51
+ .map((write) => `${write.change === "create" ? "created" : "updated"} ${write.path}`)
52
+ .join(", ")}.\n`);
53
+ return 0;
54
+ }
@@ -0,0 +1,7 @@
1
+ export const AGENT_SKILL_GUIDANCE = {
2
+ "SKILL.md": "---\nname: aventara\ndescription: Build and change this project's Aventara API (NestJS + Prisma, @aventara/core). Use when editing src/aventara.config.ts, src/resources/*.resource.ts or prisma/schema.prisma; when exposing or hiding Resources, fields or operations (exposed, exemptions); when adding guards, pipes, field validators (AvZ), computed fields (compute.create/update/read), hooks, interceptors, filters, transactions, forceTransaction, an outbox, ctx.$state or the agent layer; and when an Aventara code appears (A2004, V1000, CONFIG_*, COMPILER_*, AvConstructionError, AvValidationError).\n---\n\n# Aventara\n\nAventara turns the Prisma schema into a typed API: every model is a Resource with standard operations (`find`, `create`, `update`, `delete`, `upsert`), served at `api` (`/api`) and called from the generated frontend client. You configure it; you do not write controllers. Written for `@aventara/core` {{CORE_VERSION}}.\n\nThere is one way to do each task. Follow these rules; the reference files hold the detail:\n\n- `reference/config.md`: the configuration tree, where every key goes, exposure, limits.\n- `reference/stages.md`: which stage fits a task, the order stages run in, the rules.\n- `reference/recipes.md`: complete, type-checked examples for the common tasks.\n- `reference/errors.md`: every code (`A…`, `V…`, `CONFIG_…`, `COMPILER_…`) with its cause and fix.\n\n## Where things are\n\n| File | What it is |\n|---|---|\n| `prisma/schema.prisma` | The data model. Every model is a Resource, named exactly as the model. |\n| `src/aventara.config.ts` | `aventaraConfig(prisma)` returns the configuration `as const`. Keep the `as const`. |\n| `src/resources/<name>.resource.ts` | One Resource's configuration, written with `defineResource`, spread into `resources`. |\n| `src/generated/` | Generated by `aventara:prepare` (Prisma client and `discovery.artifact.ts`). Never edit it. |\n| `src/app.module.ts` | `AventaraModule.forRootAsync(...)` builds the framework from `aventaraConfig`. |\n\n## Rules\n\n1. **Change the schema, then prepare.** After editing `prisma/schema.prisma` run your migration (`npx prisma migrate dev` or `npx prisma db push`), then `npm run aventara:prepare`. Run `aventara:prepare` again after adding, moving or removing a `compute` stage; the server warns `generated types are stale` until you do. Restart the server and regenerate the frontend client (`npm run avclient:generate` in the frontend): the contract hash changed.\n2. **Configure, never wrap.** Do not write a Nest controller, service or REST route for a model. Every model already has its operations; shape them with `exposed`/`exemptions` and pipelines. Server code calls `framework.application.<Resource>.<family>.<variant>(args)`.\n3. **Expose with exemptions.** `exposed: true` exposes everything the adapter offers and `exemptions` lists what to hide; `exposed: false` exposes nothing and `exemptions` lists what to expose. Hide at the root what no caller may reach; hide in `client` what only your server code may reach. An overlay (`application`, `client`, `agent`) only narrows the root's view: to expose something in one overlay only, expose it at the root and hide it in the other.\n4. **Pick the stage from the table below.** Each task has one stage. Do not authorize in an interceptor, transform in a guard, or write other rows from a compute stage.\n5. **Never cast types away.** No `as`, no `!`, no `any`, no `@ts-ignore` / `@ts-expect-error` to silence an Aventara type. The types are the contract: a type error names the exact key that is wrong (a misspelled field, an overlay granting what the root hid, a missing required field). Fix the key. To get typed arguments in a stage, register it on the operation (`resources.<R>.operations.<family>.<variant>.pipelines`) inside a `defineResource` block; to type `ctx.<Resource>` calls, give the stage type your model (`AvAfterHook<Model>`).\n6. **Annotate stages in the `as const` config.** An inline arrow in `aventaraConfig()` has no contextual type (TS7006). Declare it with its stage type (`const requireUser: AvGuard = (ctx) => …`) or move it into a `defineResource` block, where stages are typed from where they sit.\n7. **Fill server-side values with `compute.create`.** A required field the caller must not send (an owner id, a slug, a hash) gets `pipelines.compute.create`; that also makes it optional on create. A pipe cannot fill a missing required field: the framework refuses the create (`A2004`, `V1000`) before any pipe runs. Hiding a required field from a layer that still creates the Resource needs a `compute.create`, or startup fails with `COMPILER_HIDDEN_REQUIRED_FIELD`.\n8. **Validate values with `AvZ`.** Length, range, pattern and format rules are field validators (`AvZ.maxLength(80)`, `AvZ.email()`); they are part of the contract, so the generated client checks them before sending. A rule across fields or one that needs the database is a guard.\n9. **Read the user from `ctx.$state`.** Declare it once (`declare module \"@aventara/core\" { interface AvState { user?: … } }`), set it from `AventaraModule`'s `state` option (remote requests) or a call's `{ state }` option (server code), and read `ctx.$state.user` in every stage. Never parse headers again in each stage.\n10. **Side effects that must commit with the write: `forceTransaction` + an `after` hook.** Set `forceTransaction: true` on the operation and write the related rows (an outbox event, a counter) through `ctx.<Resource>` or `ctx.$sql` in its `after` hook. Without it the write has already committed when the hook runs.\n11. **Errors are codes.** Branch on `code` or the error class (`AvNotFoundError`, `AvValidationError`, …), never on a message. Throw `new AvError(\"A4000\", \"Sign in first.\")` from a stage to answer a code. Look up any code in `reference/errors.md` (or `AvErrorCatalog[code]` from `@aventara/core`); a startup diagnostic carries its `docs` URL.\n12. **Do not edit generated files.** `src/generated/` and the frontend's generated client are rebuilt by `aventara:prepare` and `avclient generate`.\n\n## Which stage\n\n| You want to | Use | Where it goes |\n|---|---|---|\n| Check a permission | a guard | `pipelines.guards`, at the layer, a Resource, a family or a variant |\n| Refuse a write to one field | a field write guard | `resources.<R>.fields.<f>.pipelines.guards` |\n| Normalize input (trim, lower-case, default, scope a query) | a pipe, or a field pipe for one value | `pipelines.pipes`, or `resources.<R>.fields.<f>.pipelines.pipes` |\n| Enforce a rule on a value (length, range, pattern, format) | a validator | `resources.<R>.fields.<f>.pipelines.validators` |\n| Fill in or derive a field's value | compute | `resources.<R>.fields.<f>.pipelines.compute` (`create`, `update`, `read`) |\n| React after a write (event, audit row, counter) | an `after` hook writing through `ctx`, with `forceTransaction` when it must commit with the write | `pipelines.hooks.after` |\n| Wrap execution (timing, caching, retries, `AsyncLocalStorage`) | an interceptor | `pipelines.interceptors` |\n| Turn an error into an answer | a filter | `pipelines.filters` |\n| Check a whole transaction | the Resource's transaction stages | `resources.<R>.transactions.pipelines` |\n| Share request information (the signed-in user) | `ctx.$state` | `AventaraModule`'s `state` option, or a call's `{ state }` |\n| Call another operation from a stage | `ctx.<Resource>.<family>.<variant>(args)` | the `ctx` every stage gets (compute can only read) |\n\nOrder: framework validation → guards → pipes (operation, then field) → write compute (create/update) → re-check (Aventara's checks on pipe and compute output: `A3004`, `A3000`; then field validators and Json shapes on every value, computed included: `A2004`) → interceptors → `hooks.before` → the operation (then compute read) → `hooks.after` → interceptors unwind; filters handle a failure. `ctx` calls are raw: they run validation, field pipes, compute and validators, and none of your guards, pipes, hooks, interceptors or filters.\n\n## The shape of a configuration\n\n```ts\n// src/aventara.config.ts\nimport { type AvGuard, AvError } from \"@aventara/core\";\nimport { createPrismaAdapter } from \"@aventara/prisma7-adapter\";\nimport { discovery } from \"./generated/aventara/discovery.artifact.js\";\nimport type { PrismaService } from \"./prisma.service.js\";\nimport { post } from \"./resources/post.resource.js\";\n\nconst requireUser: AvGuard = (ctx) => {\n if (ctx.$state.user === undefined) throw new AvError(\"A4000\", \"Sign in first.\");\n return true;\n};\n\nexport async function aventaraConfig(prisma: PrismaService) {\n return {\n api: \"/api\",\n exposed: true,\n adapter: await createPrismaAdapter({\n client: prisma,\n discovery,\n provider: \"postgresql\",\n driver: \"@prisma/adapter-pg\",\n }),\n // Root: hidden from every caller.\n exemptions: {\n resources: { User: { fields: { passwordHash: [\"select\", \"filter\", \"order\"] } } },\n },\n resources: { ...post.root },\n // Remote callers (HTTP, the generated client).\n client: {\n exposed: true,\n exemptions: { resources: { User: { operations: { delete: true } } } },\n pipelines: { guards: [requireUser] },\n },\n } as const;\n}\n```\n\n## Before you finish\n\n- `npx tsc --noEmit` passes with no casts added.\n- The server starts: an `AvConstructionError` lists each problem as `{ code, path, message, docs }`; fix the key at `path`.\n- After a schema or compute change you ran `npm run aventara:prepare`, and the frontend regenerated its client.\n\nThese files are generated by `aventara` for this project's Aventara version. Refresh them after upgrading: `npx @aventara/cli@<your @aventara/core version> skill`. A file you edited is kept (pass `--force` to replace it).\n",
3
+ "reference/config.md": "# Configuration reference\n\n`aventaraConfig(prisma)` in `src/aventara.config.ts` returns one object `as const`. Every name in it (Resource, field, operation) is checked against your model by TypeScript and again at startup.\n\n## The tree\n\n```text\nroot\n api \"/api\": where the protocol is mounted (root only)\n mcp \"/mcp\" or { path, authorization: { servers, verify } }: turns MCP on (root only)\n adapter await createPrismaAdapter({ client, discovery, provider, driver }) (root only)\n diagnostics (d: AvDiagnostic) => void: the real error behind every A3xxx (root only)\n misuse (e: AvMisuseEvent) => void: refused unreadable server-side input (root only)\n application overlay for your server code (framework.application, appTx, ctx calls)\n client overlay for remote callers (HTTP, the generated client)\n agent overlay for AI agents over MCP, plus extends: \"client\" | \"application\" | \"root\"\n -- the six layer keys, at the root and in every overlay --\n exposed true: exemptions hide; false: exemptions are all that is exposed\n exemptions { resources: { <R>: true | { operations?, fields? } } }\n transactionsStyle \"interactive\" | \"none\"\n limits { maxListLimit, maxNestingDepth, maxTransactionOperations, maxBooleanNodes, maxJsonDepth, maxRequestBytes }\n pipelines { guards, pipes, hooks: { before, after }, interceptors, filters }\n resources\n <Resource>\n pipelines every operation of the Resource\n operations\n <family> find | create | update | delete | upsert\n pipelines every variant of the family\n forceTransaction true: a plain call runs as a one-step transaction\n <variant> e.g. create.one, find.many\n pipelines that one operation (typed ctx.$args)\n forceTransaction\n transactions\n pipelines once per transaction with a top-level step on the Resource\n fields\n <field>\n definition root only: add a virtual field, or give a Json field its AvZ shape\n pipelines\n validators AvZ checks\n pipes (value) => value\n compute { create, update, read }\n guards (value, ctx) => boolean\n```\n\n## How a layer combines with the root\n\n| Key | Rule |\n|---|---|\n| `exposed` + `exemptions` | Narrowing only. An overlay starts from the root's view and can hide more, never bring back what the root hid. |\n| `transactionsStyle` | Override: the overlay's value when present, else the root's. |\n| `limits` | Override per limit. |\n| `pipelines` (every level) | Additive: the root's stages run, then the overlay's. Nothing removes a stage. |\n| field `validators` | Additive. |\n| field `compute` | Chained: the root's stage runs, then the overlay's, which receives the root's result as `value`. |\n| field `definition` | Root only (`CONFIG_FORBIDDEN_IN_OVERLAY` in an overlay). |\n| `forceTransaction` | Forced when the root or the operation's overlay sets it. |\n\nAn override wins when present, not when truthy (`false` overrides). An explicit `undefined` is refused: leave the key out. The `agent` layer narrows the layer it `extends` (default `\"client\"`).\n\n## Exposure\n\n| Value in `exemptions` | Under `exposed: true` (Omit) | Under `exposed: false` (Pick) |\n|---|---|---|\n| `true` | hide the entry and everything under it | expose the entry and everything under it |\n| `false` | nothing (same as leaving it out) | nothing |\n| a list | hide the listed entries one level down | expose only the listed entries |\n| an object | go one level deeper | go one level deeper |\n\n| Level | Names you can list |\n|---|---|\n| `operations` | `find`, `create`, `update`, `delete`, `upsert` |\n| a family | `find`: `first`, `unique`, `many`, `count`; `create`: `one`, `many`, `count`; `update`: `first`, `unique`, `many`, `count`; `delete`: `first`, `unique`, `count`; `upsert`: `unique` |\n| a scalar field | `select`, `filter`, `order`, `orderNulls`, `create`, `update` |\n| a relation field | `select`, `include`, `filter`, `order`, `pagination`, `reducers`, `create`, `update` |\n| a capability's tokens | `filter`: operators (`equals`, `in`, `contains`, …); `create`/`update`: directives (`$increment`, `$push`, …); relation `create`/`update`: `$create`, `$connect`, `$connectOrCreate`, `$disconnect`, `$set`, `$update`, `$delete`, `$upsert` |\n\nPrisma has no `delete.many`. Common shapes:\n\n- Write-only field: `fields: { passwordHash: [\"select\", \"filter\", \"order\"] }`.\n- Read-only for remote callers: `client.exemptions…fields: { likeCount: [\"create\", \"update\"] }`.\n- Hide a Resource from remote callers: `client.exemptions.resources.AuditLog: true`. Hide the relations that point at it too, or startup fails with `COMPILER_DANGLING_RELATION`.\n- A public client that reaches only what the frontend calls: `client: { exposed: false, exemptions: { resources: { Post: { operations: { find: [\"many\", \"unique\"] }, fields: { id: [\"select\"], title: [\"select\"] } } } } }`. A unique operation needs its identifier exposed (`COMPILER_MISSING_IDENTIFIER`).\n\nA hidden field disappears from that layer's inputs, outputs, filters, ordering and the generated client. A client exemption changes the contract hash: regenerate the frontend client.\n\n## Limits\n\n| Limit | Default | Exceeded |\n|---|---|---|\n| `maxRequestBytes` | `1048576` | `A2010` |\n| `maxNestingDepth` | `12` | `A2009` / `V1013` |\n| `maxListLimit` | `250` | `A2009` / `V1014` |\n| `maxTransactionOperations` | `20` | `A2009` / `V1014` |\n| `maxBooleanNodes` | `50` | `A2009` / `V1015` |\n| `maxJsonDepth` | `64` | `A2009` / `V1021` |\n\n## Fields\n\n- Add a virtual field at the root: `definition: { type: { scalar: \"string\" }, nullable: false, list: false, lifecycle: [\"VIRTUAL\"], capabilities: { select: true } }` plus a `compute.read` stage. A write-only virtual input says `capabilities: { select: false, create: [], update: [] }`.\n- Type a `Json` column: `definition: { json: AvZ.object({ … }) }`; every write is checked against it and the client types follow it.\n- A `compute.create` stage makes its field optional on create, and must return a value when the caller sent none.\n- A `compute.read` stage with no `dependsOn` declares no field it reads, so it computes from the same inputs for every row: such a value usually belongs in response metadata or `ctx.$state`, not in a field.\n\n## Startup\n\nA wrong configuration throws one `AvConstructionError` before the server listens: `stage` (`configuration`, `discovery` or `contract-compilation`) and `diagnostics`, each `{ code, path, message, docs }`. `path` names the key; `reference/errors.md` explains each code.\n",
4
+ "reference/recipes.md": "# Recipes\n\nEvery block here compiles against the Aventara release this skill was written for. Model names (`User`, `Post`, `Order`, `OutboxEvent`) are examples: use your schema's.\n\n## The signed-in user, for every stage\n\nDeclare what the request state holds, once:\n\n```ts\n// src/aventara.state.ts\ndeclare module \"@aventara/core\" {\n interface AvState {\n user?: { id: number; role: \"member\" | \"admin\" };\n }\n}\nexport {};\n```\n\nSeed it from your auth middleware's result, in `src/app.module.ts`:\n\n```ts\n// src/app.module.ts\nimport { Module } from \"@nestjs/common\";\nimport { AventaraModule } from \"@aventara/nest\";\nimport type { Request } from \"express\";\nimport { aventaraConfig } from \"./aventara.config.js\";\nimport { PrismaModule, PrismaService } from \"./prisma.service.js\";\n\ntype AuthedRequest = Request & { user?: { id: number; role: \"member\" | \"admin\" } };\n\n@Module({\n imports: [\n PrismaModule,\n AventaraModule.forRootAsync({\n imports: [PrismaModule],\n inject: [PrismaService],\n useFactory: async (prisma: PrismaService) => ({\n config: await aventaraConfig(prisma),\n state: (req: AuthedRequest) => ({ user: req.user }),\n }),\n }),\n ],\n})\nexport class AppModule {}\n```\n\nServer code passes it per call: `framework.application.Post.create.one({ data }, { state: { user } })`. Guards read it:\n\n```ts\nimport { AvError, type AvGuard } from \"@aventara/core\";\n\nexport const requireUser: AvGuard = (ctx) => {\n if (ctx.$state.user === undefined) throw new AvError(\"A4000\", \"Sign in first.\");\n return true;\n};\nexport const adminOnly: AvGuard = (ctx) => ctx.$state.user?.role === \"admin\";\n```\n\nPut `requireUser` in `client: { pipelines: { guards: [requireUser] } }` (remote callers only), and `adminOnly` where it applies (`client: { resources: { Post: { operations: { delete: { pipelines: { guards: [adminOnly] } } } } } }`).\n\n## A Resource in its own file, with a server-filled owner\n\n`Post.authorId` is required; remote callers must not choose it. `compute.create` fills it from the signed-in user, which also makes it optional on create; the `client` exemption takes both `authorId` and the `author` relation out of the remote create and update inputs. Hiding only `authorId` is not enough: `author: { $connect: … }` would still let a caller set or move the owner.\n\n```ts\n// src/resources/post.resource.ts\nimport { AvError, defineResource } from \"@aventara/core\";\nimport { AvZ } from \"@aventara/core/avz\";\nimport { discovery } from \"../generated/aventara/discovery.artifact.js\";\n\ntype Model = typeof discovery.model;\n\nconst slugify = (text: string) =>\n text.toLowerCase().trim().replace(/[^a-z0-9]+/g, \"-\").replace(/^-|-$/g, \"\");\n\nexport const post = defineResource<Model>()(\"Post\", {\n fields: {\n title: { pipelines: { validators: [AvZ.regex(/\\S/), AvZ.maxLength(200)] } },\n slug: {\n pipelines: {\n compute: {\n create: ({ data }) => slugify(String(data.title)),\n update: ({ data, value }) => (data.title === undefined ? value : slugify(String(data.title))),\n },\n },\n },\n authorId: {\n pipelines: {\n compute: {\n create: ({ value, ctx }) => {\n const user = ctx.$state.user;\n if (user !== undefined) return user.id;\n if (typeof value === \"number\") return value;\n throw new AvError(\"A4000\", \"Sign in first.\");\n },\n },\n },\n },\n },\n});\n```\n\nSpread it into the configuration: `resources: { ...post.root }`. Take `authorId`, `author` and `slug` out of the remote inputs in the `client` layer's `exemptions` (next recipe): exposure lives in a layer's `exemptions`, never in a `defineResource` block. Run `npm run aventara:prepare` after adding compute stages.\n\n## Exposure: what each caller may reach\n\n```ts\n// src/aventara.config.ts (the returned object)\nexemptions: {\n resources: {\n User: { fields: { passwordHash: [\"select\", \"filter\", \"order\"] } }, // write-only everywhere\n },\n},\nclient: {\n exposed: true,\n exemptions: {\n resources: {\n AuditLog: true, // remote callers never see it\n User: { operations: { delete: true }, fields: { internalNotes: true } },\n Post: { fields: { authorId: [\"create\", \"update\"], author: [\"create\", \"update\"], slug: [\"create\", \"update\"] } }, // filled by the server; a caller writing `author` gets A2004 (V1006)\n },\n },\n},\n```\n\n## Values: validators and a field pipe\n\n```ts\n// in a defineResource block's fields, or resources.<R>.fields\nemail: {\n pipelines: {\n pipes: [(value) => (typeof value === \"string\" ? value.trim().toLowerCase() : value)],\n validators: [AvZ.email(), AvZ.maxLength(254)],\n },\n},\n```\n\nValidators are `AvZ` checks only (`minLength`, `maxLength`, `regex`, `email`, `uuid`, `gte`, `lte`, `multipleOf`, `maxItems`, …). A rule across two fields, or one that reads the database, is a guard.\n\n## An outbox event that commits with the order\n\n```ts\n// src/resources/order.resource.ts\nimport { defineResource } from \"@aventara/core\";\nimport { discovery } from \"../generated/aventara/discovery.artifact.js\";\n\ntype Model = typeof discovery.model;\n\nexport const order = defineResource<Model>()(\"Order\", {\n operations: {\n create: {\n one: {\n forceTransaction: true,\n pipelines: {\n hooks: {\n after: [\n async (result, ctx) => {\n // The result holds what the caller selected: narrow it, never cast it.\n const id = typeof result === \"object\" && result !== null && \"id\" in result ? result.id : undefined;\n // Runs inside the order's transaction: a throw rolls the order back.\n if (typeof id !== \"number\") throw new Error('create orders with select: [\"id\"]');\n await ctx.OutboxEvent.create.one({\n data: { topic: \"order.created\", payload: { orderId: id } },\n select: [\"id\"],\n });\n },\n ],\n },\n },\n },\n },\n },\n});\n```\n\nOrders are created by server code, which selects `id`: `framework.application.Order.create.one({ data, select: [\"id\"] })`. Hide both from remote callers: `client.exemptions.resources: { OutboxEvent: true, Order: { operations: { create: true } } }`. A consumer reads new `OutboxEvent` rows (or, on PostgreSQL with a queue, the hook sends the event's id with ``ctx.$sql`…` ``).\n\n## Server code\n\nNest's injection carries no type argument, so `src/aventara.config.ts` exports the framework's type once:\n\n```ts\n// src/aventara.config.ts (beside aventaraConfig)\nimport { createFramework } from \"@aventara/core\";\n\nexport const buildFramework = async (prisma: PrismaService) => createFramework(await aventaraConfig(prisma));\nexport type AppFramework = Awaited<ReturnType<typeof buildFramework>>;\n```\n\n```ts\n// src/post-reader.service.ts\nimport { Injectable } from \"@nestjs/common\";\nimport { AvNotFoundError } from \"@aventara/core\";\nimport { InjectFramework } from \"@aventara/nest\";\nimport type { AppFramework } from \"./aventara.config.js\";\n\n@Injectable()\nexport class PostReader {\n constructor(@InjectFramework() private readonly framework: AppFramework) {}\n\n async findOrNull(id: number) {\n try {\n return await this.framework.application.Post.find.unique({ where: { id }, select: [\"id\", \"title\"] });\n } catch (error) {\n if (error instanceof AvNotFoundError) return null;\n throw error;\n }\n }\n}\n```\n\n`framework.application` calls resolve the data and throw the code's `AvError` subclass; `find.first` resolves `null` instead of throwing. Several writes that must succeed together: `framework.transaction([framework.appTx.Post.create.one(…), …])`, or `ctx.$transaction(async (tx) => …)` inside a stage.\n\n## Agents (MCP)\n\n```ts\n// in the returned configuration\nmcp: \"/mcp\",\nagent: {\n extends: \"client\",\n exposed: true,\n exemptions: { resources: { User: { operations: { delete: true } } } },\n},\n```\n\nWithout `mcp.authorization` the endpoint answers callers on the same machine only; set `authorization: { servers, verify }` before deploying. A guard can tell an agent apart: `ctx.$origin === \"agent\"`.\n",
5
+ "reference/stages.md": "# Stages reference\n\n## The stages\n\n| Stage | Key | Receives | Returns |\n|---|---|---|---|\n| Guard | `guards` | `(ctx)` | `true`/`undefined` to allow, `false` to deny (`A4001`); or throw an `AvError` |\n| Pipe | `pipes` | `(args, ctx)` | new arguments, or `undefined` to keep them; the only stage that changes arguments |\n| Hook | `hooks.before`, `hooks.after` | before `(ctx)`; after `(result, ctx)` | nothing |\n| Interceptor | `interceptors` | `(ctx, next)` | the result of `next()`, called exactly once |\n| Filter | `filters` | `(error, ctx)` | an `AvError` to answer with, or `undefined` to pass it on |\n| Validators (field) | `fields.<f>.pipelines.validators` | `AvZ` checks | data, part of the contract |\n| Field pipe | `fields.<f>.pipelines.pipes` | `(value)` | the new value; no `ctx` |\n| Compute (field) | `fields.<f>.pipelines.compute` | `create`/`update`: `({ value, data, ctx })`; `read`: `{ dependsOn, compute: ({ record }) }` | the field's value |\n| Field write guard | `fields.<f>.pipelines.guards` | `(value, ctx)` | `false` refuses when the caller sends the field |\n\nA relation field takes write guards only. Stage types to annotate with: `AvGuard`, `AvPipe`, `AvBeforeHook`, `AvAfterHook`, `AvInterceptor`, `AvFilter`, `AvFieldPipe`, `AvFieldGuard`, and `AvTransactionGuard`, `AvTransactionPipe`, `AvTransactionBeforeHook`, `AvTransactionAfterHook`, `AvTransactionInterceptor`, `AvTransactionFilter`. Pass your model (`AvAfterHook<Model>`, `type Model = typeof discovery.model`) when the stage calls `ctx.<Resource>`.\n\n## Levels\n\n| Level | Key | Runs for |\n|---|---|---|\n| Layer | `pipelines` | every operation of the layer |\n| Resource | `resources.<R>.pipelines` | every operation of R |\n| Family | `resources.<R>.operations.<family>.pipelines` | every variant of the family |\n| Variant | `resources.<R>.operations.<family>.<variant>.pipelines` | that operation; `ctx.$args` is typed |\n| Field | `resources.<R>.fields.<f>.pipelines` | field f's value |\n| Resource transactions | `resources.<R>.transactions.pipelines` | once per transaction with a top-level step on R |\n\nThe root's stages run for every origin; an overlay's only for its own (`ctx.$origin`: `\"application\"`, `\"client\"` or `\"agent\"`). Register a stage once, at the outermost level it applies to: a root stage copied into an overlay runs twice. Registering a stage does not expose an operation.\n\n## Order\n\n```text\nframework validation (required create fields included; a field compute.create fills is not required yet)\n-> guards layer -> Resource -> family -> variant (root, then overlay, at each), then field write guards\n-> pipes the same levels, then field pipes\n-> compute create or update (write compute)\n-> re-check Aventara's own checks on the pipe and compute output (A3004, A3000), then field validators and Json shapes on every value, computed included (A2004)\n-> interceptors enter, outer to inner\n-> hooks.before\n-> operation one adapter call, then compute read on the returned rows\n-> hooks.after\n-> interceptors unwind\non a failure after validation: filters, inner to outer; the first that handles it wins\n```\n\nA call refused by a guard, the re-check or a validator runs no interceptor and no hook. A `unique` operation that matches no row fails at the operation (`AvNotFoundError`, `A2003`); no `after` hook runs.\n\nWith `forceTransaction` (or as a step of a transaction plan), guards and pipes run before the transaction, and validators judge the literal values no write compute changes; a refusal there opens nothing. Then: `BEGIN -> the Resource's transaction stages -> write compute -> re-check (computed and $ref values included) -> interceptors -> hooks.before -> adapter call -> hooks.after -> interceptors -> COMMIT`. A refusal inside rolls the transaction back. The hooks' `ctx` calls and `ctx.$sql` join it; the guards' and pipes' calls do not.\n\n## Rules that are easy to miss\n\n- `ctx` calls (`ctx.<Resource>.<family>.<variant>(args)`) are raw: validation, field pipes, compute and validators run; guards, pipes, hooks, interceptors, filters and transaction stages do not. They are application-origin: the `application` overlay's exposure applies, the `client` overlay's does not. They resolve data directly and throw the `AvError` subclass of a failure.\n- `ctx.$args` is frozen. A pipe returns new arguments; an `after` hook gets a frozen result. To answer with a different result, use an interceptor. A `Date` or bytes value cannot be frozen, so each stage gets its own copy: changing one in place changes nothing another stage sees, what is written or what the caller receives.\n- `ctx.$state` is the only thing a stage writes in place: one object per request, shared by every stage and inherited by `ctx` calls. Compute can read it, not write it.\n- `compute.read` gets the row (`record`) frozen, with its own copy of each `Date` or bytes value: return the field's value. Writing into `record` throws and answers `A3000`; the answer is built from the row as read.\n- Compute gets a read-only `ctx`: `find.*` calls only, no writes, no `$sql`, no `$transaction` (reaching one answers `A3000`). Effects on other rows belong in an `after` hook under `forceTransaction`.\n- A required create field the caller left out is refused before the guards; no pipe can fill it. Use `compute.create` or a database default.\n- Validators run after write compute, so they judge the computed value too: a `compute.create` result that fails a validator answers `A2004`. Aventara's own re-check comes first: a pipe that returns a wrong type or an unknown key, or drops a required field, answers `A3004`, as does a pipe or compute that writes a reference twice; a compute that returns a wrong type or a value its field's `format` refuses (a relative path for `format: \"url\"`), or leaves a required field missing, answers `A3000`.\n- Validators do not judge a directive's operand (`{ $increment: 500 }`); `$push` entries are judged.\n- A Resource reached only through a nested write runs that Resource's guards and field stages, not its transaction stages, hooks or interceptors.\n- Without a transaction, a write has committed when its `after` hook runs.\n- A guard or pipe that throws a non-`AvError` answers `A3000` and the real error goes to `diagnostics`; throw `new AvError(code, message)` to answer a code. A thrown `A2004` may carry `issues: [{ path: [\"arguments\", …], code: \"V1001\", message }]`.\n- A pipe's output is re-checked against the server's own contract before compute and validators see it: returning a field the Resource does not have, or a wrong type, answers `A3004`. Name pipes (`function scopeToTenant(…)`) so the server log names them.\n"
6
+ };
7
+ export const AGENT_SKILL_ERRORS = "# Aventara error codes\n\nEvery code `@aventara/core` 1.0.0-rc.2 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.\n\nBranch on the code, never on the message. In code, `AvErrorCatalog[code]` gives the same entry: `{ code, kind, message, cause, fix, docs }`.\n\n## Success codes (A1xxx)\n\nAn operation's `code` when it succeeded. The generated client resolves for these.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`A1000`](https://aventara-framework.vercel.app/docs/error-codes#a1000) | Success. | A read, a count or another operation without a more specific success code succeeded. | Nothing: the call succeeded. |\n| [`A1001`](https://aventara-framework.vercel.app/docs/error-codes#a1001) | No match. | A first-style operation (find.first, update.first, delete.first) matched no row; data is null and the generated client resolves null. | Nothing: handle the null result. A unique operation answers A2003 instead. |\n| [`A1002`](https://aventara-framework.vercel.app/docs/error-codes#a1002) | Created. | create.one created a row. | Nothing: the call succeeded. |\n| [`A1003`](https://aventara-framework.vercel.app/docs/error-codes#a1003) | Created many. | create.many or create.count created rows. | Nothing: the call succeeded. |\n| [`A1004`](https://aventara-framework.vercel.app/docs/error-codes#a1004) | Updated. | update.first or update.unique updated a row. | Nothing: the call succeeded. |\n| [`A1005`](https://aventara-framework.vercel.app/docs/error-codes#a1005) | Updated many. | update.many or update.count updated rows. | Nothing: the call succeeded. |\n| [`A1006`](https://aventara-framework.vercel.app/docs/error-codes#a1006) | Deleted. | delete.first or delete.unique deleted a row. | Nothing: the call succeeded. |\n| [`A1007`](https://aventara-framework.vercel.app/docs/error-codes#a1007) | Deleted many. | delete.count deleted rows. | Nothing: the call succeeded. |\n| [`A1008`](https://aventara-framework.vercel.app/docs/error-codes#a1008) | Upserted. | upsert.unique created or updated a row. | Nothing: the call succeeded. |\n| [`A1009`](https://aventara-framework.vercel.app/docs/error-codes#a1009) | Transaction committed. | A whole transaction plan committed; data holds one result per step. | Nothing: the plan committed. |\n\n## Request errors (A2xxx)\n\nThe caller can fix these. The generated client and server-side calls throw the code's `AvError` subclass.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`A2000`](https://aventara-framework.vercel.app/docs/error-codes#a2000) | Invalid request. | The HTTP request cannot be interpreted: malformed JSON, a body that is not a JSON object, an unknown `/_*` path, a body that does not decode in its Content-Encoding, or a missing Aventara-Protocol-Version or Aventara-Contract-Hash header (the message names it). | Send a JSON object with both identity headers. The generated client does this for you. |\n| [`A2001`](https://aventara-framework.vercel.app/docs/error-codes#a2001) | Unknown Resource. | The Resource is not in the caller's contract: misspelt, hidden by exposure, or added to the schema after aventara:prepare last ran. cause.message is \"The requested Resource is not available.\" and the issue is V1005 at `[\"resource\"]`. | Use the model's exact name. After a schema change run the adapter's generate step (`npm run aventara:prepare` in a scaffolded project), restart, and regenerate the client. A Resource hidden on purpose is reached from server code (framework.application). |\n| [`A2002`](https://aventara-framework.vercel.app/docs/error-codes#a2002) | Unknown operation. | The operation is not available on that Resource: a typo, removed by exposure, an operation the adapter does not offer (such as delete.many), or a ctx.$sql, appTx.$sql or ctx.$transaction the adapter or the layer cannot run. The issue is V1006 at `[\"family\"]` or `[\"variant\"]`. | Call an operation the contract lists (`GET <api>/_contract`), regenerate a stale client, and check the layer's exemptions and transactionsStyle. |\n| [`A2003`](https://aventara-framework.vercel.app/docs/error-codes#a2003) | Not found. | A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs. | Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier. |\n| [`A2004`](https://aventara-framework.vercel.app/docs/error-codes#a2004) | Validation failed. | The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator's issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `[\"arguments\"]` and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write. | Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it. |\n| [`A2005`](https://aventara-framework.vercel.app/docs/error-codes#a2005) | Contract mismatch. | The caller's Aventara-Contract-Hash is not the server's current one: the schema, the configuration, the limits or the exposure changed, or the client targets another deployment. | Regenerate the client against the running server: `npm run avclient:generate`. |\n| [`A2006`](https://aventara-framework.vercel.app/docs/error-codes#a2006) | Protocol mismatch. | Aventara-Protocol-Version is not 1. | Send `Aventara-Protocol-Version: 1`, or upgrade the client. |\n| [`A2007`](https://aventara-framework.vercel.app/docs/error-codes#a2007) | Invalid transaction reference. | A transaction $ref or fingerprint is invalid: the issues are V1010, V1011, V1016, V1017 or V1018, and cause.operation is the step. A $ref with no fingerprint is A2004 / V1000. | Let the generated client or framework.appTx build plans; check the $ref path and that the referenced step comes earlier. |\n| [`A2008`](https://aventara-framework.vercel.app/docs/error-codes#a2008) | Conflict. | A uniqueness or reference constraint failed: a duplicate value, a missing parent, or a $disconnect or $set that would orphan a required child (also through update.first or a developer SQL statement). Sending it again unchanged fails the same way. | Change the data. For a duplicate, update the existing row or use upsert.unique. |\n| [`A2009`](https://aventara-framework.vercel.app/docs/error-codes#a2009) | Limit exceeded. | A contract limit was crossed: nesting depth (V1013), list or plan size (V1014), boolean nodes (V1015) or Json value depth (V1021). | Ask for less (paginate, split the plan), or raise the limit under `limits`. |\n| [`A2010`](https://aventara-framework.vercel.app/docs/error-codes#a2010) | Payload too large. | The decoded request body exceeds maxRequestBytes (default 1048576). | Send less, or raise limits.maxRequestBytes. |\n| [`A2011`](https://aventara-framework.vercel.app/docs/error-codes#a2011) | Unsupported media type. | The body is not application/json, or its Content-Encoding is not gzip, deflate or br. | Send `Content-Type: application/json`. |\n| [`A2012`](https://aventara-framework.vercel.app/docs/error-codes#a2012) | Method not allowed. | The HTTP method does not fit the route; the Allow header names the right one (`POST` on operations and `/_transactions`, `GET` on `/_contract`). | Use the method the Allow header names. |\n| [`A2013`](https://aventara-framework.vercel.app/docs/error-codes#a2013) | Concurrent modification. | A competing writer reached the state first; nothing was written. | Retry. No retry is automatic. |\n| [`A2014`](https://aventara-framework.vercel.app/docs/error-codes#a2014) | Stale selection. | The selected state changed before the write; nothing was written. | Read again, then retry. |\n\n## Server errors (A3xxx)\n\nNot caller-actionable: `cause.message` is generic, and the real error goes to the root `diagnostics` sink.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`A3000`](https://aventara-framework.vercel.app/docs/error-codes#a3000) | Internal error. | An unexpected failure on the server: a stage threw an error that is not an AvError, an interceptor called next() twice, a compute stage reached a write through its read-only ctx, a compute stage returned a value of the wrong type, or a compute.create returned undefined for a required field. cause.message is always \"Internal framework error.\" | Read the real error in the diagnostics sink (the root `diagnostics` key) or the server log line for the request id, and fix that stage. To answer with a code of your choice, throw an AvError. |\n| [`A3001`](https://aventara-framework.vercel.app/docs/error-codes#a3001) | Adapter error. | A database or ORM failure that is not something the caller can fix. cause.message is always \"Adapter execution failed.\" | Read the raw error in the diagnostics sink; check the database, its connection and the migration state. |\n| [`A3002`](https://aventara-framework.vercel.app/docs/error-codes#a3002) | Transaction error. | Transaction infrastructure failed (commit or rollback, or the transaction expired or was closed under a step), or a Resource transaction interceptor called next() twice or resolved without calling it. | Read the diagnostics sink. Call next() exactly once in a transaction interceptor; retry an infrastructure failure. |\n| [`A3003`](https://aventara-framework.vercel.app/docs/error-codes#a3003) | Serialization error. | A result could not be encoded for transport. | Read the diagnostics sink; make compute.read stages and Json values return plain data of the field's type. |\n| [`A3004`](https://aventara-framework.vercel.app/docs/error-codes#a3004) | Pipe output invalid. | A server pipe, field pipe or transaction pipe (or write compute) returned arguments the server's own contract refuses: a field the Resource does not have, a wrong type, a dropped required field, a reference written together with its relation (V1012), or a changed plan. cause.message is always \"A server pipeline produced invalid arguments.\" | Read the server log line, which names the pipe, the operation and the path, and fix the pipe. A pipe returns a new object of the same shape, or undefined to keep the arguments. |\n\n## Authentication and authorization (A4xxx)\n\nProduced by your guards; the framework never authenticates by itself.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`A4000`](https://aventara-framework.vercel.app/docs/error-codes#a4000) | Unauthenticated. | A guard threw new AvError(\"A4000\", ...): authentication is required or failed. The framework never authenticates by itself. | Send the credentials your guards expect, such as the Authorization header. |\n| [`A4001`](https://aventara-framework.vercel.app/docs/error-codes#a4001) | Forbidden. | A guard returned false (\"Operation denied by a framework guard.\"), a field write guard or a nested write's target guard included, or a stage threw A4001. | Call as a caller the guard admits, or stop sending the guarded field. To answer another code, throw an AvError from the guard. |\n| [`A4002`](https://aventara-framework.vercel.app/docs/error-codes#a4002) | Policy denied. | A guard or hook threw new AvError(\"A4002\", ...) with a policy reason of your own. | Read cause.message for the policy's reason. |\n\n## Validation issues (V1xxx)\n\nEach entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A2009`: `{ code, path, message }`, plus `check` and `params` for a failed field validator.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`V1000`](https://aventara-framework.vercel.app/docs/error-codes#v1000) | Required value missing. | A required value is absent: a create leaves out a required field that has no database default and no compute.create (the path names the field), a required relation and its reference field are both missing, a typed Json field lacks a required key, or a $ref has no fingerprint. | Send the value at the issue's path. When the server must fill it, give the field a compute.create stage or a database default; a pipe cannot fill it. |\n| [`V1001`](https://aventara-framework.vercel.app/docs/error-codes#v1001) | Invalid runtime/wire type. | A value of the wrong type: a string where a number is expected, a negative limit, a non-integer id, null given to an operator with no null meaning (gt, contains, in), two directives in one scalar action object (`{ $increment: 1, $decrement: 2 }`), or a server-side value that is not plain data. | Send the type the contract states at that path (the generated client's types say it). |\n| [`V1002`](https://aventara-framework.vercel.app/docs/error-codes#v1002) | Invalid enum value. | A value that is not a member of the enum, or outside a typed Json field's AvZ.enum or AvZ.literal. | Send one of the enum's members. |\n| [`V1003`](https://aventara-framework.vercel.app/docs/error-codes#v1003) | Invalid format (for example UUID/DateTime/base64). | A malformed datetime, decimal, base64 or UUID string, or a failed pattern or format validator (`check: \"string_format\"`). | Send the canonical encoding (ISO-8601 UTC datetime, decimal string, base64 bytes) and satisfy the validator that check and params name. |\n| [`V1004`](https://aventara-framework.vercel.app/docs/error-codes#v1004) | Value outside accepted range. | A number, length or size outside its range: a failed bound, length or size validator, a BigInt outside signed 64 bits, an AvZ.int() outside the safe integer range, or a transaction with no operations. | Send a value inside the range; check and params name the validator that failed. |\n| [`V1005`](https://aventara-framework.vercel.app/docs/error-codes#v1005) | Unknown or inaccessible field. | A field that does not exist or is hidden in the caller's layer, an unknown Resource (under A2001), or a key a typed Json field's shape does not declare. | Use a field the caller's contract exposes, expose it through exemptions, or make the call from server code. |\n| [`V1006`](https://aventara-framework.vercel.app/docs/error-codes#v1006) | Unsupported filter/order/mutation directive. | An operator, directive or nested write the field does not offer (at any depth, inside not too; a to-many $update's data takes only what a bulk write takes), a mode it does not offer, or an operation that is not available (under A2002). | Use what the contract lists for that field or Resource, or expose it through exemptions. |\n| [`V1007`](https://aventara-framework.vercel.app/docs/error-codes#v1007) | Invalid unique identifier shape. | The where of a unique operation is not exactly one complete identifier. | Pass exactly one identifier: the id, or every field of one @unique or @@unique. |\n| [`V1008`](https://aventara-framework.vercel.app/docs/error-codes#v1008) | Invalid select/include/reducer projection. | Both select and include, a write-only field selected, or a relation that is not selectable there. | Use select or include, not both, and select only readable fields. |\n| [`V1009`](https://aventara-framework.vercel.app/docs/error-codes#v1009) | Invalid or non-deterministic cursor. | A cursor without an explicit orderBy, or cursor keys that do not match the ordered fields. | Give an orderBy and a cursor holding exactly its ordered fields. |\n| [`V1010`](https://aventara-framework.vercel.app/docs/error-codes#v1010) | Invalid transaction reference path. | A $ref path the earlier step does not return, a $ref to a step that is not in the transaction, or to a step with no single row (a many or count step). | Select the referenced field in the earlier step, and reference a single-row step. |\n| [`V1011`](https://aventara-framework.vercel.app/docs/error-codes#v1011) | Transaction reference type mismatch. | The referenced value's type does not fit its target, a nullable source where only a non-null value is accepted included. | Reference a field of the target's type, non-null where the target needs it. |\n| [`V1012`](https://aventara-framework.vercel.app/docs/error-codes#v1012) | Invalid relation action/cardinality. | A nested write that does not fit the relation (a list where one record is expected), a reference field set together with its own relation, one record mixing reference fields and relation writes where the adapter cannot run both, or a record written through a relation that writes that relation (or a field holding its reference) back, where the adapter admits no such write. | Write either the relation or its reference field, in the relation's cardinality. |\n| [`V1013`](https://aventara-framework.vercel.app/docs/error-codes#v1013) | Maximum nesting depth exceeded. | Nested projections, filters or writes deeper than maxNestingDepth. | Nest less, or raise limits.maxNestingDepth. |\n| [`V1014`](https://aventara-framework.vercel.app/docs/error-codes#v1014) | Configured limit exceeded. | A limit above maxListLimit, or more steps than maxTransactionOperations. | Paginate or split the plan, or raise the limit. |\n| [`V1015`](https://aventara-framework.vercel.app/docs/error-codes#v1015) | Boolean/filter node limit exceeded. | A where tree with more nodes than maxBooleanNodes. | Simplify the filter (in instead of many OR clauses), or raise limits.maxBooleanNodes. |\n| [`V1016`](https://aventara-framework.vercel.app/docs/error-codes#v1016) | Invalid transaction reference index. | A $ref to the referencing step itself or a later one, or to an index no step has. | Reference an earlier step only. |\n| [`V1017`](https://aventara-framework.vercel.app/docs/error-codes#v1017) | Transaction fingerprint mismatch. | A hand-written plan whose fingerprint does not match its step. | Let the generated client, framework.appTx or framework.clientTx build the plan. |\n| [`V1018`](https://aventara-framework.vercel.app/docs/error-codes#v1018) | Transaction reference resolved to no value. | The referenced step returned no value at that path, such as a first step that matched no row. | Make sure the earlier step returns a row, or use a unique step, which fails with A2003. |\n| [`V1019`](https://aventara-framework.vercel.app/docs/error-codes#v1019) | Transaction step depends on a row removed by an earlier step's cascade | A later step uses a row that an earlier step's delete removed through a database cascade. | Reorder or drop the dependent step. |\n| [`V1020`](https://aventara-framework.vercel.app/docs/error-codes#v1020) | Transaction mixes application and client steps. | One framework.transaction([...]) list mixes appTx and clientTx steps. | Build every step of a plan from one origin. |\n| [`V1021`](https://aventara-framework.vercel.app/docs/error-codes#v1021) | Json value depth limit exceeded. | A Json field's value nests deeper than maxJsonDepth. | Flatten the value, or raise limits.maxJsonDepth. |\n\n## Startup: configuration\n\n`createFramework` (and `AventaraModule`) throws one `AvConstructionError` with `stage: \"configuration\"`; each diagnostic is `{ code, path, message, docs }`.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`CONFIG_REQUIRED`](https://aventara-framework.vercel.app/docs/error-codes#config_required) | A required configuration key is missing. | A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions. | Add the key at the diagnostic's path. |\n| [`CONFIG_UNKNOWN_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_unknown_property) | Unknown configuration key. | A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree. | Remove it, or move it where it belongs (the configuration reference's \"Where a key can go\"). |\n| [`CONFIG_INVALID_TYPE`](https://aventara-framework.vercel.app/docs/error-codes#config_invalid_type) | A configuration key has the wrong type. | A value of the wrong type, or a key set to an explicit undefined. | Give the value the type the reference states; leave a key out rather than setting it to undefined. |\n| [`CONFIG_INVALID_VALUE`](https://aventara-framework.vercel.app/docs/error-codes#config_invalid_value) | A configuration value is not allowed. | A malformed api or mcp path, a negative limit, an unknown transactionsStyle, a format that is not a named zod format, or forceTransaction under an effective transactionsStyle \"none\". | Use a value the message allows. |\n| [`CONFIG_MOVED_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_moved_property) | A configuration key moved in rc.1. | Configuration written for 1.0.0-rc.0: restrictions, entrypoint, fields, behaviors, transactions or pipelines.before/after. | Move it where the message says: restrictions to exposed + exemptions, entrypoint to api, fields to `resources.<R>.fields.<f>.definition`, transactions to transactionsStyle, before/after to pipelines.hooks. |\n| [`CONFIG_FORBIDDEN_IN_OVERLAY`](https://aventara-framework.vercel.app/docs/error-codes#config_forbidden_in_overlay) | A root-only key is written in an overlay. | A field definition in application, client or agent. | Define the field once at the root (`resources.<R>.fields.<f>.definition`); an overlay only chooses its visibility. |\n\n## Startup: discovery\n\n`AvConstructionError` with `stage: \"discovery\"`: what the adapter reported does not hold together.\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`DISCOVERY_TRANSACTION_CAPABILITY_MISMATCH`](https://aventara-framework.vercel.app/docs/error-codes#discovery_transaction_capability_mismatch) | The adapter's transaction capability does not match it. | The adapter declares interactive transactions but provides no transaction function. | A defect of the adapter: install the adapter release that matches @aventara/core exactly. |\n\n## Startup: contract compilation\n\n`AvConstructionError` with `stage: \"contract-compilation\"`; a diagnostic's `path` starts with its layer (`root.`, `application.`, `client.`, `agent.`).\n\n| Code | Meaning | Cause | Fix |\n|---|---|---|---|\n| [`COMPILER_UNKNOWN_RESOURCE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_unknown_resource) | Unknown Resource. | A Resource name the adapter's model does not have, in resources or in exemptions. | Use the model's exact name. After a schema change run `npm run aventara:prepare`. |\n| [`COMPILER_UNKNOWN_FIELD`](https://aventara-framework.vercel.app/docs/error-codes#compiler_unknown_field) | Unknown field. | A field that is neither in the schema nor defined at the root. | Fix the name, run aventara:prepare after a schema change, or add the field with a root definition. |\n| [`COMPILER_RELATION_FIELD_CONFIG_UNSUPPORTED`](https://aventara-framework.vercel.app/docs/error-codes#compiler_relation_field_config_unsupported) | A relation field takes write guards only. | Field pipes or compute on a relation field. | Put the stage on the relation's reference field, or use an operation pipe. |\n| [`COMPILER_INCOMPLETE_FIELD`](https://aventara-framework.vercel.app/docs/error-codes#compiler_incomplete_field) | An added field is incomplete. | A definition that adds a field leaves out type, nullable or list. | Spell out type, nullable and list, with lifecycle: [\"VIRTUAL\"] for a field that has no column. |\n| [`COMPILER_STRUCTURAL_FIELD_OVERRIDE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_structural_field_override) | A discovered field's structure was redefined. | A definition on a field the adapter discovered changes its type, format or capabilities. | Restate only its type, nullable and list. Change capabilities through exemptions and the column through the schema. |\n| [`COMPILER_MISSING_SCALAR`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_scalar) | Unknown scalar type. | A definition names a scalar type that does not exist. | Use a built-in scalar: string, boolean, int, float, bigint, decimal, datetime, bytes or json. |\n| [`COMPILER_MISSING_ENUM`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_enum) | Unknown enum. | A definition names an enum the model does not have. | Use an enum of the schema, and run aventara:prepare after adding one. |\n| [`COMPILER_DANGLING_RELATION`](https://aventara-framework.vercel.app/docs/error-codes#compiler_dangling_relation) | A visible relation points at a hidden Resource. | A layer hides a Resource but still exposes a relation field that targets it. | Hide the relation field too (`exemptions.resources.<R>.fields.<relation>: true`), or expose the target. |\n| [`COMPILER_UNAVAILABLE_OPERATION`](https://aventara-framework.vercel.app/docs/error-codes#compiler_unavailable_operation) | Operation not available. | An operation the adapter does not offer (such as delete.many), or an overlay granting one that its parent hid. | Remove it. To reach an operation from one overlay only, expose it at the root and hide it in the other overlay. |\n| [`COMPILER_UNAVAILABLE_CAPABILITY`](https://aventara-framework.vercel.app/docs/error-codes#compiler_unavailable_capability) | Capability not available. | A capability or token the field does not offer, or an overlay (or the agent layer) granting one its parent hid. Overlays only narrow. | Remove it, or expose it in the parent layer and hide it in the layer it must not reach. |\n| [`COMPILER_INVALID_LIFECYCLE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_lifecycle) | Invalid field lifecycle. | An added field declares a lifecycle other than VIRTUAL, a VIRTUAL field claims a database-generated lifecycle or a provider-side capability, or a definition redefines a discovered field's lifecycle. | Give an added field lifecycle: [\"VIRTUAL\"] and only the capabilities it can serve; computed lifecycles come from compute stages. |\n| [`COMPILER_MISSING_READ_COMPUTE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_read_compute) | A selectable virtual field has no compute.read. | A VIRTUAL field that can be selected has no pipelines.compute.read stage. | Add a compute.read stage, or make the field write-only with capabilities.select: false. |\n| [`COMPILER_INVALID_COMPUTE_DEPENDENCY`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_compute_dependency) | Invalid compute.read dependency. | A dependsOn entry that is not a stored scalar field of the same Resource. | List only stored scalar fields of the Resource in dependsOn. |\n| [`COMPILER_RESERVED_RESOURCE_NAME`](https://aventara-framework.vercel.app/docs/error-codes#compiler_reserved_resource_name) | Reserved Resource name. | A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref). | Rename the model; the table it maps to can keep its name. |\n| [`COMPILER_MISSING_IDENTIFIER`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_identifier) | A unique operation has no identifier. | A layer exposes a unique variant (find.unique, update.unique, delete.unique, upsert.unique) and no complete identifier of the Resource: a Pick that did not pick one, or an Omit that hid every one. | Expose an identifier field (id, or every field of one unique), or exempt the unique variants. |\n| [`COMPILER_HIDDEN_REQUIRED_FIELD`](https://aventara-framework.vercel.app/docs/error-codes#compiler_hidden_required_field) | A layer that creates a Resource hides a required field. | A layer still creates the Resource (a create or upsert variant, or a nested create) while a required field, or a required relation's reference field, is hidden or has its create capability exempted, and no compute.create fills it. | Expose the field, give it a compute.create stage, or exempt the layer's create operations. |\n| [`COMPILER_INVALID_VALIDATOR`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_validator) | A field validator cannot apply. | A validator that is not built with AvZ, does not fit the field's type (AvZ.maxLength on an int, AvZ.maxItems on a field that is not a list), or sits on a relation or Json field. | Use an AvZ check that fits the field's type; a Json field's checks go in its json shape. |\n| [`COMPILER_INVALID_JSON_SHAPE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_json_shape) | A Json shape cannot apply. | A json shape on a field that is not a discovered Json field, a shape not built with AvZ, or one that admits null or absence at its top level. | Put the shape on a Json field, build it with AvZ, and keep its top level non-null (the column's nullability decides null). |\n| [`COMPILER_INVALID_CLIENT_CONTRACT`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_client_contract) | The compiled client contract is invalid. | The finished client contract failed its own structural check: a framework defect. | Report it with the diagnostic's path and message. |\n";
@@ -0,0 +1,116 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AGENT_SKILL_ERRORS, AGENT_SKILL_GUIDANCE, } from "./agent-skill.generated.js";
3
+ export const SKILL_DIRECTORY = ".claude/skills/aventara";
4
+ export const SKILL_MANIFEST = `${SKILL_DIRECTORY}/.aventara-skill.json`;
5
+ export const AGENTS_FILE = "AGENTS.md";
6
+ export const AGENTS_SECTION_START = "<!-- aventara:start -->";
7
+ export const AGENTS_SECTION_END = "<!-- aventara:end -->";
8
+ export const AGENTS_SECTION = [
9
+ AGENTS_SECTION_START,
10
+ "## Aventara",
11
+ "",
12
+ "This project's API is built with Aventara (https://aventara-framework.vercel.app). Before changing `src/aventara.config.ts`, `src/resources/` or `prisma/schema.prisma`, read `.claude/skills/aventara/SKILL.md` and follow its rules.",
13
+ "Every error code (`A2004`, `V1000`, `COMPILER_…`) is explained in `.claude/skills/aventara/reference/errors.md`.",
14
+ "Refresh these files after upgrading Aventara: `npx @aventara/cli@<your @aventara/core version> skill`.",
15
+ AGENTS_SECTION_END,
16
+ ].join("\n");
17
+ export function agentSkillFiles(input) {
18
+ return {
19
+ ...Object.fromEntries(Object.entries(AGENT_SKILL_GUIDANCE).map(([file, text]) => [
20
+ file,
21
+ text.replaceAll("{{CORE_VERSION}}", input.coreVersion),
22
+ ])),
23
+ "reference/errors.md": input.errors ?? AGENT_SKILL_ERRORS,
24
+ };
25
+ }
26
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
27
+ function manifestOf(text) {
28
+ if (text === undefined) {
29
+ return undefined;
30
+ }
31
+ try {
32
+ const parsed = JSON.parse(text);
33
+ return typeof parsed.files === "object" && parsed.files !== null
34
+ ? { coreVersion: String(parsed.coreVersion), files: parsed.files }
35
+ : undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ export function mergeAgentsSection(before) {
42
+ if (before === undefined) {
43
+ return { text: `${AGENTS_SECTION}\n` };
44
+ }
45
+ const starts = before.split(AGENTS_SECTION_START).length - 1;
46
+ const ends = before.split(AGENTS_SECTION_END).length - 1;
47
+ if (starts === 0 && ends === 0) {
48
+ const separator = before === "" ? "" : before.endsWith("\n") ? "\n" : "\n\n";
49
+ return { text: `${before}${separator}${AGENTS_SECTION}\n` };
50
+ }
51
+ const start = before.indexOf(AGENTS_SECTION_START);
52
+ const end = before.indexOf(AGENTS_SECTION_END);
53
+ if (starts !== 1 || ends !== 1 || end < start) {
54
+ return {
55
+ refused: `${AGENTS_FILE} has its ${AGENTS_SECTION_START} and ${AGENTS_SECTION_END} markers out of order or more than once, so its Aventara section was not updated; leave one pair around the section, or remove both to have it appended`,
56
+ };
57
+ }
58
+ return {
59
+ text: `${before.slice(0, start)}${AGENTS_SECTION}${before.slice(end + AGENTS_SECTION_END.length)}`,
60
+ };
61
+ }
62
+ export function planAgentSkill(input) {
63
+ const writes = [];
64
+ const warnings = [];
65
+ const previous = manifestOf(input.read(SKILL_MANIFEST));
66
+ const recorded = {};
67
+ const files = agentSkillFiles({
68
+ coreVersion: input.coreVersion,
69
+ ...(input.errors === undefined ? {} : { errors: input.errors }),
70
+ });
71
+ for (const [file, content] of Object.entries(files)) {
72
+ const target = `${SKILL_DIRECTORY}/${file}`;
73
+ const before = input.read(target);
74
+ if (before === content) {
75
+ recorded[file] = sha256(content);
76
+ continue;
77
+ }
78
+ const last = previous?.files[file];
79
+ const ours = before === undefined || (last !== undefined && sha256(before) === last);
80
+ if (ours || input.force) {
81
+ writes.push({
82
+ path: target,
83
+ content,
84
+ change: before === undefined ? "create" : "replace",
85
+ });
86
+ recorded[file] = sha256(content);
87
+ continue;
88
+ }
89
+ warnings.push(`${target} was changed since aventara wrote it, so it was kept; run \`aventara skill --force\` to replace it with this version's`);
90
+ if (last !== undefined) {
91
+ recorded[file] = last;
92
+ }
93
+ }
94
+ const manifest = `${JSON.stringify({ coreVersion: input.coreVersion, files: recorded }, null, "\t")}\n`;
95
+ const manifestBefore = input.read(SKILL_MANIFEST);
96
+ if (manifestBefore !== manifest) {
97
+ writes.push({
98
+ path: SKILL_MANIFEST,
99
+ content: manifest,
100
+ change: manifestBefore === undefined ? "create" : "replace",
101
+ });
102
+ }
103
+ const agentsBefore = input.read(AGENTS_FILE);
104
+ const agents = mergeAgentsSection(agentsBefore);
105
+ if ("refused" in agents) {
106
+ warnings.push(agents.refused);
107
+ }
108
+ else if (agents.text !== agentsBefore) {
109
+ writes.push({
110
+ path: AGENTS_FILE,
111
+ content: agents.text,
112
+ change: agentsBefore === undefined ? "create" : "replace",
113
+ });
114
+ }
115
+ return { writes, warnings };
116
+ }
@@ -1,2 +1,3 @@
1
- export const DOCS_BASE_URL = "https://aventara-docs.vercel.app";
1
+ export const DOCS_BASE_URL = "https://aventara-framework.vercel.app";
2
+ export const EXPOSURE_DOCS = `${DOCS_BASE_URL}/docs/exposure`;
2
3
  export const RATE_LIMITING_DOCS = `${DOCS_BASE_URL}/docs/limits-and-safety#rate-limiting`;
@@ -1,9 +1,11 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { lowestVersionOf } from "../../catalog/range.reader.js";
4
- import { SCAFFOLD_ENTRYPOINT, } from "../template.registry.js";
4
+ import { EXPOSURE_DOCS } from "../docs.links.js";
5
+ import { SCAFFOLD_API, } from "../template.registry.js";
5
6
  const CLIENT_OUTPUT = "src/generated/prisma";
6
7
  const SCHEMA = "prisma/schema.prisma";
8
+ const CONFIG_FILE = "src/aventara.config.ts";
7
9
  const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
8
10
  const DRIVER_PREFIX = "@prisma/adapter-";
9
11
  const NEW_SERVICE = {
@@ -108,21 +110,41 @@ function prismaService(input) {
108
110
  ].join("\n");
109
111
  }
110
112
  function aventaraConfig(input) {
111
- const file = "src/aventara.config.ts";
113
+ const file = CONFIG_FILE;
112
114
  const service = input.service.exportName;
113
115
  return [
116
+ "import type { AvDiagnostic } from '@aventara/core';",
114
117
  `import { createPrismaAdapter } from '${input.entry.adapterPackage}';`,
118
+ "import { Logger } from '@nestjs/common';",
115
119
  `import { discovery } from '${specifier(file, input.artifact.replace(/\.ts$/, ""), input.js)}';`,
116
120
  `import type { ${service} } from '${specifier(file, input.service.file.replace(/\.ts$/, ""), input.js)}';`,
117
121
  "",
122
+ "const logger = new Logger('Aventara');",
123
+ "",
118
124
  "/**",
119
- " * Aventara's configuration: the entrypoint, and the restrictions and",
120
- " * pipelines you add. AventaraModule (app.module.ts) builds the framework from",
121
- " * it; `discovery` is regenerated by the aventara:prepare script.",
125
+ " * Aventara's configuration: where the API is mounted (`api`), and what you add",
126
+ " * to it (`exposed` and `exemptions`, `pipelines`, and the per-Resource",
127
+ " * `resources` tree).",
128
+ " * AventaraModule (app.module.ts) builds the framework from it; `discovery` is",
129
+ " * regenerated by the aventara:prepare script.",
122
130
  " */",
123
131
  `export async function aventaraConfig(prisma: ${service}) {`,
124
132
  " return {",
125
- ` entrypoint: '${SCAFFOLD_ENTRYPOINT}',`,
133
+ ` api: '${SCAFFOLD_API}',`,
134
+ " // Everything the adapter offers is exposed. List what to hide under",
135
+ " // `exemptions`, or set `exposed: false` and list what to expose instead.",
136
+ ` // See: ${EXPOSURE_DOCS}`,
137
+ " exposed: true,",
138
+ " // When something fails inside the server, the caller gets a plain 500 and",
139
+ " // Aventara logs one line naming the request. This logs the error itself,",
140
+ " // with its stack, through Nest's logger.",
141
+ " diagnostics: (diagnostic: AvDiagnostic) => {",
142
+ " const { error } = diagnostic;",
143
+ " logger.error(",
144
+ ` \`\${diagnostic.code} (request \${diagnostic.requestId}): raw error\`,`,
145
+ " error instanceof Error ? error.stack : String(error),",
146
+ " );",
147
+ " },",
126
148
  " adapter: await createPrismaAdapter({",
127
149
  input.service.clientMember === undefined
128
150
  ? " client: prisma,"
@@ -144,6 +166,7 @@ function prepare(input) {
144
166
  `--provider ${input.provider}`,
145
167
  `--driver ${input.entry.drivers[input.provider]}`,
146
168
  `--out ${input.artifact}`,
169
+ `--config ${CONFIG_FILE}`,
147
170
  ].join(" ");
148
171
  }
149
172
  function appModule(service, module, js) {
@@ -191,7 +214,7 @@ function scaffold(input) {
191
214
  content: prismaService({ ...input, clientOutput: CLIENT_OUTPUT, js }),
192
215
  },
193
216
  {
194
- path: "src/aventara.config.ts",
217
+ path: CONFIG_FILE,
195
218
  content: aventaraConfig({
196
219
  ...input,
197
220
  service: NEW_SERVICE,
@@ -239,7 +262,7 @@ function reuse(input) {
239
262
  ]
240
263
  : []),
241
264
  {
242
- path: "src/aventara.config.ts",
265
+ path: CONFIG_FILE,
243
266
  content: aventaraConfig({
244
267
  entry: input.entry,
245
268
  provider: facts.provider,
@@ -1,5 +1,5 @@
1
1
  import { PRISMA7_TEMPLATES } from "./prisma7/prisma7.templates.js";
2
- export const SCAFFOLD_ENTRYPOINT = "/api";
2
+ export const SCAFFOLD_API = "/api";
3
3
  export const TEMPLATE_REGISTRY = {
4
4
  prisma7: PRISMA7_TEMPLATES,
5
5
  };