@eigenpal/cli 0.5.8 → 0.5.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @eigenpal/cli
2
2
 
3
- ## 1.0.0
3
+ ## 0.5.9
4
4
 
5
5
  ### Major Changes
6
6
 
@@ -333,6 +333,10 @@ actual) { ... }` declaration around your statements. Parameter order is
333
333
 
334
334
  Also scrubs internal references from CLI source ahead of open-sourcing — generic placeholder names in docstrings and test fixtures, internal-tracker IDs stripped from comments, and one user-facing error message updated to point at the public issue tracker.
335
335
 
336
+ - 75caaec: `eigenpal workflow push` and `eigenpal workflow validate` now surface non-fatal **schema-quality warnings** when an output schema is loose enough to hurt downstream typing: categorical fields like `category`/`status`/`kind` declared as plain strings (no `enum`), `transform.script` functions returning `any`/`unknown`, untyped objects (`type: object` with no `properties`), and untyped arrays (`type: array` with no `items`). Warnings print to stderr and never block the push.
337
+
338
+ Updated the bundled CLI skill (`workflow-yaml.md`) with a "Be specific with types" section teaching `enum` fields, per-value meanings inline in `description`, and how `transform.script` TS literal unions auto-produce JSON Schema enums.
339
+
336
340
  - be693a3: `EIGENPAL_API_KEY` is now a complete profile bypass — when set, the active profile's `baseUrl` is also ignored (previously the env key was paired with the profile's URL, which is always wrong because an API key is provisioned for one server).
337
341
 
338
342
  In CI:
package/dist/cli.js CHANGED
@@ -28107,7 +28107,8 @@ var init_parser = __esm(() => {
28107
28107
  maxConcurrency: exports_external.number().min(1).max(30).default(3),
28108
28108
  pagesPerBatch: exports_external.number().min(1).max(20).default(5),
28109
28109
  prompt: exports_external.string().optional().describe("Custom prompt for LLM-based parsing"),
28110
- languages: exports_external.array(exports_external.string()).optional().describe("OCR language hints")
28110
+ languages: exports_external.array(exports_external.string()).optional().describe("OCR language hints"),
28111
+ tenantId: exports_external.string().optional()
28111
28112
  });
28112
28113
  ParserInputSchema = exports_external.object({
28113
28114
  data: exports_external.instanceof(Buffer).or(exports_external.instanceof(Uint8Array)),
@@ -65788,8 +65789,16 @@ function convertTsTypeToJsonSchema(node, ctx) {
65788
65789
  case "TSVoidKeyword":
65789
65790
  return {};
65790
65791
  case "TSAnyKeyword":
65791
- case "TSUnknownKeyword":
65792
+ case "TSUnknownKeyword": {
65793
+ const keyword = node.type === "TSAnyKeyword" ? "any" : "unknown";
65794
+ ctx.warnings.push({
65795
+ code: "weak-return-type",
65796
+ message: `\`${keyword}\` produces an empty schema, so downstream steps can't autocomplete this field or its descendants. Describe the actual shape (\`{ name: string; total: number }\`) or a literal union (\`"low" | "medium" | "high"\`) for the strongest downstream typing.`,
65797
+ start: node.start ?? undefined,
65798
+ end: node.end ?? undefined
65799
+ });
65792
65800
  return {};
65801
+ }
65793
65802
  case "TSNeverKeyword":
65794
65803
  return { not: {} };
65795
65804
  case "TSArrayType":
@@ -66138,13 +66147,15 @@ function compileTypedScript(opts) {
66138
66147
  });
66139
66148
  }
66140
66149
  const convertIssues = [];
66150
+ const convertWarnings = [];
66141
66151
  const inScopeSchemas = new Map(Object.entries(opts.inScopeSchemas ?? {}));
66142
66152
  const localAliases = collectLocalAliases(aliasDecls);
66143
66153
  const returnSchema = opts.kind === "evaluator" ? { type: "number" } : convertTsTypeToJsonSchema(returnTsType, {
66144
66154
  localAliases,
66145
66155
  inScopeSchemas,
66146
66156
  resolving: new Set,
66147
- issues: convertIssues
66157
+ issues: convertIssues,
66158
+ warnings: convertWarnings
66148
66159
  });
