@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 +4 -3
- package/dist/context/project.d.ts +9 -0
- package/dist/context/project.js +10 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/make/make.d.ts +1 -1
- package/dist/make/make.js +4 -1
- package/dist/plan/knowledge.js +2 -0
- package/dist/plan/plan.d.ts +10 -0
- package/dist/plan/plan.js +38 -0
- package/dist/plan/render.js +2 -1
- package/package.json +1 -1
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
|
-
|
|
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. */
|
package/dist/context/project.js
CHANGED
|
@@ -506,12 +506,17 @@ function parseModels(schema) {
|
|
|
506
506
|
const body = match[2];
|
|
507
507
|
if (!name || body === undefined)
|
|
508
508
|
continue;
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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';
|
package/dist/make/make.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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);
|
package/dist/plan/knowledge.js
CHANGED
|
@@ -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.`;
|
package/dist/plan/plan.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/plan/render.js
CHANGED
|
@@ -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
|
}
|