@akanjs/devkit 3.2.0-rc.7 → 3.2.0-rc.8
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/CHANGELOG.md +52 -0
- package/akanApp/akanApp.host.test.ts +76 -0
- package/akanApp/akanApp.host.ts +47 -54
- package/akanApp/devHostPolicy.test.ts +11 -11
- package/akanApp/devHostPolicy.ts +2 -4
- package/akanMcpContract.ts +17 -7
- package/akanMcpToolError.ts +16 -0
- package/codeAgent/skills/akan-debug/SKILL.md +1 -1
- package/codeAgent/skills/akan-mcp/SKILL.md +2 -1
- package/codeAgent/skills/akan-scaffold/SKILL.md +1 -1
- package/codeAgent/skills/akan-validate/SKILL.md +1 -0
- package/codeAgent/tools/McpClient.ts +28 -6
- package/codeAgent/tools/McpToolPack.ts +1 -1
- package/codeAgent/tools/akanCodePlugins.test.ts +13 -1
- package/commandDecorators/cliError.ts +72 -0
- package/commandDecorators/command.ts +112 -57
- package/commandDecorators/commandDecorators.test.ts +92 -6
- package/commandDecorators/exitCodeError.ts +6 -7
- package/commandDecorators/index.ts +1 -0
- package/commandDecorators/targetMeta.ts +1 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +25 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +8 -21
- package/package.json +3 -3
- package/typecheck/projectTypecheck.ts +5 -1
- package/typecheck/typecheck.proc.ts +3 -2
- package/typecheck/typecheckFailure.ts +6 -1
- package/workflow/artifacts.ts +8 -7
- package/workflow/fieldType.ts +92 -0
- package/workflow/index.ts +1 -0
- package/workflow/plan.ts +13 -2
- package/workflow/source.ts +0 -5
|
@@ -44,6 +44,7 @@ export interface TargetOption {
|
|
|
44
44
|
runsOnWorkspaceRoot?: boolean;
|
|
45
45
|
stdio?: boolean;
|
|
46
46
|
failureExitCode?: number;
|
|
47
|
+
/** The exit of a `usage` or `unknownTarget` failure, for a command whose 2 already answers something else. */
|
|
47
48
|
usageExitCode?: number;
|
|
48
49
|
/** It deletes files in its app, lib, pkg or module, so one left out or unknown is refused, never picked or asked. */
|
|
49
50
|
destructive?: boolean;
|
|
@@ -68,6 +68,31 @@ describe("IncrementalBuilderHost", () => {
|
|
|
68
68
|
host.stop();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
test("a stop while a restart is pending leaves the builder down once the delay passes", async () => {
|
|
72
|
+
const { spawns, host } = startRecordingHost();
|
|
73
|
+
spawns[0]?.options.onExit?.();
|
|
74
|
+
expect(host.status).toBe("restarting");
|
|
75
|
+
|
|
76
|
+
host.stop();
|
|
77
|
+
await wait(1_050);
|
|
78
|
+
expect(spawns).toHaveLength(1);
|
|
79
|
+
expect(host.status).toBe("stopped");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a builder that crashes again after coming back ready waits the first delay again", async () => {
|
|
83
|
+
const { spawns, host } = startRecordingHost();
|
|
84
|
+
spawns[0]?.options.onExit?.();
|
|
85
|
+
await wait(1_050);
|
|
86
|
+
expect(spawns).toHaveLength(2);
|
|
87
|
+
|
|
88
|
+
spawns[1]?.options.ipc?.({ type: "builder-ready" });
|
|
89
|
+
spawns[1]?.options.onExit?.();
|
|
90
|
+
await wait(1_050);
|
|
91
|
+
expect(spawns).toHaveLength(3);
|
|
92
|
+
|
|
93
|
+
host.stop();
|
|
94
|
+
});
|
|
95
|
+
|
|
71
96
|
test("recycles a ready builder gracefully and replaces it immediately", async () => {
|
|
72
97
|
const spawns = mockSpawns();
|
|
73
98
|
const onRestartReady = mock();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { Logger } from "akanjs/common";
|
|
3
|
+
import { Logger, RestartSchedule } from "akanjs/common";
|
|
4
4
|
import type { BuilderMessage, BuilderRequest } from "akanjs/server";
|
|
5
5
|
import { CSR_DEV_DIRNAME, CSR_DEV_PATCHING_MARKER, SSR_DEV_DIRNAME } from "akanjs/server/hmr/csrDevManifest";
|
|
6
6
|
import { MemoryLimit } from "akanjs/server/memoryLimit";
|
|
@@ -49,8 +49,6 @@ interface IncrementalBuilderStartOptions {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export class IncrementalBuilderHost {
|
|
52
|
-
static readonly #restartBaseDelayMs = 1_000;
|
|
53
|
-
static readonly #restartMaxDelayMs = 30_000;
|
|
54
52
|
static readonly #recycleDrainTimeoutMs = 30_000;
|
|
55
53
|
// apps/akan's builder peaks near 950MB holding the SSR registry's patcher through a route build, and arming CSR adds
|
|
56
54
|
// a second patcher: this recycles only a builder grown well past both.
|
|
@@ -65,8 +63,7 @@ export class IncrementalBuilderHost {
|
|
|
65
63
|
readonly #onOutput: ((kind: "stdout" | "stderr", text: string) => void) | null;
|
|
66
64
|
#proc: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe"> | null = null;
|
|
67
65
|
#status: IncrementalBuilderStatus = "stopped";
|
|
68
|
-
#
|
|
69
|
-
#restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
readonly #restart = new RestartSchedule({ min: 1_000, max: 30_000 });
|
|
70
67
|
#recycleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
71
68
|
#recycleRequested: boolean = false;
|
|
72
69
|
#spawnAfterRecycle: boolean = false;
|
|
@@ -136,7 +133,7 @@ export class IncrementalBuilderHost {
|
|
|
136
133
|
if (msg.type === "builder-ready" && !this.ready) {
|
|
137
134
|
this.ready = true;
|
|
138
135
|
this.#status = "ready";
|
|
139
|
-
this.#
|
|
136
|
+
this.#restart.reset();
|
|
140
137
|
if (isRestart) this.#startOptions.onRestartReady?.();
|
|
141
138
|
else this.#startOptions.onReady?.();
|
|
142
139
|
}
|
|
@@ -210,20 +207,13 @@ export class IncrementalBuilderHost {
|
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
209
|
#scheduleRestart() {
|
|
213
|
-
if (this.#manualStop || this.#
|
|
210
|
+
if (this.#manualStop || this.#restart.pending) return;
|
|
214
211
|
this.#status = "restarting";
|
|
215
|
-
const
|
|
216
|
-
const delay = Math.min(
|
|
217
|
-
IncrementalBuilderHost.#restartBaseDelayMs * 2 ** attempt,
|
|
218
|
-
IncrementalBuilderHost.#restartMaxDelayMs,
|
|
219
|
-
);
|
|
220
|
-
this.#restartAttempts = attempt + 1;
|
|
221
|
-
this.logger.warn(`builder exited after ready; restarting in ${delay}ms (attempt ${this.#restartAttempts})`);
|
|
222
|
-
this.#restartTimer = setTimeout(() => {
|
|
223
|
-
this.#restartTimer = null;
|
|
212
|
+
const delay = this.#restart.schedule(() => {
|
|
224
213
|
if (this.#manualStop) return;
|
|
225
214
|
this.#spawn(true);
|
|
226
|
-
}
|
|
215
|
+
});
|
|
216
|
+
this.logger.warn(`builder exited after ready; restarting in ${delay}ms (attempt ${this.#restart.attempts})`);
|
|
227
217
|
}
|
|
228
218
|
/** Drain-and-exit rather than `kill()`, so no rebuild is truncated; false when the request was not sent. */
|
|
229
219
|
recycle(reason: string): boolean {
|
|
@@ -294,10 +284,7 @@ export class IncrementalBuilderHost {
|
|
|
294
284
|
this.#manualStop = true;
|
|
295
285
|
this.#clearRecycle();
|
|
296
286
|
this.#failInFlight("builder was stopped before answering");
|
|
297
|
-
|
|
298
|
-
clearTimeout(this.#restartTimer);
|
|
299
|
-
this.#restartTimer = null;
|
|
300
|
-
}
|
|
287
|
+
this.#restart.cancel();
|
|
301
288
|
if (this.#proc) this.#proc.kill();
|
|
302
289
|
this.#proc = null;
|
|
303
290
|
this.ready = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.2.0-rc.
|
|
3
|
+
"version": "3.2.0-rc.8",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"./*": "./*.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@akanjs/language": "3.2.0-rc.
|
|
44
|
+
"@akanjs/language": "3.2.0-rc.8",
|
|
45
45
|
"@earendil-works/pi-agent-core": "1.3.0",
|
|
46
46
|
"@earendil-works/pi-ai": "1.3.0",
|
|
47
47
|
"@earendil-works/pi-codemode": "1.3.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@earendil-works/pi-tui": "1.3.0",
|
|
51
51
|
"@inquirer/prompts": "^8.7.3",
|
|
52
52
|
"@tailwindcss/node": "^4.3.3",
|
|
53
|
-
"akanjs": "3.2.0-rc.
|
|
53
|
+
"akanjs": "3.2.0-rc.8",
|
|
54
54
|
"chalk": "^6.0.1",
|
|
55
55
|
"commander": "^15.0.0",
|
|
56
56
|
"dayjs": "^1.11.23",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, rm } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { CliError } from "../commandDecorators/cliError";
|
|
3
4
|
import type { TypecheckRecord } from "../typeChecker";
|
|
4
5
|
import { TypecheckFailure } from "./typecheckFailure";
|
|
5
6
|
|
|
@@ -69,7 +70,10 @@ export class ProjectTypecheck {
|
|
|
69
70
|
if (exitCode === 0) return;
|
|
70
71
|
const records = report === "records" ? ProjectTypecheck.#parseRecords(stdout) : null;
|
|
71
72
|
if (records?.length) throw new TypecheckFailure(records);
|
|
72
|
-
|
|
73
|
+
const output = (stderr || stdout).trim();
|
|
74
|
+
if (exitCode === TypecheckFailure.workerExitCode && report === "pretty")
|
|
75
|
+
throw new CliError("checkFailed", output || "Typecheck failed");
|
|
76
|
+
throw new Error(output || `Typecheck failed with exit code ${exitCode}`);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
static #parseRecords(stdout: string): TypecheckRecord[] | null {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from "akanjs/common";
|
|
2
2
|
import { TypeChecker } from "../typeChecker";
|
|
3
|
+
import { TypecheckFailure } from "./typecheckFailure";
|
|
3
4
|
|
|
4
5
|
try {
|
|
5
6
|
const filePath = process.env.AKAN_TYPECHECK_FILE;
|
|
@@ -28,11 +29,11 @@ try {
|
|
|
28
29
|
//? Not process.cwd(): that is the realpath (/private/var on macOS) while TypeScript keeps the spelling it was given.
|
|
29
30
|
const root = process.env.AKAN_TYPECHECK_ROOT ?? process.cwd();
|
|
30
31
|
process.stdout.write(JSON.stringify(TypeChecker.toRecords(result.errors, root)));
|
|
31
|
-
process.exit(result.errors.length > 0 ?
|
|
32
|
+
process.exit(result.errors.length > 0 ? TypecheckFailure.workerExitCode : 0);
|
|
32
33
|
}
|
|
33
34
|
if (result.errors.length > 0) {
|
|
34
35
|
console.error(result.message);
|
|
35
|
-
process.exit(
|
|
36
|
+
process.exit(TypecheckFailure.workerExitCode);
|
|
36
37
|
}
|
|
37
38
|
} catch (error) {
|
|
38
39
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { CliError } from "../commandDecorators/cliError";
|
|
1
2
|
import type { TypecheckRecord } from "../typeChecker";
|
|
2
3
|
|
|
3
|
-
export class TypecheckFailure extends
|
|
4
|
+
export class TypecheckFailure extends CliError<"checkFailed"> {
|
|
5
|
+
//? The worker's exit when the project has type errors, so its crash, which exits 1, is told apart.
|
|
6
|
+
static readonly workerExitCode = 3;
|
|
7
|
+
|
|
4
8
|
constructor(readonly records: TypecheckRecord[]) {
|
|
5
9
|
super(
|
|
10
|
+
"checkFailed",
|
|
6
11
|
records
|
|
7
12
|
.slice(0, 20)
|
|
8
13
|
.map((record) => `${record.file ?? "-"}:${record.line}:${record.col} ${record.code} ${record.message}`)
|
package/workflow/artifacts.ts
CHANGED
|
@@ -95,11 +95,11 @@ export const createWorkflowApplyReport = ({
|
|
|
95
95
|
postApplyChecks?: WorkflowApplyReport["postApplyChecks"];
|
|
96
96
|
recommendations?: WorkflowApplyReport["recommendations"];
|
|
97
97
|
}): WorkflowApplyReport => {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
action: inferNextActionCode(action, diagnostics)
|
|
102
|
-
|
|
98
|
+
const status = workflowStatus(diagnostics);
|
|
99
|
+
const validationCommands = status === "failed" ? [] : (recommendedValidationCommands ?? commands);
|
|
100
|
+
const nextActionsWithIntent = nextActions
|
|
101
|
+
.map((action) => ({ ...action, action: inferNextActionCode(action, diagnostics) }))
|
|
102
|
+
.filter((action) => status !== "failed" || action.action !== "validate");
|
|
103
103
|
if (sourceChangeBlocked(diagnostics) && !nextActionsWithIntent.some((action) => action.action === "blocked")) {
|
|
104
104
|
nextActionsWithIntent.unshift({
|
|
105
105
|
command: `akan workflow explain ${workflow}`,
|
|
@@ -111,12 +111,13 @@ export const createWorkflowApplyReport = ({
|
|
|
111
111
|
(left, right) => nextActionPriority(left, diagnostics) - nextActionPriority(right, diagnostics),
|
|
112
112
|
);
|
|
113
113
|
const sourceFilesChanged = uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
|
|
114
|
-
const generatedFilesSynced =
|
|
114
|
+
const generatedFilesSynced =
|
|
115
|
+
status === "failed" ? [] : uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
|
|
115
116
|
return {
|
|
116
117
|
schemaVersion: 1,
|
|
117
118
|
workflow,
|
|
118
119
|
mode,
|
|
119
|
-
status
|
|
120
|
+
status,
|
|
120
121
|
summary: {
|
|
121
122
|
sourceFilesChanged,
|
|
122
123
|
generatedFilesSynced,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { Sys } from "../commandDecorators";
|
|
3
|
+
import { ensureBaseImport, ensureNamedImport, getSysRoot, moduleComponentName, moduleSourcePaths } from "./source";
|
|
4
|
+
import { sourceFileFor } from "./sourceAst";
|
|
5
|
+
import type { WorkflowDiagnostic } from "./types";
|
|
6
|
+
|
|
7
|
+
const builtInFieldTypes = new Set(["String", "Boolean", "Date", "Map"]);
|
|
8
|
+
const baseFieldTypes = new Set(["Int", "Float", "ID", "Any", "Binary", "ImageResult"]);
|
|
9
|
+
const fieldTypeReference = /^\[*([A-Za-z_$][\w$]*)((?:\.[A-Za-z_$][\w$]*)*)\]*$/;
|
|
10
|
+
|
|
11
|
+
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
12
|
+
const [, name = "", member] = fieldTypeReference.exec(typeName) ?? [];
|
|
13
|
+
if (member || !baseFieldTypes.has(name)) return content;
|
|
14
|
+
return ensureBaseImport(content, name);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const valueNamesOf = (content: string) => {
|
|
18
|
+
const names = new Set<string>();
|
|
19
|
+
for (const statement of sourceFileFor("source.ts", content).statements) {
|
|
20
|
+
if (ts.isImportDeclaration(statement)) {
|
|
21
|
+
const clause = statement.importClause;
|
|
22
|
+
if (!clause || clause.isTypeOnly) continue;
|
|
23
|
+
if (clause.name) names.add(clause.name.text);
|
|
24
|
+
const bindings = clause.namedBindings;
|
|
25
|
+
if (bindings && ts.isNamespaceImport(bindings)) names.add(bindings.name.text);
|
|
26
|
+
else for (const element of bindings?.elements ?? []) if (!element.isTypeOnly) names.add(element.name.text);
|
|
27
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
28
|
+
for (const declaration of statement.declarationList.declarations)
|
|
29
|
+
if (ts.isIdentifier(declaration.name)) names.add(declaration.name.text);
|
|
30
|
+
} else if (
|
|
31
|
+
(ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isFunctionDeclaration(statement)) &&
|
|
32
|
+
statement.name
|
|
33
|
+
)
|
|
34
|
+
names.add(statement.name.text);
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const fieldTypeSource = async (
|
|
40
|
+
sys: Sys,
|
|
41
|
+
module: string,
|
|
42
|
+
typeName: string,
|
|
43
|
+
constantContent: string,
|
|
44
|
+
): Promise<{ import?: { name: string; from: string }; diagnostic?: WorkflowDiagnostic }> => {
|
|
45
|
+
const reference = fieldTypeReference.exec(typeName);
|
|
46
|
+
if (!reference)
|
|
47
|
+
return {
|
|
48
|
+
diagnostic: {
|
|
49
|
+
severity: "error",
|
|
50
|
+
code: "primitive-field-type-unknown",
|
|
51
|
+
input: "type",
|
|
52
|
+
message: `Field type "${typeName}" is not a type name field() takes. Name one type, such as Address, or a list of one, such as [Address], then plan this again.`,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const [, name = "", member] = reference;
|
|
56
|
+
if (builtInFieldTypes.has(name) || baseFieldTypes.has(name) || valueNamesOf(constantContent).has(name)) return {};
|
|
57
|
+
const [scalars, models] = await Promise.all([sys.getScalarModules(), sys.getDatabaseModules()]);
|
|
58
|
+
const scalar = member ? undefined : scalars.find((scalarName) => moduleComponentName(scalarName) === name);
|
|
59
|
+
if (scalar) return { import: { name, from: `../__scalar/${scalar}/${scalar}.constant` } };
|
|
60
|
+
const model = member ? undefined : models.find((modelName) => moduleComponentName(modelName) === name);
|
|
61
|
+
if (model) return { import: { name, from: `../${model}/${model}.constant` } };
|
|
62
|
+
const root = getSysRoot(sys);
|
|
63
|
+
const constantPath = `${root}/${moduleSourcePaths(module).constant}`;
|
|
64
|
+
const named = (kind: string, names: string[]) =>
|
|
65
|
+
names.length ? `Its ${kind} are ${names.map(moduleComponentName).join(", ")}.` : `It has no ${kind}.`;
|
|
66
|
+
return {
|
|
67
|
+
diagnostic: {
|
|
68
|
+
severity: "error",
|
|
69
|
+
code: "primitive-field-type-unknown",
|
|
70
|
+
input: "type",
|
|
71
|
+
message: [
|
|
72
|
+
`Field type "${typeName}" names ${name}, which is not a built-in or akanjs/base type, not imported by ${constantPath}, and not a scalar or model in ${root}.`,
|
|
73
|
+
named("scalars", scalars),
|
|
74
|
+
named("models", models),
|
|
75
|
+
`Create the scalar with the create-scalar workflow first, or import ${name} into ${constantPath}, then plan this again.`,
|
|
76
|
+
].join(" "),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const withFieldTypeImport = async (
|
|
82
|
+
sys: Sys,
|
|
83
|
+
module: string,
|
|
84
|
+
typeName: string,
|
|
85
|
+
content: string,
|
|
86
|
+
diagnostics: WorkflowDiagnostic[],
|
|
87
|
+
) => {
|
|
88
|
+
const withBaseImport = ensureBaseTypeImport(content, typeName);
|
|
89
|
+
const source = await fieldTypeSource(sys, module, typeName, withBaseImport);
|
|
90
|
+
if (source.diagnostic) diagnostics.push(source.diagnostic);
|
|
91
|
+
return source.import ? ensureNamedImport(withBaseImport, source.import.from, source.import.name) : withBaseImport;
|
|
92
|
+
};
|
package/workflow/index.ts
CHANGED
package/workflow/plan.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { capitalize } from "akanjs/common";
|
|
|
2
2
|
import type { Sys, Workspace } from "../commandDecorators";
|
|
3
3
|
import { withoutApplyHints, workflowPlanApproval } from "./artifacts";
|
|
4
4
|
import { resolveWorkflowSys } from "./executor";
|
|
5
|
+
import { fieldTypeSource } from "./fieldType";
|
|
5
6
|
import { InteractionScaffold } from "./interactionScaffold";
|
|
6
7
|
import { KeyRename } from "./keyRename";
|
|
7
8
|
import { MutationScaffold } from "./mutationScaffold";
|
|
8
9
|
import { identifierPattern, sysNamePattern } from "./plainName";
|
|
9
|
-
import { coerceFieldDefault, getSysRoot, moduleSourcePaths } from "./source";
|
|
10
|
+
import { coerceFieldDefault, getSysRoot, moduleSourcePaths, normalizeFieldType } from "./source";
|
|
10
11
|
import type {
|
|
11
12
|
WorkflowDiagnostic,
|
|
12
13
|
WorkflowInputSpec,
|
|
@@ -541,6 +542,13 @@ const moduleNotDatabase = async (sys: Sys, spec: WorkflowSpec, module: string):
|
|
|
541
542
|
};
|
|
542
543
|
};
|
|
543
544
|
|
|
545
|
+
const fieldTypeUnknown = async (sys: Sys, spec: WorkflowSpec, module: string, type: string | null) => {
|
|
546
|
+
if (spec.name !== "add-field" || !type || ["enum", "number", "numeric", "upload"].includes(type.toLowerCase()))
|
|
547
|
+
return null;
|
|
548
|
+
const constant = await sys.readFile(moduleSourcePaths(module).constant);
|
|
549
|
+
return (await fieldTypeSource(sys, module, normalizeFieldType(type), constant)).diagnostic ?? null;
|
|
550
|
+
};
|
|
551
|
+
|
|
544
552
|
export const workflowTargetDiagnostics = async (
|
|
545
553
|
workspace: Workspace,
|
|
546
554
|
spec: WorkflowSpec,
|
|
@@ -563,7 +571,10 @@ export const workflowTargetDiagnostics = async (
|
|
|
563
571
|
const module = planString(inputs, "module");
|
|
564
572
|
const moduleSpec: WorkflowInputSpec | undefined = spec.inputs.module;
|
|
565
573
|
if (!module || !moduleSpec?.mustExist) return [];
|
|
566
|
-
const refused =
|
|
574
|
+
const refused =
|
|
575
|
+
(await moduleMissing(workspace, sys, module)) ??
|
|
576
|
+
(await moduleNotDatabase(sys, spec, module)) ??
|
|
577
|
+
(await fieldTypeUnknown(sys, spec, module, planString(inputs, "type")));
|
|
567
578
|
return refused ? [refused] : [];
|
|
568
579
|
};
|
|
569
580
|
|
package/workflow/source.ts
CHANGED
|
@@ -238,11 +238,6 @@ export const ensureImportDeclaration = (content: string, moduleSpecifier: string
|
|
|
238
238
|
|
|
239
239
|
export const ensureBaseImport = (content: string, name: string) => ensureNamedImport(content, "akanjs/base", name);
|
|
240
240
|
|
|
241
|
-
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
242
|
-
if (typeName !== "Int" && typeName !== "Float") return content;
|
|
243
|
-
return ensureBaseImport(content, typeName);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
241
|
export type FieldDefaultValue = string | number | boolean | null;
|
|
247
242
|
|
|
248
243
|
const numericDefault = (typeName: "Int" | "Float", rawDefault: FieldDefaultValue): string | null => {
|