66149
66160
  if (convertIssues.length > 0) {
66150
66161
  return {
@@ -66157,10 +66168,7 @@ function compileTypedScript(opts) {
66157
66168
  }
66158
66169
  let stripped;
66159
66170
  try {
66160
- const out = import_sucrase.transform(opts.source, {
66161
- transforms: ["typescript"]
66162
- });
66163
- stripped = out.code;
66171
+ stripped = import_sucrase.transform(opts.source, { transforms: ["typescript"] }).code;
66164
66172
  } catch (err) {
66165
66173
  const e2 = err;
66166
66174
  return failWith({
@@ -66174,7 +66182,8 @@ function compileTypedScript(opts) {
66174
66182
  js: stripped,
66175
66183
  returnSchema,
66176
66184
  paramNames: actualParamNames
66177
- }
66185
+ },
66186
+ warnings: convertWarnings
66178
66187
  };
66179
66188
  }
66180
66189
  function failWith(issue2) {
@@ -181601,7 +181610,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
181601
181610
  // package.json
181602
181611
  var package_default = {
181603
181612
  name: "@eigenpal/cli",
181604
- version: "0.5.8",
181613
+ version: "0.5.9",
181605
181614
  description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
181606
181615
  keywords: [
181607
181616
  "eigenpal",
@@ -181641,6 +181650,8 @@ var package_default = {
181641
181650
  scripts: {
181642
181651
  dev: "bun src/cli.ts",
181643
181652
  build: "bun run generate:skill && bun build src/cli.ts --outdir dist --target node --format esm && rm -rf dist/templates dist/skill && cp -R src/templates dist/templates && cp -R src/skill dist/skill",
181653
+ generate: "bun run generate:cli-docs && bun run generate:skill",
181654
+ "generate:check": "bun run generate:cli-docs:check && bun run generate:skill:check",
181644
181655
  "generate:skill": "bun scripts/generate-skill-reference.ts",
181645
181656
  "generate:skill:check": "bun scripts/generate-skill-reference.ts --check",
181646
181657
  "generate:cli-docs": "bun scripts/generate-cli-docs.ts",
@@ -188043,6 +188054,155 @@ function parseWorkflow(yaml) {
188043
188054
  }
188044
188055
  return result.data;
188045
188056
  }
188057
+ // ../workflow-yaml/src/validate-quality.ts
188058
+ init_src();
188059
+ var CATEGORICAL_NAMES = new Set([
188060
+ "category",
188061
+ "subcategory",
188062
+ "kind",
188063
+ "status",
188064
+ "type",
188065
+ "subtype",
188066
+ "level",
188067
+ "severity",
188068
+ "priority",
188069
+ "state",
188070
+ "stage",
188071
+ "tier",
188072
+ "classification",
188073
+ "monitoring",
188074
+ "frequency",
188075
+ "reference",
188076
+ "sectionname",
188077
+ "code",
188078
+ "flag",
188079
+ "signal",
188080
+ "mode",
188081
+ "phase"
188082
+ ]);
188083
+ var SEPARATOR_LIST_RE = /(?:^|[\s(])((?:[A-Za-z0-9_-]{1,20})(?:\s*[|,/]\s*[A-Za-z0-9_-]{1,20}){1,7})(?:$|[\s).])/;
188084
+ function validateSchemaQuality(workflow2) {
188085
+ const warnings = [];
188086
+ for (let i5 = 0;i5 < workflow2.steps.length; i5++) {
188087
+ const step = workflow2.steps[i5];
188088
+ if (!step)
188089
+ continue;
188090
+ const basePath = `steps.${i5}`;
188091
+ walkStep(step, basePath, warnings);
188092
+ }
188093
+ return warnings;
188094
+ }
188095
+ function walkStep(step, basePath, out) {
188096
+ if (step.type === "ai.extract") {
188097
+ const schema = step.with?.schema;
188098
+ if (schema && typeof schema === "object") {
188099
+ walkSchema(schema, `${basePath}.with.schema`, out);
188100
+ }
188101
+ return;
188102
+ }
188103
+ if (step.type === "transform.script") {
188104
+ const cfg = step.with;
188105
+ const source2 = typeof cfg?.function === "string" ? cfg.function : null;
188106
+ if (!source2)
188107
+ return;
188108
+ const paramNames = cfg?.inputs && typeof cfg.inputs === "object" && !Array.isArray(cfg.inputs) ? Object.keys(cfg.inputs) : [];
188109
+ const compiled = compileTypedScript({ kind: "transform", source: source2, paramNames });
188110
+ if (!compiled.ok)
188111
+ return;
188112
+ for (const w10 of compiled.warnings) {
188113
+ if (w10.code === "weak-return-type") {
188114
+ out.push({
188115
+ code: "weak-script-return",
188116
+ field: `${basePath}.with.function`,
188117
+ message: `transform.script return type is too loose: ${w10.message}`,
188118
+ hint: 'Replace `unknown`/`any` with a concrete shape, or use a TS literal union (e.g. `"low" | "medium" | "high"`) when the value belongs to a closed set.'
188119
+ });
188120
+ }
188121
+ }
188122
+ const schema = compiled.result.returnSchema;
188123
+ if (schema.type === "object" && (!schema.properties || Object.keys(schema.properties).length === 0) && schema.additionalProperties !== false) {
188124
+ out.push({
188125
+ code: "untyped-object",
188126
+ field: `${basePath}.with.function`,
188127
+ message: "transform.script returns an untyped object. Downstream steps cannot autocomplete its fields.",
188128
+ hint: "Describe the actual shape in the return annotation: `function script(...): { name: string; total: number }`."
188129
+ });
188130
+ }
188131
+ return;
188132
+ }
188133
+ const withAny = step.with;
188134
+ const nestedSteps = withAny?.steps;
188135
+ if (Array.isArray(nestedSteps)) {
188136
+ for (let i5 = 0;i5 < nestedSteps.length; i5++) {
188137
+ const s5 = nestedSteps[i5];
188138
+ if (s5)
188139
+ walkStep(s5, `${basePath}.with.steps.${i5}`, out);
188140
+ }
188141
+ }
188142
+ const nestedBranches = withAny?.branches;
188143
+ if (Array.isArray(nestedBranches)) {
188144
+ for (let b8 = 0;b8 < nestedBranches.length; b8++) {
188145
+ const branch = nestedBranches[b8];
188146
+ if (branch && Array.isArray(branch.steps)) {
188147
+ for (let i5 = 0;i5 < branch.steps.length; i5++) {
188148
+ const s5 = branch.steps[i5];
188149
+ if (s5)
188150
+ walkStep(s5, `${basePath}.with.branches.${b8}.steps.${i5}`, out);
188151
+ }
188152
+ }
188153
+ }
188154
+ }
188155
+ }
188156
+ function walkSchema(schema, basePath, out) {
188157
+ const type = schema.type;
188158
+ if (type === "object" && (!schema.properties || Object.keys(schema.properties).length === 0) && schema.additionalProperties !== false) {
188159
+ out.push({
188160
+ code: "untyped-object",
188161
+ field: basePath,
188162
+ message: "Object schema has no properties. The LLM has no guidance on what fields to emit and downstream autocomplete is empty.",
188163
+ hint: "Add concrete fields under `properties`, or set `additionalProperties: false` if the object is intentionally open."
188164
+ });
188165
+ }
188166
+ if (type === "array" && (schema.items === undefined || isEmptyObject(schema.items))) {
188167
+ out.push({
188168
+ code: "untyped-array-items",
188169
+ field: basePath,
188170
+ message: "Array items are untyped. The LLM does not know what shape each element should take.",
188171
+ hint: "Add an `items:` block describing the element type, e.g. `items: { type: object, properties: { ... } }`."
188172
+ });
188173
+ }
188174
+ if (schema.properties && typeof schema.properties === "object") {
188175
+ for (const [name, raw] of Object.entries(schema.properties)) {
188176
+ if (!raw || typeof raw !== "object")
188177
+ continue;
188178
+ const prop = raw;
188179
+ const propType = prop.type;
188180
+ const propEnum = Array.isArray(prop.enum) && prop.enum.length > 0;
188181
+ const lower = name.toLowerCase();
188182
+ const description = typeof prop.description === "string" ? prop.description : "";
188183
+ const looksCategoricalByName = CATEGORICAL_NAMES.has(lower);
188184
+ const looksCategoricalByDescription = description.length > 0 && SEPARATOR_LIST_RE.test(description);
188185
+ if (propType === "string" && !propEnum && (looksCategoricalByName || looksCategoricalByDescription) && !looksLikeFreeform(prop)) {
188186
+ out.push({
188187
+ code: "categorical-missing-enum",
188188
+ field: `${basePath}.properties.${name}`,
188189
+ message: `Field \`${name}\` looks categorical but has no \`enum\`. Coded values extract more reliably when the LLM is constrained to a closed set.`,
188190
+ hint: "In the workflow builder, switch the field type to `Enum` and list the allowed values. In raw YAML, add `enum: ['value1', 'value2', ...]` to the field, and write the per-value meanings into the field's `description`."
188191
+ });
188192
+ }
188193
+ walkSchema(prop, `${basePath}.properties.${name}`, out);
188194
+ }
188195
+ }
188196
+ if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
188197
+ walkSchema(schema.items, `${basePath}.items`, out);
188198
+ }
188199
+ }
188200
+ function isEmptyObject(v11) {
188201
+ return typeof v11 === "object" && v11 !== null && !Array.isArray(v11) && Object.keys(v11).length === 0;
188202
+ }
188203
+ function looksLikeFreeform(prop) {
188204
+ return prop.pattern !== undefined || prop.format !== undefined;
188205
+ }
188046
188206
  // src/commands/validate.ts
188047
188207
  var import_yaml6 = __toESM(require_dist(), 1);
188048
188208
  import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
@@ -188061,24 +188221,46 @@ function isDirSafe(path15, issues, field) {
188061
188221
  }
188062
188222
  function validateWorkflowYaml(path15) {
188063
188223
  if (!existsSync10(path15)) {
188064
- return [{ field: path15, message: "File not found." }];
188224
+ return { issues: [{ field: path15, message: "File not found." }], warnings: [] };
188065
188225
  }
188066
188226
  const text = readFileSync10(path15, "utf-8");
188067
188227
  try {
188068
- parseWorkflow(text);
188069
- return [];
188228
+ const definition = parseWorkflow(text);
188229
+ let warnings = [];
188230
+ try {
188231
+ warnings = validateSchemaQuality(definition);
188232
+ } catch {
188233
+ warnings = [];
188234
+ }
188235
+ return { issues: [], warnings };
188070
188236
  } catch (err2) {
188071
188237
  if (err2 instanceof YamlParseError) {
188072
188238
  const where = err2.line !== undefined ? ` (line ${err2.line})` : "";
188073
- return [{ field: "yaml", message: `${err2.message}${where}` }];
188239
+ return { issues: [{ field: "yaml", message: `${err2.message}${where}` }], warnings: [] };
188074
188240
  }
188075
188241
  if (err2 instanceof WorkflowValidationError) {
188076
- return err2.errors.map((e8) => ({
188077
- field: e8.path.length > 0 ? e8.path.map(String).join(".") : "(root)",
188078
- message: e8.message
188079
- }));
188242
+ return {
188243
+ issues: err2.errors.map((e8) => ({
188244
+ field: e8.path.length > 0 ? e8.path.map(String).join(".") : "(root)",
188245
+ message: e8.message
188246
+ })),
188247
+ warnings: []
188248
+ };
188080
188249
  }
188081
- return [{ field: "(root)", message: err2 instanceof Error ? err2.message : String(err2) }];
188250
+ return {
188251
+ issues: [{ field: "(root)", message: err2 instanceof Error ? err2.message : String(err2) }],
188252
+ warnings: []
188253
+ };
188254
+ }
188255
+ }
188256
+ function printSchemaQualityWarnings(path15, warnings) {
188257
+ if (warnings.length === 0)
188258
+ return;
188259
+ warn(`workflow.yaml ${ui.dim(`(${path15})`)}: ${warnings.length} schema-quality warning${warnings.length === 1 ? "" : "s"}`);
188260
+ for (const w10 of warnings) {
188261
+ warn(` ${ui.dim(w10.field)} ${w10.message}`);
188262
+ if (w10.hint)
188263
+ warn(` ${ui.dim("hint:")} ${w10.hint}`);
188082
188264
  }
188083
188265
  }
188084
188266
  function validateEvaluatorsYaml(path15) {
@@ -188261,9 +188443,23 @@ function rootDir(opts) {
188261
188443
  return resolve6(opts.dir ?? process.cwd());
188262
188444
  }
188263
188445
  function registerAllValidateCommand(parent) {
188264
- parent.command("validate").description("Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`.").option("--dir <path>", "Project root (defaults to cwd; resolves the three default paths from here)").action((opts) => {
188446
+ parent.command("validate [path]").description("Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`.").option("--dir <path>", "Project root (defaults to cwd; resolves the three default paths from here)").action((path15, opts) => {
188447
+ if (path15) {
188448
+ const target = resolve6(path15);
188449
+ const wfResult2 = validateWorkflowYaml(target);
188450
+ const wfOk2 = printIssues("workflow.yaml", target, wfResult2.issues);
188451
+ if (wfOk2)
188452
+ printSchemaQualityWarnings(target, wfResult2.warnings);
188453
+ if (!wfOk2)
188454
+ process.exit(1);
188455
+ return;
188456
+ }
188265
188457
  const root2 = rootDir(opts);
188266
- const wfOk = printIssues("workflow.yaml", relPath(root2, join12(root2, "workflow.yaml")), validateWorkflowYaml(join12(root2, "workflow.yaml")));
188458
+ const wfResult = validateWorkflowYaml(join12(root2, "workflow.yaml"));
188459
+ const wfPath = relPath(root2, join12(root2, "workflow.yaml"));
188460
+ const wfOk = printIssues("workflow.yaml", wfPath, wfResult.issues);
188461
+ if (wfOk)
188462
+ printSchemaQualityWarnings(wfPath, wfResult.warnings);
188267
188463
  const evOk = printIssues("evaluators.yaml", relPath(root2, join12(root2, "evaluators.yaml")), validateEvaluatorsYaml(join12(root2, "evaluators.yaml")));
188268
188464
  const dsOk = printIssues("dataset/", relPath(root2, join12(root2, "dataset")), validateDatasetFolder(join12(root2, "dataset")));
188269
188465
  if (!wfOk || !evOk || !dsOk)
@@ -188916,6 +189112,7 @@ flag would otherwise shadow it. Validation failures come back as
188916
189112
  return;
188917
189113
  }
188918
189114
  success2(`Pushed ${ui.bold(workflowId)} (v${result.version ?? nextVersion})`);
189115
+ printSchemaQualityWarnings2(result.warnings);
188919
189116
  } else {
188920
189117
  if (opts.bump) {
188921
189118
  throw new Error("--bump requires --workflow-id (need a current version to bump from). For a new workflow, use --set-version <X.Y.Z> or omit both.");
@@ -188933,6 +189130,7 @@ flag would otherwise shadow it. Validation failures come back as
188933
189130
  const id2 = result.workflow?.id ?? result.id ?? "?";
188934
189131
  const ver = result.version?.version ?? result.workflow?.currentVersion?.version ?? result.currentVersion?.version ?? "?";
188935
189132
  success2(`Created ${ui.bold(id2)} (v${ver})`);
189133
+ printSchemaQualityWarnings2(result.warnings);
188936
189134
  }
188937
189135
  }));
188938
189136
  const moveCmd = parent.command("move <workflow-id>").description("Move a workflow to a folder path, creating folders as needed").requiredOption("--folder <path>", "Target folder path (`/` for root)");
@@ -188950,6 +189148,16 @@ flag would otherwise shadow it. Validation failures come back as
188950
189148
  }));
188951
189149
  }
188952
189150
  var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
189151
+ function printSchemaQualityWarnings2(warnings) {
189152
+ if (!warnings || warnings.length === 0)
189153
+ return;
189154
+ warn(`${warnings.length} schema-quality warning${warnings.length === 1 ? "" : "s"} (push succeeded; sharper types make downstream steps easier to wire):`);
189155
+ for (const w10 of warnings) {
189156
+ warn(` ${ui.dim(w10.field)} ${w10.message}`);
189157
+ if (w10.hint)
189158
+ warn(` ${ui.dim("hint:")} ${w10.hint}`);
189159
+ }
189160
+ }
188953
189161
  function isBumpLevel(value) {
188954
189162
  return value === "patch" || value === "minor" || value === "major";
188955
189163
  }
@@ -20,7 +20,7 @@ Manage workflows: push, pull, run, evaluate.
20
20
  - [`eigenpal workflow pull [options] <workflow-id>`](#eigenpal-workflow-pull-options-workflow-id)
21
21
  - [`eigenpal workflow push [options]`](#eigenpal-workflow-push-options)
22
22
  - [`eigenpal workflow move [options] <workflow-id>`](#eigenpal-workflow-move-options-workflow-id)
23
- - [`eigenpal workflow validate [options]`](#eigenpal-workflow-validate-options)
23
+ - [`eigenpal workflow validate [options] [path]`](#eigenpal-workflow-validate-options-path)
24
24
  - [`eigenpal workflow clear-local [options] [examples...]`](#eigenpal-workflow-clear-local-options-examples)
25
25
  - [`eigenpal workflow evaluators pull [options] <workflow-id>`](#eigenpal-workflow-evaluators-pull-options-workflow-id)
26
26
  - [`eigenpal workflow evaluators push [options] <workflow-id>`](#eigenpal-workflow-evaluators-push-options-workflow-id)
@@ -100,7 +100,7 @@ workflow
100
100
  ├── evaluator-type
101
101
  │ ├── list|ls
102
102
  │ └── get <type>
103
- ├── validate
103
+ ├── validate [path]
104
104
  ├── clear-local [examples...]
105
105
  └── step
106
106
  └── exec <type>
@@ -110,14 +110,14 @@ workflow
110
110
 
111
111
  ### Core
112
112
 
113
- | Command | Description |
114
- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
115
- | `eigenpal workflow list | ls [options]` | List workflows the caller can read. |
116
- | `eigenpal workflow pull [options] <workflow-id>` | Download the YAML definition of the workflow at its current version. |
117
- | `eigenpal workflow push [options]` | Create or update a workflow from a YAML file. |
118
- | `eigenpal workflow move [options] <workflow-id>` | Move a workflow to a folder path, creating folders as needed |
119
- | `eigenpal workflow validate [options]` | Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`. |
120
- | `eigenpal workflow clear-local [options] [examples...]` | Delete local execution artifacts under ./dataset/examples/. Keeps the latest run per example by default. |
113
+ | Command | Description |
114
+ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
115
+ | `eigenpal workflow list | ls [options]` | List workflows the caller can read. |
116
+ | `eigenpal workflow pull [options] <workflow-id>` | Download the YAML definition of the workflow at its current version. |
117
+ | `eigenpal workflow push [options]` | Create or update a workflow from a YAML file. |
118
+ | `eigenpal workflow move [options] <workflow-id>` | Move a workflow to a folder path, creating folders as needed |
119
+ | `eigenpal workflow validate [options] [path]` | Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`. |
120
+ | `eigenpal workflow clear-local [options] [examples...]` | Delete local execution artifacts under ./dataset/examples/. Keeps the latest run per example by default. |
121
121
 
122
122
  ### Evaluators
123
123
 
@@ -256,9 +256,15 @@ Move a workflow to a folder path, creating folders as needed
256
256
  | `--base-url <url>` | no | | Server base URL |
257
257
  | `--json` | no | | Output the raw server response as JSON |
258
258
 
259
- ### `eigenpal workflow validate [options]`
259
+ ### `eigenpal workflow validate [options] [path]`
260
260
 
261
- Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`.
261
+ Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`.
262
+
263
+ ### Arguments
264
+
265
+ | Name | Required | Variadic | Description |
266
+ | ------ | -------- | -------- | ----------- |
267
+ | `path` | no | no | |
262
268
 
263
269
  ### Options
264
270
 
@@ -191,6 +191,76 @@ The script runs in a WASM sandbox: 5s wall clock, 10 MB heap, no network,
191
191
  no filesystem. Trips show up as `script_timeout` / `script_memory_limit`
192
192
  on the step execution.
193
193
 
194
+ ## Be specific with types
195
+
196
+ Every output schema you write is read three times: by the LLM (for
197
+ `ai.extract`), by downstream steps (autocomplete + template resolution),
198
+ and by the runtime validator. **The more specific the type, the better
199
+ the workflow runs.**
200
+
201
+ ### Use `enum` for coded values
202
+
203
+ Categorical fields like `category`, `status`, `kind`, `severity`,
204
+ `monitoring`, `frequency` should be enums whenever the value set is
205
+ closed. The LLM is constrained to emit only allowed values, and
206
+ downstream `transform.script` steps know exactly what they'll see.
207
+
208
+ ```yaml
209
+ - name: classify
210
+ type: ai.extract
211
+ with:
212
+ input: '{{ input.text }}'
213
+ schema:
214
+ type: object
215
+ properties:
216
+ category:
217
+ type: string
218
+ enum: ['901', '902']
219
+ description: |
220
+ Covenant category.
221
+ 901 = Odkladacie podmienky (conditions precedent).
222
+ 902 = Následné podmienky (subsequent / ongoing covenants).
223
+ required: [category]
224
+ ```
225
+
226
+ Per-value meanings go inline in the field's `description`. The LLM picks
227
+ them up reliably from there, and the scope browser renders the allowed
228
+ values when downstream steps reference the field.
229
+
230
+ ### `transform.script` — TS literal unions become JSON Schema enums
231
+
232
+ A TS literal-union return type compiles to a JSON-Schema enum
233
+ automatically. Use it when the function returns a value from a closed set:
234
+
235
+ ```yaml
236
+ - name: severity
237
+ type: transform.script
238
+ with:
239
+ inputs:
240
+ score: '{{ steps.score.output.value }}'
241
+ function: |
242
+ function script(score: number): { level: 'low' | 'medium' | 'high' } {
243
+ return { level: score > 0.8 ? 'high' : score > 0.4 ? 'medium' : 'low' };
244
+ }
245
+ ```
246
+
247
+ Downstream steps autocomplete `low | medium | high` for `level`.
248
+
249
+ ### `eigenpal workflow push` warns on weak types
250
+
251
+ Push surfaces non-fatal **schema-quality warnings** when it sees patterns
252
+ that hurt downstream typing. Common ones:
253
+
254
+ | Warning code | What it means |
255
+ | ---------------------------- | -------------------------------------------------------------------------------------------- |
256
+ | `categorical-missing-enum` | A field named like `category` / `status` / `kind` is a plain `string`. Consider declaring an `enum`. |
257
+ | `weak-script-return` | A `transform.script` returns `any` or `unknown`. Replace with a concrete shape or a literal union. |
258
+ | `untyped-object` | A `type: object` field has no `properties`. The LLM has no guidance on what to emit. |
259
+ | `untyped-array-items` | A `type: array` field has no `items:` declared. |
260
+
261
+ Warnings do not block the push. They print to stderr as `! …` lines. Run
262
+ `eigenpal workflow validate` locally to surface them before pushing.
263
+
194
264
  ## Common patterns
195
265
 
196
266
  ### Conditional branches
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
5
  "keywords": [
6
6
  "eigenpal",
@@ -40,6 +40,8 @@
40
40
  "scripts": {
41
41
  "dev": "bun src/cli.ts",
42
42
  "build": "bun run generate:skill && bun build src/cli.ts --outdir dist --target node --format esm && rm -rf dist/templates dist/skill && cp -R src/templates dist/templates && cp -R src/skill dist/skill",
43
+ "generate": "bun run generate:cli-docs && bun run generate:skill",
44
+ "generate:check": "bun run generate:cli-docs:check && bun run generate:skill:check",
43
45
  "generate:skill": "bun scripts/generate-skill-reference.ts",
44
46
  "generate:skill:check": "bun scripts/generate-skill-reference.ts --check",
45
47
  "generate:cli-docs": "bun scripts/generate-cli-docs.ts",