@basaltkit/ai 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands.js CHANGED
@@ -4,7 +4,7 @@ import { detectProject, nodeReader } from './context/project.js';
4
4
  import { hasErrors, runDoctor } from './doctor/run.js';
5
5
  import { applyFixEdits, fixableIds, planFix, renderFixes } from './doctor/fixes.js';
6
6
  import { createProvider, providerEnvFromProcess } from './provider/factory.js';
7
- import { createPlan } from './plan/plan.js';
7
+ import { createPlan, withGeneratorEntities } from './plan/plan.js';
8
8
  import { renderPlan } from './plan/render.js';
9
9
  import { packageSteps, runMake } from './make/make.js';
10
10
  import { renderMakeResult } from './make/render.js';
@@ -140,7 +140,7 @@ export function aiCommands(options = {}) {
140
140
  io.log('Planning...');
141
141
  io.log('');
142
142
  try {
143
- const plan = await createPlan(provider, ctx, request);
143
+ const plan = withGeneratorEntities(await createPlan(provider, ctx, request), ctx);
144
144
  renderPlan(plan, io);
145
145
  return 0;
146
146
  }
@@ -174,7 +174,8 @@ export function aiCommands(options = {}) {
174
174
  io.log('');
175
175
  let plan;
176
176
  try {
177
- plan = await createPlan(provider, ctx, request);
177
+ // Shown and built with an entity for every generator step.
178
+ plan = withGeneratorEntities(await createPlan(provider, ctx, request), ctx);
178
179
  }
179
180
  catch (error) {
180
181
  io.error(error.message);
@@ -35,9 +35,18 @@ export interface DetectedStack {
35
35
  logger: boolean;
36
36
  ai: boolean;
37
37
  }
38
+ /** A model column as declared: `type` keeps Prisma's spelling minus `?`/`[]`. */
39
+ export interface PrismaColumn {
40
+ name: string;
41
+ type: string;
42
+ optional: boolean;
43
+ list: boolean;
44
+ }
38
45
  export interface PrismaModel {
39
46
  name: string;
40
47
  fields: string[];
48
+ /** The same fields with their types, so a resource can be generated for an existing model. */
49
+ columns?: PrismaColumn[];
41
50
  /** True when the model carries a `tenantId` column (tenant-scoped). */
42
51
  tenantScoped: boolean;
43
52
  /** The `@@map("…")` table name, when the model declares one. */
@@ -506,12 +506,17 @@ function parseModels(schema) {
506
506
  const body = match[2];
507
507
  if (!name || body === undefined)
508
508
  continue;
509
- const fields = body
510
- .split('\n')
511
- .map((line) => line.trim().split(/\s+/)[0])
512
- .filter((token) => !!token && !token.startsWith('@') && !token.startsWith('//'));
509
+ const columns = [];
510
+ for (const line of body.split('\n')) {
511
+ const [field, rawType] = line.trim().split(/\s+/);
512
+ if (!field || field.startsWith('@') || field.startsWith('//'))
513
+ continue;
514
+ const type = (rawType ?? '').replace(/[?[\]]/g, '');
515
+ columns.push({ name: field, type, optional: rawType?.endsWith('?') === true, list: rawType?.endsWith('[]') === true });
516
+ }
517
+ const fields = columns.map((c) => c.name);
513
518
  const table = /@@map\(\s*"([^"]+)"\s*\)/.exec(body)?.[1];
514
- models.push({ name, fields, tenantScoped: fields.includes('tenantId'), ...(table ? { table } : {}) });
519
+ models.push({ name, fields, columns, tenantScoped: fields.includes('tenantId'), ...(table ? { table } : {}) });
515
520
  }
516
521
  return models;
517
522
  }
package/dist/index.d.ts CHANGED
@@ -6,13 +6,13 @@ export { fetchWithRetry, type RetryOptions } from './provider/http.js';
6
6
  export { globalSseFetch, parseSseContent, type SseFetch, type SseResponse } from './provider/sse.js';
7
7
  export { createProvider, providerEnvFrom, providerEnvFromProcess, type CreateProviderOptions, type ProviderEnv, type ProviderName, } from './provider/factory.js';
8
8
  export { runGeneration, generateText, withAbort, throwIfAborted, abortError, isAbortError, type OnProgress, type WorkflowProgress, type WorkflowRunOptions, } from './generate.js';
9
- export { detectProject, nodeReader, memoryReader, type AppFileInfo, type CentralPlatformRouteHint, type DefaultMemoryStore, type DetectedStack, type EnvFileInfo, type FrameworkSchema, type MigrationsInfo, type PrismaInfo, type PrismaModel, type ProjectContext, type ProjectReader, type ServerFileInfo, type SourceHint, type SourceHints, } from './context/project.js';
9
+ export { detectProject, nodeReader, memoryReader, type AppFileInfo, type CentralPlatformRouteHint, type DefaultMemoryStore, type DetectedStack, type EnvFileInfo, type FrameworkSchema, type MigrationsInfo, type PrismaInfo, type PrismaColumn, type PrismaModel, type ProjectContext, type ProjectReader, type ServerFileInfo, type SourceHint, type SourceHints, } from './context/project.js';
10
10
  export { defineRule, type Diagnostic, type DiagnosticCategory, type DoctorRule, type Severity, } from './doctor/types.js';
11
11
  export { DEFAULT_RULES } from './doctor/rules.js';
12
12
  export { hasErrors, runDoctor } from './doctor/run.js';
13
13
  export { applyFixEdits, fixableIds, planFix, renderFixes, FIXES, type FileEdit, type FixApplier, type FixOutcome, type FixStatus, } from './doctor/fixes.js';
14
14
  export { analyze, type AnalysisReport } from './analyze/run.js';
15
- export { createPlan, parsePlan, type CreatePlanOptions } from './plan/plan.js';
15
+ export { createPlan, generatorTargets, parsePlan, withGeneratorEntities, type CreatePlanOptions } from './plan/plan.js';
16
16
  export { buildPlanContext } from './plan/context.js';
17
17
  export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
18
18
  export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog, type EcosystemPackage } from './plan/packages.js';
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ export { applyFixEdits, fixableIds, planFix, renderFixes, FIXES, } from './docto
18
18
  // Analysis report (spec §5, Analyze).
19
19
  export { analyze } from './analyze/run.js';
20
20
  // Planning (spec §3, Plan mode) — natural language → architecture plan.
21
- export { createPlan, parsePlan } from './plan/plan.js';
21
+ export { createPlan, generatorTargets, parsePlan, withGeneratorEntities } from './plan/plan.js';
22
22
  export { buildPlanContext } from './plan/context.js';
23
23
  export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
24
24
  export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog } from './plan/packages.js';
