@elaraai/create-e3 1.0.3 → 1.0.5
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/bin/create-e3.mjs +10 -0
- package/dist/index.js +159 -20
- package/package.json +3 -2
- package/templates/e3/README.md +30 -12
- package/templates/e3/eslint.config.js +12 -4
- package/templates/e3/package.json +7 -1
- package/templates/e3/pyproject.toml +2 -3
- package/templates/e3/src/index.spec.ts +7 -7
- package/templates/e3/src/index.ts +14 -8
- package/templates/e3/src/index.ui.ts +22 -0
- package/templates/e3/src/surface.tsx +16 -0
- package/templates/e3/template.json +55 -0
- package/templates/e3/tsconfig.json +4 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
4
|
+
* Licensed under AGPL-3.0-or-later. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Static bin shim — exists in fresh checkout so pnpm install can create
|
|
7
|
+
* the bin symlink BEFORE the package is built. Dynamic-imports the
|
|
8
|
+
* built CLI entrypoint; argv flows through via process.argv.
|
|
9
|
+
*/
|
|
10
|
+
await import('../dist/index.js');
|
package/dist/index.js
CHANGED
|
@@ -39,15 +39,38 @@ var DOTFILE_RENAMES = {
|
|
|
39
39
|
gitignore: ".gitignore",
|
|
40
40
|
npmrc: ".npmrc"
|
|
41
41
|
};
|
|
42
|
+
var MANIFEST_FILE = "template.json";
|
|
42
43
|
function substituteTokens(content, names) {
|
|
43
44
|
return content.replaceAll("__PROJECT_NAME__", names.projectName).replaceAll("__DISPLAY_NAME__", names.displayName).replaceAll("__WORKSPACE_NAME__", names.workspaceName);
|
|
44
45
|
}
|
|
45
|
-
function transformPackageJson(raw, names, version) {
|
|
46
|
+
function transformPackageJson(raw, names, version, manifest, enabled) {
|
|
46
47
|
const pkg = JSON.parse(raw);
|
|
47
48
|
pkg.name = `@elaraai/${names.projectName}`;
|
|
48
49
|
pkg.description = names.displayName;
|
|
49
50
|
pkg.version = "0.0.1";
|
|
50
51
|
delete pkg.private;
|
|
52
|
+
if (manifest) {
|
|
53
|
+
const deps = pkg.dependencies;
|
|
54
|
+
const devDeps = pkg.devDependencies;
|
|
55
|
+
const scripts = pkg.scripts;
|
|
56
|
+
for (const [feature, spec] of Object.entries(manifest.features)) {
|
|
57
|
+
if (enabled(feature))
|
|
58
|
+
continue;
|
|
59
|
+
for (const d of spec.dependencies ?? [])
|
|
60
|
+
delete deps?.[d];
|
|
61
|
+
for (const d of spec.devDependencies ?? [])
|
|
62
|
+
delete devDeps?.[d];
|
|
63
|
+
for (const s of spec.scripts ?? [])
|
|
64
|
+
delete scripts?.[s];
|
|
65
|
+
}
|
|
66
|
+
for (const [name, variants] of Object.entries(manifest.scriptVariants ?? {})) {
|
|
67
|
+
const match = variants.find((v) => v.when.every(enabled));
|
|
68
|
+
if (match)
|
|
69
|
+
scripts[name] = match.value;
|
|
70
|
+
else
|
|
71
|
+
delete scripts?.[name];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
51
74
|
const pin = `^${version}`;
|
|
52
75
|
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
53
76
|
const deps = pkg[field];
|
|
@@ -77,6 +100,12 @@ function extOf(path) {
|
|
|
77
100
|
const dot = base.lastIndexOf(".");
|
|
78
101
|
return dot <= 0 ? "" : base.slice(dot);
|
|
79
102
|
}
|
|
103
|
+
function loadManifest(templateDir) {
|
|
104
|
+
const path = join(templateDir, MANIFEST_FILE);
|
|
105
|
+
if (!existsSync(path))
|
|
106
|
+
return null;
|
|
107
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
108
|
+
}
|
|
80
109
|
function scaffold(options) {
|
|
81
110
|
const { kind, name, templateDir, version } = options;
|
|
82
111
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -84,6 +113,23 @@ function scaffold(options) {
|
|
|
84
113
|
if (!existsSync(templateDir)) {
|
|
85
114
|
throw new Error(`Template directory not found: ${templateDir}`);
|
|
86
115
|
}
|
|
116
|
+
const manifest = loadManifest(templateDir);
|
|
117
|
+
const enabled = (feature) => options.features?.[feature] ?? manifest?.features[feature]?.default ?? true;
|
|
118
|
+
const skip = /* @__PURE__ */ new Set();
|
|
119
|
+
const renames = {};
|
|
120
|
+
if (manifest) {
|
|
121
|
+
for (const [feature, spec] of Object.entries(manifest.features)) {
|
|
122
|
+
if (enabled(feature)) {
|
|
123
|
+
for (const f of spec.disable ?? [])
|
|
124
|
+
skip.add(f);
|
|
125
|
+
for (const [src, dest] of Object.entries(spec.rename ?? {}))
|
|
126
|
+
renames[src] = dest;
|
|
127
|
+
} else {
|
|
128
|
+
for (const f of spec.files ?? [])
|
|
129
|
+
skip.add(f);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
87
133
|
const names = deriveNames(name, cwd);
|
|
88
134
|
const inPlace = name === ".";
|
|
89
135
|
const projectDir = inPlace ? cwd : join(cwd, names.projectName);
|
|
@@ -92,17 +138,21 @@ function scaffold(options) {
|
|
|
92
138
|
}
|
|
93
139
|
mkdirSync(projectDir, { recursive: true });
|
|
94
140
|
for (const srcPath of walk(templateDir)) {
|
|
95
|
-
const rel = relative(templateDir, srcPath);
|
|
96
|
-
|
|
141
|
+
const rel = relative(templateDir, srcPath).replaceAll("\\", "/");
|
|
142
|
+
if (rel === MANIFEST_FILE)
|
|
143
|
+
continue;
|
|
144
|
+
if (skip.has(rel))
|
|
145
|
+
continue;
|
|
146
|
+
const destRel = renames[rel] ?? rel;
|
|
147
|
+
const segments = destRel.split("/");
|
|
97
148
|
const last = segments[segments.length - 1];
|
|
98
149
|
segments[segments.length - 1] = DOTFILE_RENAMES[last] ?? last;
|
|
99
|
-
const
|
|
100
|
-
const destPath = join(projectDir, destRel);
|
|
150
|
+
const destPath = join(projectDir, segments.join("/"));
|
|
101
151
|
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
102
152
|
const baseName = segments[segments.length - 1];
|
|
103
153
|
if (baseName === "package.json") {
|
|
104
|
-
const
|
|
105
|
-
writeFileSync(destPath, substituteTokens(
|
|
154
|
+
const transformed = transformPackageJson(readFileSync(srcPath, "utf8"), names, version, manifest, enabled);
|
|
155
|
+
writeFileSync(destPath, substituteTokens(transformed, names));
|
|
106
156
|
} else if (TEXT_REPLACE_EXT.has(extOf(srcPath)) || baseName.startsWith(".")) {
|
|
107
157
|
writeFileSync(destPath, substituteTokens(readFileSync(srcPath, "utf8"), names));
|
|
108
158
|
} else {
|
|
@@ -111,7 +161,7 @@ function scaffold(options) {
|
|
|
111
161
|
}
|
|
112
162
|
log(`Created ${names.projectName} (${kind}) at ${projectDir}`);
|
|
113
163
|
if (options.install) {
|
|
114
|
-
runInstall(kind, projectDir, log);
|
|
164
|
+
runInstall(kind, projectDir, enabled, log);
|
|
115
165
|
}
|
|
116
166
|
return { ...names, projectDir, inPlace };
|
|
117
167
|
}
|
|
@@ -119,14 +169,14 @@ function hasCommand(cmd) {
|
|
|
119
169
|
const probe = process.platform === "win32" ? "where" : "which";
|
|
120
170
|
return spawnSync(probe, [cmd], { stdio: "ignore" }).status === 0;
|
|
121
171
|
}
|
|
122
|
-
function runInstall(kind, projectDir, log) {
|
|
172
|
+
function runInstall(kind, projectDir, enabled, log) {
|
|
123
173
|
log("Installing Node dependencies (npm install)...");
|
|
124
174
|
const npm = spawnSync("npm", ["install"], { cwd: projectDir, stdio: "inherit", shell: process.platform === "win32" });
|
|
125
175
|
if (npm.status !== 0) {
|
|
126
176
|
log("npm install failed \u2014 fix the issue and re-run `npm install`.");
|
|
127
177
|
return;
|
|
128
178
|
}
|
|
129
|
-
if (kind === "e3") {
|
|
179
|
+
if (kind === "e3" && enabled("runner:east-py")) {
|
|
130
180
|
if (hasCommand("uv")) {
|
|
131
181
|
log("Installing Python dependencies (uv sync)...");
|
|
132
182
|
const uv = spawnSync("uv", ["sync"], { cwd: projectDir, stdio: "inherit", shell: process.platform === "win32" });
|
|
@@ -139,15 +189,15 @@ function runInstall(kind, projectDir, log) {
|
|
|
139
189
|
}
|
|
140
190
|
|
|
141
191
|
// ../scaffold-core/dist/cli.js
|
|
142
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
192
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
143
193
|
import { dirname, join as join2 } from "node:path";
|
|
194
|
+
import { createInterface } from "node:readline/promises";
|
|
144
195
|
import { fileURLToPath } from "node:url";
|
|
145
|
-
function runCreateCli(kind, moduleUrl) {
|
|
196
|
+
async function runCreateCli(kind, moduleUrl) {
|
|
146
197
|
const pkgRoot = join2(dirname(fileURLToPath(moduleUrl)), "..");
|
|
147
198
|
const args = process.argv.slice(2);
|
|
148
199
|
if (args.includes("--help") || args.includes("-h")) {
|
|
149
|
-
|
|
150
|
-
console.log(` create-${kind} <project-name|.> [--install|--no-install]`);
|
|
200
|
+
printHelp(kind);
|
|
151
201
|
return;
|
|
152
202
|
}
|
|
153
203
|
const version = JSON.parse(readFileSync2(join2(pkgRoot, "package.json"), "utf8")).version;
|
|
@@ -155,22 +205,111 @@ function runCreateCli(kind, moduleUrl) {
|
|
|
155
205
|
const name = args.find((a) => !a.startsWith("-")) ?? ".";
|
|
156
206
|
const install = args.includes("--install") ? true : args.includes("--no-install") ? false : Boolean(process.stdout.isTTY);
|
|
157
207
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
208
|
+
const features = await resolveFeatures(templateDir, args);
|
|
209
|
+
const result = scaffold({ kind, name, templateDir, version, install, features });
|
|
210
|
+
printNextSteps(kind, result.projectName, result.inPlace, install, features);
|
|
160
211
|
} catch (err) {
|
|
161
212
|
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
213
|
process.exit(1);
|
|
163
214
|
}
|
|
164
215
|
}
|
|
165
|
-
function
|
|
216
|
+
async function resolveFeatures(templateDir, args) {
|
|
217
|
+
const manifestPath = join2(templateDir, "template.json");
|
|
218
|
+
if (!existsSync2(manifestPath))
|
|
219
|
+
return {};
|
|
220
|
+
const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
221
|
+
const features = {};
|
|
222
|
+
for (const [key, spec] of Object.entries(manifest.features))
|
|
223
|
+
features[key] = spec.default ?? true;
|
|
224
|
+
const runnerKeys = Object.keys(manifest.features).filter((k) => k.startsWith("runner:"));
|
|
225
|
+
const runnerName = (key) => key.slice("runner:".length);
|
|
226
|
+
const runnersFlag = args.find((a) => a.startsWith("--runners="));
|
|
227
|
+
const selectionFlags = ["--tests", "--no-tests", "--ui", "--no-ui", "--eslint", "--no-eslint"].some((f) => args.includes(f)) || Boolean(runnersFlag);
|
|
228
|
+
if (args.includes("--tests"))
|
|
229
|
+
features["tests"] = true;
|
|
230
|
+
if (args.includes("--no-tests"))
|
|
231
|
+
features["tests"] = false;
|
|
232
|
+
if (args.includes("--ui"))
|
|
233
|
+
features["ui"] = true;
|
|
234
|
+
if (args.includes("--no-ui"))
|
|
235
|
+
features["ui"] = false;
|
|
236
|
+
if (args.includes("--eslint"))
|
|
237
|
+
features["eslint"] = true;
|
|
238
|
+
if (args.includes("--no-eslint"))
|
|
239
|
+
features["eslint"] = false;
|
|
240
|
+
if (args.includes("--editor-diagnostics"))
|
|
241
|
+
features["editor-diagnostics"] = true;
|
|
242
|
+
if (args.includes("--no-editor-diagnostics"))
|
|
243
|
+
features["editor-diagnostics"] = false;
|
|
244
|
+
if (runnersFlag) {
|
|
245
|
+
const chosen = new Set(runnersFlag.slice("--runners=".length).split(",").map((s) => s.trim()).filter(Boolean));
|
|
246
|
+
for (const key of runnerKeys)
|
|
247
|
+
features[key] = chosen.has(runnerName(key));
|
|
248
|
+
}
|
|
249
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !selectionFlags;
|
|
250
|
+
if (!interactive)
|
|
251
|
+
return features;
|
|
252
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
253
|
+
try {
|
|
254
|
+
if ("tests" in manifest.features) {
|
|
255
|
+
features["tests"] = await askYesNo(rl, "Include tests?", features["tests"]);
|
|
256
|
+
}
|
|
257
|
+
if ("ui" in manifest.features) {
|
|
258
|
+
features["ui"] = await askYesNo(rl, "Include UI components (east-ui + e3-ui)?", features["ui"]);
|
|
259
|
+
}
|
|
260
|
+
if (runnerKeys.length > 0) {
|
|
261
|
+
const defaults = runnerKeys.filter((k) => features[k]).map(runnerName);
|
|
262
|
+
const answer = (await rl.question(`Runners to include (comma-separated) [${defaults.join(",")}]: `)).trim();
|
|
263
|
+
const picks = new Set(answer ? answer.split(",").map((s) => s.trim()).filter(Boolean) : defaults);
|
|
264
|
+
for (const key of runnerKeys)
|
|
265
|
+
features[key] = picks.has(runnerName(key));
|
|
266
|
+
}
|
|
267
|
+
if ("eslint" in manifest.features) {
|
|
268
|
+
features["eslint"] = await askYesNo(rl, "Include ESLint with the East lint rules?", features["eslint"]);
|
|
269
|
+
}
|
|
270
|
+
if ("editor-diagnostics" in manifest.features) {
|
|
271
|
+
features["editor-diagnostics"] = await askYesNo(rl, "Include East editor diagnostics (TypeScript language service plugin)?", features["editor-diagnostics"]);
|
|
272
|
+
}
|
|
273
|
+
} finally {
|
|
274
|
+
rl.close();
|
|
275
|
+
}
|
|
276
|
+
return features;
|
|
277
|
+
}
|
|
278
|
+
async function askYesNo(rl, question, def) {
|
|
279
|
+
const answer = (await rl.question(`${question} [${def ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
|
|
280
|
+
if (answer === "")
|
|
281
|
+
return def;
|
|
282
|
+
return answer.startsWith("y");
|
|
283
|
+
}
|
|
284
|
+
function printHelp(kind) {
|
|
285
|
+
console.log(`Usage: npm create @elaraai/${kind} <project-name> [-- <options>]`);
|
|
286
|
+
console.log(` create-${kind} <project-name|.> [options]`);
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log("Options:");
|
|
289
|
+
console.log(" --install | --no-install install dependencies after scaffolding (default: TTY)");
|
|
290
|
+
console.log(" --eslint | --no-eslint include ESLint with the East lint rules (default: yes)");
|
|
291
|
+
console.log(" --editor-diagnostics | --no-editor-diagnostics include the East tsserver plugin for editor squiggles (default: yes)");
|
|
292
|
+
if (kind === "e3") {
|
|
293
|
+
console.log(" --tests | --no-tests include test files (default: yes)");
|
|
294
|
+
console.log(" --ui | --no-ui include east-ui + e3-ui UI components (default: no)");
|
|
295
|
+
console.log(" --runners=east-node,east-c,east-py East runtimes to include (default: all)");
|
|
296
|
+
}
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log("Run interactively (a TTY with no feature flags) to be prompted for these.");
|
|
299
|
+
}
|
|
300
|
+
function printNextSteps(kind, projectName, inPlace, installed, features) {
|
|
166
301
|
console.log("");
|
|
167
302
|
console.log("Next steps:");
|
|
168
303
|
if (!inPlace)
|
|
169
304
|
console.log(` cd ${projectName}`);
|
|
170
|
-
if (!installed)
|
|
171
|
-
|
|
305
|
+
if (!installed) {
|
|
306
|
+
if (kind === "e3")
|
|
307
|
+
console.log(features["runner:east-py"] === false ? " npm install" : " npm run setup");
|
|
308
|
+
else
|
|
309
|
+
console.log(" npm install");
|
|
310
|
+
}
|
|
172
311
|
console.log(kind === "e3" ? " npm run start" : " npm run test");
|
|
173
312
|
}
|
|
174
313
|
|
|
175
314
|
// src/index.ts
|
|
176
|
-
runCreateCli("e3", import.meta.url);
|
|
315
|
+
await runCreateCli("e3", import.meta.url);
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elaraai/create-e3",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Scaffold a new e3 project (BSL-1.1, Node + Python, durable execution): npm create @elaraai/e3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-e3": "./
|
|
7
|
+
"create-e3": "./bin/create-e3.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
+
"bin",
|
|
10
11
|
"dist",
|
|
11
12
|
"templates"
|
|
12
13
|
],
|
package/templates/e3/README.md
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
# __DISPLAY_NAME__
|
|
2
2
|
|
|
3
|
-
e3 project (BSL-1.1) —
|
|
3
|
+
An e3 project (BSL-1.1) — typed East logic, durable dataflow execution.
|
|
4
|
+
|
|
5
|
+
East turns inputs and logic into **decisions**. This starter ships one: `reorder_qty`
|
|
6
|
+
in `src/index.ts` recommends how many units to reorder to bring stock (`on_hand`) up to
|
|
7
|
+
its target (`reorder_to`), never negative. Edit it, add inputs and tasks, and grow the
|
|
8
|
+
package from there.
|
|
4
9
|
|
|
5
10
|
## Setup
|
|
6
11
|
|
|
7
12
|
```bash
|
|
8
|
-
npm run setup # npm install
|
|
13
|
+
npm run setup # install dependencies (npm install, plus `uv sync` if you kept the Python runner)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Run
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run start # deploy from ./src/index.ts, then run the dataflow once
|
|
20
|
+
npm run watch # re-deploy and re-run on every save
|
|
21
|
+
npm run deploy # create the repo (if needed) and deploy without running
|
|
9
22
|
```
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
The package is the default export of `src/index.ts`; the e3 CLI deploys it straight from source.
|
|
25
|
+
|
|
26
|
+
## Test
|
|
12
27
|
|
|
13
28
|
```bash
|
|
14
29
|
npm run build # compile TypeScript
|
|
15
|
-
npm run test # build, export IR, run
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
npm run
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
npm run test # build, export the IR, and run the tests
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`npm run test` is present if you scaffolded with tests. With the Python (east-py) runner it also
|
|
34
|
+
runs the exported IR through the Python runtime; without it, tests run on Node only.
|
|
35
|
+
|
|
36
|
+
## Other
|
|
37
|
+
|
|
38
|
+
```bash
|
|
21
39
|
npm run lint # lint sources
|
|
22
|
-
npm run clean # remove build output,
|
|
40
|
+
npm run clean # remove build output, dependencies, and the local repo
|
|
23
41
|
```
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
If you scaffolded with UI, `src/surface.tsx` holds a decision surface — a `ui()` task an operator
|
|
44
|
+
uses to observe and act on the recommendation, registered in the package next to the decision.
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import tseslint from "@typescript-eslint/eslint-plugin";
|
|
2
2
|
import tsparser from "@typescript-eslint/parser";
|
|
3
|
+
import east from "@elaraai/eslint-plugin-east";
|
|
3
4
|
|
|
4
5
|
export default [
|
|
5
6
|
{ ignores: ["dist/", "node_modules/", ".venv/"] },
|
|
6
7
|
{
|
|
7
|
-
files: ["src/**/*.ts"],
|
|
8
|
+
files: ["src/**/*.ts", "src/**/*.tsx"],
|
|
8
9
|
languageOptions: {
|
|
9
10
|
parser: tsparser,
|
|
10
|
-
|
|
11
|
+
// Type-aware linting: the East rules read the TypeScript program, so the
|
|
12
|
+
// parser must build it (`projectService` discovers the nearest tsconfig).
|
|
13
|
+
parserOptions: {
|
|
14
|
+
projectService: true,
|
|
15
|
+
tsconfigRootDir: import.meta.dirname,
|
|
16
|
+
},
|
|
11
17
|
},
|
|
12
|
-
plugins: { "@typescript-eslint": tseslint },
|
|
13
|
-
|
|
18
|
+
plugins: { "@typescript-eslint": tseslint, east },
|
|
19
|
+
// One rule runs the whole East idiom diagnostic set (prefer some()/none, no
|
|
20
|
+
// hand-rolled variants, prefer the <Tag> over Tag.Root(...), etc.).
|
|
21
|
+
rules: { "east/east-rules": "warn" },
|
|
14
22
|
},
|
|
15
23
|
];
|
|
@@ -23,15 +23,21 @@
|
|
|
23
23
|
"@elaraai/east-node-io": "workspace:*",
|
|
24
24
|
"@elaraai/east-py-datascience": "workspace:*",
|
|
25
25
|
"@elaraai/e3": "workspace:*",
|
|
26
|
-
"@elaraai/e3-types": "workspace:*"
|
|
26
|
+
"@elaraai/e3-types": "workspace:*",
|
|
27
|
+
"@elaraai/east-ui": "workspace:*",
|
|
28
|
+
"@elaraai/e3-ui": "workspace:*"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@elaraai/e3-cli": "workspace:*",
|
|
32
|
+
"@elaraai/east-node-cli": "workspace:*",
|
|
33
|
+
"@elaraai/east-c-cli": "workspace:*",
|
|
30
34
|
"@types/node": "^22",
|
|
31
35
|
"typescript": "^5",
|
|
32
36
|
"eslint": "^9",
|
|
33
37
|
"@typescript-eslint/eslint-plugin": "^8",
|
|
34
38
|
"@typescript-eslint/parser": "^8",
|
|
39
|
+
"@elaraai/eslint-plugin-east": "workspace:*",
|
|
40
|
+
"@elaraai/tsserver-plugin-east": "workspace:*",
|
|
35
41
|
"cross-env": "^7",
|
|
36
42
|
"rimraf": "^6"
|
|
37
43
|
},
|
|
@@ -5,9 +5,8 @@ requires-python = ">=3.11"
|
|
|
5
5
|
version = "0.1.0"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"elaraai-east-py",
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# "elaraai-east-py-io",
|
|
8
|
+
"elaraai-east-py-std",
|
|
9
|
+
"elaraai-east-py-io",
|
|
11
10
|
"elaraai-east-py-datascience",
|
|
12
11
|
"elaraai-east-py-cli",
|
|
13
12
|
"pytest",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { East } from "@elaraai/east";
|
|
2
2
|
import { describeEast, Assert } from "@elaraai/east-node-std";
|
|
3
|
-
import {
|
|
3
|
+
import { reorderFn } from "./index.js";
|
|
4
4
|
|
|
5
5
|
describeEast("__DISPLAY_NAME__", (test) => {
|
|
6
|
-
test("
|
|
7
|
-
const
|
|
8
|
-
$(Assert.equal(
|
|
6
|
+
test("reorders up to the target when below it", ($) => {
|
|
7
|
+
const qty = $.let(reorderFn(12n, 50n));
|
|
8
|
+
$(Assert.equal(qty, East.value(38n)));
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
test("
|
|
12
|
-
const
|
|
13
|
-
$(Assert.equal(
|
|
11
|
+
test("never recommends a negative reorder", ($) => {
|
|
12
|
+
const qty = $.let(reorderFn(80n, 50n));
|
|
13
|
+
$(Assert.equal(qty, East.value(0n)));
|
|
14
14
|
});
|
|
15
15
|
}, { exportOnly: true });
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import e3 from "@elaraai/e3";
|
|
2
|
-
import { East,
|
|
2
|
+
import { East, IntegerType } from "@elaraai/east";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// In East a decision is a typed task over inputs. This one recommends how many
|
|
5
|
+
// units to reorder to bring stock up to its target level — never negative.
|
|
6
|
+
export const onHandInput = e3.input("on_hand", IntegerType, 12n);
|
|
7
|
+
export const targetInput = e3.input("reorder_to", IntegerType, 50n);
|
|
5
8
|
|
|
6
|
-
export const
|
|
7
|
-
[
|
|
8
|
-
|
|
9
|
-
($,
|
|
9
|
+
export const reorderFn = East.function(
|
|
10
|
+
[IntegerType, IntegerType],
|
|
11
|
+
IntegerType,
|
|
12
|
+
($, onHand, target) => {
|
|
13
|
+
const gap = $.let(target.subtract(onHand));
|
|
14
|
+
$.return(East.greater(gap, 0n).ifElse(() => gap, () => 0n));
|
|
15
|
+
},
|
|
10
16
|
);
|
|
11
17
|
|
|
12
|
-
export const
|
|
18
|
+
export const reorderQty = e3.task("reorder_qty", [onHandInput, targetInput], reorderFn);
|
|
13
19
|
|
|
14
|
-
export default e3.package("__PROJECT_NAME__", "1.0.0",
|
|
20
|
+
export default e3.package("__PROJECT_NAME__", "1.0.0", reorderQty);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import e3 from "@elaraai/e3";
|
|
2
|
+
import { East, IntegerType } from "@elaraai/east";
|
|
3
|
+
|
|
4
|
+
import { surface } from "./surface.js";
|
|
5
|
+
|
|
6
|
+
// In East a decision is a typed task over inputs. This one recommends how many
|
|
7
|
+
// units to reorder to bring stock up to its target level — never negative.
|
|
8
|
+
export const onHandInput = e3.input("on_hand", IntegerType, 12n);
|
|
9
|
+
export const targetInput = e3.input("reorder_to", IntegerType, 50n);
|
|
10
|
+
|
|
11
|
+
export const reorderFn = East.function(
|
|
12
|
+
[IntegerType, IntegerType],
|
|
13
|
+
IntegerType,
|
|
14
|
+
($, onHand, target) => {
|
|
15
|
+
const gap = $.let(target.subtract(onHand));
|
|
16
|
+
$.return(East.greater(gap, 0n).ifElse(() => gap, () => 0n));
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const reorderQty = e3.task("reorder_qty", [onHandInput, targetInput], reorderFn);
|
|
21
|
+
|
|
22
|
+
export default e3.package("__PROJECT_NAME__", "1.0.0", reorderQty, surface);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { East } from "@elaraai/east";
|
|
2
|
+
import { ui } from "@elaraai/e3-ui";
|
|
3
|
+
import { Box, Text, UIComponentType } from "@elaraai/east-ui";
|
|
4
|
+
|
|
5
|
+
// A decision surface for an operator to observe and act on the recommendation.
|
|
6
|
+
// east-ui / e3-ui JSX authoring is still being finalised — build this out: bind
|
|
7
|
+
// reorderQty and the inputs with Data.bind, then add the Observe / Decide controls.
|
|
8
|
+
export const surface = ui(
|
|
9
|
+
"surface",
|
|
10
|
+
[],
|
|
11
|
+
East.function([], UIComponentType, (_$) => (
|
|
12
|
+
<Box padding="6">
|
|
13
|
+
<Text textStyle="heading-md">__DISPLAY_NAME__</Text>
|
|
14
|
+
</Box>
|
|
15
|
+
)),
|
|
16
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"features": {
|
|
3
|
+
"tests": {
|
|
4
|
+
"default": true,
|
|
5
|
+
"files": ["src/index.spec.ts", "tests/test_unit.py"],
|
|
6
|
+
"scripts": ["test:ts", "test:export", "test:py"]
|
|
7
|
+
},
|
|
8
|
+
"ui": {
|
|
9
|
+
"default": false,
|
|
10
|
+
"files": ["src/surface.tsx", "src/index.ui.ts"],
|
|
11
|
+
"disable": ["src/index.ts"],
|
|
12
|
+
"rename": { "src/index.ui.ts": "src/index.ts" },
|
|
13
|
+
"dependencies": ["@elaraai/east-ui", "@elaraai/e3-ui"]
|
|
14
|
+
},
|
|
15
|
+
"runner:east-node": {
|
|
16
|
+
"default": true,
|
|
17
|
+
"devDependencies": ["@elaraai/east-node-cli"]
|
|
18
|
+
},
|
|
19
|
+
"runner:east-c": {
|
|
20
|
+
"default": true,
|
|
21
|
+
"devDependencies": ["@elaraai/east-c-cli"]
|
|
22
|
+
},
|
|
23
|
+
"runner:east-py": {
|
|
24
|
+
"default": true,
|
|
25
|
+
"files": ["pyproject.toml", ".python-version", "tests/test_unit.py"],
|
|
26
|
+
"scripts": ["test:py"],
|
|
27
|
+
"dependencies": ["@elaraai/east-py-datascience"]
|
|
28
|
+
},
|
|
29
|
+
"eslint": {
|
|
30
|
+
"default": true,
|
|
31
|
+
"files": ["eslint.config.js"],
|
|
32
|
+
"scripts": ["lint"],
|
|
33
|
+
"devDependencies": [
|
|
34
|
+
"eslint",
|
|
35
|
+
"@typescript-eslint/eslint-plugin",
|
|
36
|
+
"@typescript-eslint/parser",
|
|
37
|
+
"@elaraai/eslint-plugin-east"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"editor-diagnostics": {
|
|
41
|
+
"default": true,
|
|
42
|
+
"devDependencies": ["@elaraai/tsserver-plugin-east"]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scriptVariants": {
|
|
46
|
+
"test": [
|
|
47
|
+
{ "when": ["tests", "runner:east-py"], "value": "npm run build && npm run test:export && npm run test:py" },
|
|
48
|
+
{ "when": ["tests"], "value": "npm run build && npm run test:export" }
|
|
49
|
+
],
|
|
50
|
+
"setup": [
|
|
51
|
+
{ "when": ["runner:east-py"], "value": "npm install && uv sync" },
|
|
52
|
+
{ "when": [], "value": "npm install" }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"exclude": ["dist"],
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
5
6
|
"module": "nodenext",
|
|
6
7
|
"target": "esnext",
|
|
7
8
|
"lib": ["esnext", "es2024"],
|
|
@@ -13,12 +14,14 @@
|
|
|
13
14
|
"exactOptionalPropertyTypes": true,
|
|
14
15
|
"strict": true,
|
|
15
16
|
"jsx": "react-jsx",
|
|
17
|
+
"jsxImportSource": "@elaraai/east-ui",
|
|
16
18
|
"verbatimModuleSyntax": true,
|
|
17
19
|
"isolatedModules": true,
|
|
18
20
|
"noUncheckedSideEffectImports": true,
|
|
19
21
|
"moduleDetection": "force",
|
|
20
22
|
"skipLibCheck": true,
|
|
21
23
|
"noErrorTruncation": true,
|
|
22
|
-
"incremental": true
|
|
24
|
+
"incremental": true,
|
|
25
|
+
"plugins": [{ "name": "@elaraai/tsserver-plugin-east" }]
|
|
23
26
|
}
|
|
24
27
|
}
|