@@ -11,6 +11,6 @@ import type { MakeOptions, MakeResult } from './types.js';
11
11
  * decided. No destructive operations — never overwrites without `force`, never
12
12
  * touches the database, never deletes.
13
13
  */
14
- export declare function runMake(ctx: ProjectContext, plan: ArchitecturePlan, options?: MakeOptions): Promise<MakeResult>;
14
+ export declare function runMake(ctx: ProjectContext, planned: ArchitecturePlan, options?: MakeOptions): Promise<MakeResult>;
15
15
  /** The plan's `package` steps as instructions — ai:make never installs dependencies itself. */
16
16
  export declare function packageSteps(plan: ArchitecturePlan): string[];
package/dist/make/make.js CHANGED
@@ -6,6 +6,7 @@ import { join } from 'node:path';
6
6
  import { unifiedDiff } from './diff.js';
7
7
  import { domainFields, injectPrismaFields, injectZodFields } from './fields.js';
8
8
  import { assertSafePlan } from '../plan/identifiers.js';
9
+ import { withGeneratorEntities } from '../plan/plan.js';
9
10
  import { externalRelationTargets, injectPrismaRelations, inverseRelationLines, relationFieldLines, relationForeignKeys, } from './relations.js';
10
11
  import { injectOpenApiMeta } from './openapi.js';
11
12
  import { renderPermissionsFile } from './permissions.js';
@@ -22,7 +23,9 @@ import { injectAuditPlugin, injectAuditService, injectPermissionGuards } from '.
22
23
  * decided. No destructive operations — never overwrites without `force`, never
23
24
  * touches the database, never deletes.
24
25
  */
25
- export async function runMake(ctx, plan, options = {}) {
26
+ export async function runMake(ctx, planned, options = {}) {
27
+ // Generator steps whose entity the model left out still get built.
28
+ const plan = withGeneratorEntities(planned, ctx);
26
29
  // The plan is untrusted (LLM / MCP client): refuse any name that could
27
30
  // inject code into the generated sources before generating anything.
28
31
  assertSafePlan(plan);
@@ -53,4 +53,6 @@ For generator steps, put the exact command in "command" (e.g. "basalt make:resou
53
53
 
54
54
  ENUMS: a field with a fixed set of values (a status/state) MUST set "enum" to those values — e.g. "estado (pago/pendente)" → { "name": "estado", "type": "String", "enum": ["pago", "pendente"] }. It becomes a validated z.enum([...]) (stored as a String column), never a free z.string().
55
55
 
56
+ ENTITIES vs GENERATOR STEPS: every resource a "generator" step scaffolds MUST also be listed in "entities" with its fields — including when its Prisma model already exists (copy the existing fields). ai:make builds from "entities".
57
+
56
58
  RELATIONS: express a belongs-to as an entry in "relations" (e.g. an Appointment belongs to a Patient → { "name": "patient", "model": "Patient" }). Do NOT also add the "<name>Id" field to "fields" — the FK column, the @relation and the inverse field are generated. Prefer generating both sides of a relation in the same plan.`;
@@ -18,3 +18,13 @@ export interface CreatePlanOptions {
18
18
  export declare function createPlan(provider: AIProvider, ctx: ProjectContext, request: string, options?: CreatePlanOptions): Promise<ArchitecturePlan>;
19
19
  /** Parse + normalize the model's JSON into a validated plan. Tolerant of fences and missing arrays. */
20
20
  export declare function parsePlan(raw: string, request: string): ArchitecturePlan;
21
+ /** Resource names a `basalt make:resource <Name>` generator step scaffolds. */
22
+ export declare function generatorTargets(plan: ArchitecturePlan): string[];
23
+ /**
24
+ * The plan with an entity for every generator step. Models sometimes leave
25
+ * `entities` empty when the Prisma models already exist and only list
26
+ * `make:resource` steps — ai:make then had nothing to build. A missing entity is
27
+ * taken from the existing model (scalar and enum columns; relations and lists
28
+ * are left to the schema), or created bare when there is none.
29
+ */
30
+ export declare function withGeneratorEntities(plan: ArchitecturePlan, ctx: ProjectContext): ArchitecturePlan;
package/dist/plan/plan.js CHANGED
@@ -160,3 +160,41 @@ function normalizeKind(value) {
160
160
  ? value
161
161
  : 'other';
162
162
  }
163
+ /** Resource names a `basalt make:resource <Name>` generator step scaffolds. */
164
+ export function generatorTargets(plan) {
165
+ const names = [];
166
+ for (const step of plan.steps) {
167
+ if (step.kind !== 'generator' || !step.command)
168
+ continue;
169
+ const name = /make:resource\s+(\w+)/.exec(step.command)?.[1];
170
+ if (name && !names.some((n) => n.toLowerCase() === name.toLowerCase()))
171
+ names.push(name);
172
+ }
173
+ return names;
174
+ }
175
+ const PRISMA_SCALARS = new Set(['String', 'Int', 'BigInt', 'Float', 'Decimal', 'Boolean', 'DateTime', 'Json', 'Bytes']);
176
+ /**
177
+ * The plan with an entity for every generator step. Models sometimes leave
178
+ * `entities` empty when the Prisma models already exist and only list
179
+ * `make:resource` steps — ai:make then had nothing to build. A missing entity is
180
+ * taken from the existing model (scalar and enum columns; relations and lists
181
+ * are left to the schema), or created bare when there is none.
182
+ */
183
+ export function withGeneratorEntities(plan, ctx) {
184
+ const known = new Set(plan.entities.map((e) => e.name.toLowerCase()));
185
+ const missing = generatorTargets(plan).filter((name) => !known.has(name.toLowerCase()));
186
+ if (missing.length === 0)
187
+ return plan;
188
+ const models = ctx.prisma?.models ?? [];
189
+ const modelNames = new Set(models.map((m) => m.name));
190
+ const added = missing.map((name) => {
191
+ const model = models.find((m) => m.name.toLowerCase() === name.toLowerCase());
192
+ if (!model)
193
+ return { name, fields: [], tenantScoped: ctx.stack.tenancy };
194
+ const fields = (model.columns ?? [])
195
+ .filter((c) => !c.list && !modelNames.has(c.type))
196
+ .map((c) => ({ name: c.name, type: PRISMA_SCALARS.has(c.type) ? c.type : 'String' }));
197
+ return { name: model.name, fields, tenantScoped: model.tenantScoped };
198
+ });
199
+ return { ...plan, entities: [...plan.entities, ...added] };
200
+ }
@@ -1,3 +1,4 @@
1
+ import { generatorTargets } from './plan.js';
1
2
  /** Render an {@link ArchitecturePlan} in the spec's `ARCHITECTURE PLAN` style. */
2
3
  export function renderPlan(plan, io, options = {}) {
3
4
  io.log('ARCHITECTURE PLAN');
@@ -55,7 +56,7 @@ export function renderPlan(plan, io, options = {}) {
55
56
  io.log('This is a plan only — nothing was changed.');
56
57
  // ai:make only scaffolds entities: suggesting it for a plan without any
57
58
  // would end in "nothing to generate" after a confirmation prompt.
58
- io.log(plan.entities.length > 0
59
+ io.log(plan.entities.length > 0 || generatorTargets(plan).length > 0
59
60
  ? `Next: basalt ai:make "${plan.request}" implements it.`
60
61
  : 'Nothing for ai:make to generate — the steps above are applied by hand.');
61
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/ai",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },