@vimhead.dev/norn-cli 0.1.0-tip.35240723931.1 → 0.1.0-tip.35343816255.1

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.
Files changed (88) hide show
  1. package/assets/README.md +2 -24
  2. package/assets/docs/README.md +3 -1
  3. package/assets/docs/agents.md +3 -3
  4. package/assets/docs/cli.md +7 -7
  5. package/assets/docs/composition.md +64 -34
  6. package/assets/docs/persistence.md +4 -2
  7. package/assets/docs/projects.md +3 -12
  8. package/assets/{setup → docs}/providers.md +4 -3
  9. package/assets/docs/resources.md +7 -7
  10. package/assets/docs/schemas.md +130 -0
  11. package/assets/docs/workflows.md +4 -3
  12. package/assets/examples/agent-then-analysis/README.md +1 -1
  13. package/assets/examples/agent-then-analysis/plugin.ts +16 -15
  14. package/assets/examples/caller-selected-continuation/README.md +75 -0
  15. package/assets/examples/caller-selected-continuation/caller.ts +47 -0
  16. package/assets/examples/caller-selected-continuation/input.json +9 -0
  17. package/assets/examples/caller-selected-continuation/norn.project.json +4 -0
  18. package/assets/examples/caller-selected-continuation/producer.ts +32 -0
  19. package/assets/examples/coordinating-multiple-agents/README.md +1 -1
  20. package/assets/examples/coordinating-multiple-agents/plugin.ts +10 -11
  21. package/assets/examples/coordinating-multiple-agents/queue-adapter.ts +3 -4
  22. package/assets/examples/coordinating-multiple-agents/work-queue.ts +18 -18
  23. package/assets/examples/minimal-workflow/plugin.ts +2 -2
  24. package/assets/examples/shared-state/README.md +1 -1
  25. package/assets/examples/shared-state/plugin.ts +6 -6
  26. package/assets/examples/worktree-development-loop/README.md +105 -44
  27. package/assets/examples/worktree-development-loop/norn.project.json +1 -1
  28. package/assets/examples/worktree-development-loop/state.ts +6 -6
  29. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +1 -1
  30. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +9 -9
  31. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +1 -1
  32. package/assets/examples/worktree-development-loop/workflows/implementation/schema.ts +7 -7
  33. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  34. package/assets/examples/worktree-development-loop/workflows/planning/schema.ts +7 -7
  35. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +1 -1
  36. package/assets/examples/worktree-development-loop/workflows/review/schema.ts +11 -11
  37. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +1 -1
  38. package/assets/examples/worktree-development-loop/workflows/review-router/schema.ts +5 -5
  39. package/assets/package.json +1 -1
  40. package/assets/packages/cli/src/cli.ts +15 -32
  41. package/assets/packages/cli/src/client.ts +0 -7
  42. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  43. package/assets/packages/cli/src/internal/agent-response-tool.ts +9 -10
  44. package/assets/packages/cli/src/internal/agents.ts +17 -16
  45. package/assets/packages/cli/src/internal/engine.ts +20 -18
  46. package/assets/packages/cli/src/internal/errors.ts +4 -10
  47. package/assets/packages/cli/src/internal/run-state.ts +4 -0
  48. package/assets/packages/cli/src/internal/run.ts +4 -7
  49. package/assets/packages/cli/src/internal/state-store.ts +22 -14
  50. package/assets/packages/cli/src/internal/workflow-registry.ts +27 -21
  51. package/assets/packages/cli/src/plugin-loader.ts +33 -45
  52. package/assets/packages/cli/src/resources.ts +14 -12
  53. package/assets/packages/core/src/workflow-transition.ts +4 -0
  54. package/assets/packages/sdk/src/api.ts +87 -131
  55. package/assets/packages/sdk/src/files.ts +11 -10
  56. package/assets/packages/sdk/src/index.ts +0 -1
  57. package/assets/packages/sdk/src/resources.ts +2 -2
  58. package/assets/packages/sdk/src/schema.ts +53 -29
  59. package/assets/packages/sdk/src/state-adapter.ts +4 -4
  60. package/assets/tests/workflow-ref.test.ts +121 -77
  61. package/dist/cli.js +20 -36
  62. package/dist/client.d.ts +0 -4
  63. package/dist/client.js +0 -3
  64. package/dist/generated-build-info.d.ts +2 -2
  65. package/dist/generated-build-info.js +2 -2
  66. package/dist/internal/agent-response-tool.d.ts +2 -2
  67. package/dist/internal/agent-response-tool.js +7 -7
  68. package/dist/internal/agents.d.ts +2 -2
  69. package/dist/internal/agents.js +10 -10
  70. package/dist/internal/engine.d.ts +1 -1
  71. package/dist/internal/engine.js +14 -12
  72. package/dist/internal/errors.d.ts +2 -1
  73. package/dist/internal/errors.js +5 -8
  74. package/dist/internal/run-state.js +4 -0
  75. package/dist/internal/run.d.ts +2 -2
  76. package/dist/internal/run.js +10 -6
  77. package/dist/internal/state-store.d.ts +8 -7
  78. package/dist/internal/state-store.js +15 -9
  79. package/dist/internal/workflow-registry.d.ts +14 -6
  80. package/dist/internal/workflow-registry.js +20 -15
  81. package/dist/plugin-loader.d.ts +15 -21
  82. package/dist/plugin-loader.js +35 -47
  83. package/dist/resources.d.ts +1 -1
  84. package/dist/resources.js +15 -13
  85. package/package.json +3 -4
  86. package/assets/packages/sdk/src/seer/config.ts +0 -62
  87. package/assets/packages/sdk/src/seer/index.ts +0 -7
  88. package/assets/setup/releases.md +0 -76
@@ -7,6 +7,8 @@ import { getRunLeaseHealth } from "./run-lease.ts";
7
7
  import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
8
8
  import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
9
9
  import { runCurrentRoot } from "./run-store.ts";
10
+ import { jsonValueSchema } from "@vimhead.dev/norn/schema";
11
+ import { Value } from "typebox/value";
10
12
 
11
13
  export const RUN_STATE_FILE_NAME = "run-state.json";
12
14
  const LEGACY_RUN_STATE_FILE_NAME = "runtime-state.json";
@@ -208,12 +210,14 @@ export class NornRunStateStore {
208
210
  await this.files.withExclusiveLock(this.path, async (path) => {
209
211
  const current = parseNornRunState(await readRunStateFile({ path, legacyPath: join(dirname(this.path), LEGACY_RUN_STATE_FILE_NAME) }));
210
212
  const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
213
+ if (next.current?.params !== undefined) Value.Assert(jsonValueSchema, next.current.params);
211
214
  await writeJsonAtomically(path, next);
212
215
  this.state = next;
213
216
  });
214
217
  }
215
218
 
216
219
  private async write(): Promise<void> {
220
+ if (this.state.current?.params !== undefined) Value.Assert(jsonValueSchema, this.state.current.params);
217
221
  await mkdir(dirname(this.path), { recursive: true });
218
222
  await this.files.withExclusiveLock(this.path, (path) => writeJsonAtomically(path, this.state));
219
223
  }
@@ -1,6 +1,7 @@
1
1
  import { isAbsolute, relative, resolve, sep } from "node:path";
2
2
  import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent";
3
- import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode, NornWorkflowTarget, NornWorkflowTargetParamsInput, NornRun } from "@vimhead.dev/norn";
3
+ import type { NornAnyWorkflowDeclaration, NornProjectRun, NornRunOutcomeMetadata, NornWorkflowIsolationMode, NornRun } from "@vimhead.dev/norn";
4
+ import { createWorkflowTransition } from "@vimhead.dev/norn-core/workflow-transition";
4
5
  import type { NornAgentResponseCollector } from "./agent-response-tool.ts";
5
6
  import type { NornArtifacts } from "./artifacts.ts";
6
7
  import { NornAgentRunner } from "./agents.ts";
@@ -106,12 +107,8 @@ export class NornRunContext implements NornProjectRun {
106
107
  return this.resolveFromRoot(this.projectRoot, this.projectRoot, relativePath);
107
108
  }
108
109
 
109
- next<TWorkflow extends NornWorkflowTarget<any>>(workflow: TWorkflow, params: NornWorkflowTargetParamsInput<TWorkflow>): ReturnType<NornRun["next"]> {
110
- return {
111
- type: "next",
112
- workflowId: typeof workflow === "string" ? workflow : workflow.id,
113
- params,
114
- };
110
+ next(workflowId: string, params: unknown): ReturnType<NornRun["next"]> {
111
+ return createWorkflowTransition({ workflowId, params });
115
112
  }
116
113
 
117
114
  complete(metadata?: NornRunOutcomeMetadata): ReturnType<NornRun["complete"]> {
@@ -1,25 +1,33 @@
1
- import { readFile } from "node:fs/promises";
2
- import type { NornWorkflowStateDefinition, NornWorkflowState } from "@vimhead.dev/norn";
3
- import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
4
- import { isNodeError } from "@vimhead.dev/norn-core/errors";
1
+ import type { NornWorkflowState, NornWorkflowStateDefinition } from "@vimhead.dev/norn";
5
2
  import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
3
+ import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
+ import type { NornFileCoordinator } from "@vimhead.dev/norn/files";
5
+ import { jsonValueSchema } from "@vimhead.dev/norn/schema";
6
+ import { readFile } from "node:fs/promises";
7
+ import type { Static, TSchema } from "typebox";
8
+ import { Value } from "typebox/value";
9
+
10
+ function copyStateValue<Schema extends TSchema>(schema: Schema, value: unknown): Static<Schema> {
11
+ Value.Assert(jsonValueSchema, value);
12
+ return structuredClone(Value.Parse(schema, value));
13
+ }
6
14
 
7
15
  export class NornMemoryWorkflowState implements NornWorkflowState {
8
16
  private readonly data = new Map<string, unknown>();
9
17
 
10
- async get<T>(state: NornWorkflowStateDefinition<T>): Promise<T> {
18
+ async get<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema>> {
11
19
  const value = await this.getOptional(state);
12
20
  if (value === undefined) throw new Error(`Missing workflow state: ${state.id}`);
13
21
  return value;
14
22
  }
15
23
 
16
- async getOptional<T>(state: NornWorkflowStateDefinition<T>): Promise<T | undefined> {
24
+ async getOptional<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema> | undefined> {
17
25
  if (!this.data.has(state.id)) return undefined;
18
- return state.schema.parse(this.data.get(state.id));
26
+ return copyStateValue(state.schema, this.data.get(state.id));
19
27
  }
20
28
 
21
- async set<T>(state: NornWorkflowStateDefinition<T>, value: T): Promise<void> {
22
- this.data.set(state.id, state.schema.parse(value));
29
+ async set<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>, value: NoInfer<Static<Schema>>): Promise<void> {
30
+ this.data.set(state.id, copyStateValue(state.schema, value));
23
31
  }
24
32
  }
25
33
 
@@ -45,20 +53,20 @@ export class NornJsonWorkflowState implements NornWorkflowState {
45
53
  });
46
54
  }
47
55
 
48
- async get<T>(state: NornWorkflowStateDefinition<T>): Promise<T> {
56
+ async get<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema>> {
49
57
  const value = await this.getOptional(state);
50
58
  if (value === undefined) throw new Error(`Missing workflow state: ${state.id}`);
51
59
  return value;
52
60
  }
53
61
 
54
- async getOptional<T>(state: NornWorkflowStateDefinition<T>): Promise<T | undefined> {
62
+ async getOptional<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>): Promise<Static<Schema> | undefined> {
55
63
  const data = await this.input.files.withExclusiveLock(this.stateFile, (path) => this.readStateFile(path));
56
64
  if (!Object.prototype.hasOwnProperty.call(data, state.id)) return undefined;
57
- return state.schema.parse(data[state.id]);
65
+ return copyStateValue(state.schema, data[state.id]);
58
66
  }
59
67
 
60
- async set<T>(state: NornWorkflowStateDefinition<T>, value: T): Promise<void> {
61
- const parsedValue = state.schema.parse(value);
68
+ async set<Schema extends TSchema>(state: NornWorkflowStateDefinition<Schema>, value: NoInfer<Static<Schema>>): Promise<void> {
69
+ const parsedValue = copyStateValue(state.schema, value);
62
70
  await this.input.coordinateMutation(this.stateFile, () => this.input.files.withExclusiveLock(this.stateFile, async (path) => {
63
71
  const data = await this.readStateFile(path);
64
72
  Object.defineProperty(data, state.id, { value: parsedValue, enumerable: true, configurable: true, writable: true });
@@ -1,15 +1,26 @@
1
- import { z } from "zod";
2
- import { isWorkflowComplete, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornInspectedWorkflowInfo, type NornJsonSchema, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornDispose, type NornRunFail, type NornWorkflowGateInfo, type NornWorkflowImplementation, type NornRunNext, type NornRunFor, type NornWorkflowParams, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
3
- import { assertWorkflowMetadata, isPlainObject, schemaShape, schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
1
+ import { isWorkflowComplete, isWorkflowFail, isWorkflowNext, type NornAnyWorkflowDeclaration, type NornDispose, type NornInspectedWorkflowInfo, type NornRegisteredWorkflowInfo, type NornRunComplete, type NornRunFail, type NornRunFor, type NornRunNext, type NornWorkflowGateInfo, type NornWorkflowImplementation, type NornWorkflowParams, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
2
+ import { assertWorkflowMetadata, inspectSchema, isPlainObject, schemaShape, schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
3
+ import { type TSchema } from "typebox";
4
+ import { Value } from "typebox/value";
4
5
 
5
6
  export type NornRegisteredWorkflow = {
6
7
  workflow: NornAnyWorkflowDeclaration;
7
8
  implementation: NornWorkflowImplementation<NornAnyWorkflowDeclaration, unknown>;
8
- configSchema?: z.ZodType;
9
- config: unknown;
9
+ configuration?: NornRegisteredConfiguration;
10
10
  plugin?: NornWorkflowPluginInfo;
11
11
  };
12
12
 
13
+ export type NornRegisteredConfiguration = { readonly schema: TSchema; readonly input: unknown; readonly value: unknown };
14
+
15
+ export function decodePluginConfiguration(input: { readonly pluginId: string; readonly schema: TSchema | undefined; readonly value: unknown }): NornRegisteredConfiguration | undefined {
16
+ if (!input.schema) {
17
+ if (input.value !== undefined) throw new Error(`Norn config provided for plugin without config schema: ${input.pluginId}`);
18
+ return undefined;
19
+ }
20
+ const value = defaultConfigInput(input.schema, input.value);
21
+ return { schema: input.schema, input: value, value: Value.Decode(input.schema, value) };
22
+ }
23
+
13
24
  export type NornWorkflowStepResult =
14
25
  | NornRunNext
15
26
  | { readonly type: "complete"; readonly workflow: NornAnyWorkflowDeclaration; readonly metadata?: NornRunComplete["metadata"] }
@@ -21,15 +32,14 @@ export class NornWorkflowRegistry {
21
32
  register<TWorkflow extends NornAnyWorkflowDeclaration>(
22
33
  workflow: TWorkflow,
23
34
  implementation: NornWorkflowImplementation<TWorkflow, unknown>,
24
- metadata: { readonly plugin?: NornWorkflowPluginInfo; readonly configSchema?: z.ZodType; readonly config?: unknown } = {},
35
+ metadata: { readonly plugin?: NornWorkflowPluginInfo; readonly configuration?: NornRegisteredConfiguration } = {},
25
36
  ): NornDispose {
26
37
  if (this.entries.has(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
27
38
 
28
39
  const entry: NornRegisteredWorkflow = {
29
40
  workflow,
30
41
  implementation: implementation as NornWorkflowImplementation<NornAnyWorkflowDeclaration, unknown>,
31
- configSchema: metadata.configSchema,
32
- config: metadata.configSchema ? metadata.configSchema.parse(defaultConfigInput(metadata.configSchema, metadata.config)) : undefined,
42
+ configuration: metadata.configuration,
33
43
  plugin: metadata.plugin,
34
44
  };
35
45
  assertWorkflowMetadata(workflow);
@@ -68,7 +78,7 @@ export class NornWorkflowRegistry {
68
78
  const entry = this.entries.get(workflow.id);
69
79
  if (!entry) throw new Error(`Unknown workflow: ${workflow.id}`);
70
80
  if (!workflow.gate) throw new Error(`Workflow is not gated: ${workflow.id}`);
71
- const parsedParams = workflow.params.parse(params) as NornWorkflowParams<TWorkflow>;
81
+ const parsedParams = Value.Decode(workflow.params, params) as NornWorkflowParams<TWorkflow>;
72
82
  const parsedConfig = parseExecutionConfig(entry, configOverride);
73
83
  const description = await (entry.implementation as NornWorkflowImplementation<TWorkflow, unknown>).gate?.describe(run, parsedParams, parsedConfig);
74
84
  return validateGateDescription(description ?? workflow.id, workflow.id);
@@ -83,7 +93,7 @@ export class NornWorkflowRegistry {
83
93
  const entry = this.entries.get(workflow.id);
84
94
  if (!entry) throw new Error(`Unknown workflow: ${workflow.id}`);
85
95
 
86
- const parsedParams = workflow.params.parse(params) as Parameters<NornWorkflowImplementation<TWorkflow, unknown>["execute"]>[1];
96
+ const parsedParams = Value.Decode(workflow.params, params) as NornWorkflowParams<TWorkflow>;
87
97
  const parsedConfig = parseExecutionConfig(entry, configOverride);
88
98
  const result = await (entry.implementation as NornWorkflowImplementation<TWorkflow, unknown>).execute(run, parsedParams, parsedConfig);
89
99
  if (isWorkflowNext(result)) return result;
@@ -97,22 +107,22 @@ export class NornWorkflowRegistry {
97
107
  }
98
108
  }
99
109
 
100
- function defaultConfigInput(configSchema: z.ZodType, config: unknown): unknown {
110
+ function defaultConfigInput(configSchema: TSchema, config: unknown): unknown {
101
111
  if (config !== undefined) return config;
102
112
  return schemaType(unwrapSchema(configSchema)) === "object" ? {} : undefined;
103
113
  }
104
114
 
105
115
  function parseExecutionConfig(entry: NornRegisteredWorkflow, configOverride: unknown): unknown {
106
116
  const pluginConfigOverride = pluginConfigOverrideInput(entry, configOverride);
107
- if (!entry.configSchema) {
117
+ if (!entry.configuration) {
108
118
  if (pluginConfigOverride !== undefined) throw new Error(`Run config override provided for plugin without config schema: ${entry.plugin?.id ?? entry.workflow.id}`);
109
119
  return undefined;
110
120
  }
111
- if (pluginConfigOverride === undefined) return entry.config;
112
- const rawConfig = isPlainObject(entry.config) && isPlainObject(pluginConfigOverride)
113
- ? deepMerge(entry.config, pluginConfigOverride)
121
+ if (pluginConfigOverride === undefined) return entry.configuration.value;
122
+ const rawConfig = isPlainObject(entry.configuration.input) && isPlainObject(pluginConfigOverride)
123
+ ? deepMerge(entry.configuration.input, pluginConfigOverride)
114
124
  : pluginConfigOverride;
115
- return entry.configSchema.parse(rawConfig);
125
+ return Value.Decode(entry.configuration.schema, rawConfig);
116
126
  }
117
127
 
118
128
  function pluginConfigOverrideInput(entry: NornRegisteredWorkflow, configOverride: unknown): unknown {
@@ -152,7 +162,7 @@ function validateGateDescription(description: string, workflowId: string): strin
152
162
  function inspectedWorkflowInfo(entry: NornRegisteredWorkflow): NornInspectedWorkflowInfo {
153
163
  return {
154
164
  ...workflowInfo(entry),
155
- paramsSchema: jsonSchema(entry.workflow.params),
165
+ paramsSchema: inspectSchema(entry.workflow.params),
156
166
  gate: gateInfo(entry.workflow),
157
167
  };
158
168
  }
@@ -170,7 +180,3 @@ function workflowInfo(entry: NornRegisteredWorkflow): NornRegisteredWorkflowInfo
170
180
  function gateInfo(workflow: NornAnyWorkflowDeclaration): NornWorkflowGateInfo | null {
171
181
  return workflow.gate ? { enabled: true, fields: workflow.gate.fields } : null;
172
182
  }
173
-
174
- function jsonSchema(schema: z.ZodType): NornJsonSchema {
175
- return z.toJSONSchema(schema, { io: "input" }) as NornJsonSchema;
176
- }
@@ -1,39 +1,37 @@
1
- import { access, readFile, readdir } from "node:fs/promises";
2
- import { dirname, isAbsolute, join, resolve, sep } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import { createJiti } from "jiti/static";
5
- import * as typeboxModule from "typebox";
6
- import { z } from "zod";
7
- import * as zodModule from "zod";
8
1
  import * as nornModule from "@vimhead.dev/norn";
2
+ import { isWorkflowPlugin, type NornDispose, type NornPluginDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectPluginInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowPlugin, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
3
+ import { isNodeError } from "@vimhead.dev/norn-core/errors";
9
4
  import * as nornFilesModule from "@vimhead.dev/norn/files";
10
5
  import * as nornSchemaModule from "@vimhead.dev/norn/schema";
11
- import * as nornSeerModule from "@vimhead.dev/norn/seer";
12
- import { isWorkflowPlugin, type NornDispose, type NornPluginDiagnostic, type NornProjectInfo, type NornProjectInspection, type NornProjectPluginInfo, type NornWorkflowCatalogInfo, type NornWorkflowInspection, type NornWorkflowPlugin, type NornWorkflowPluginInfo } from "@vimhead.dev/norn";
6
+ import { inspectSchema } from "@vimhead.dev/norn/schema";
7
+ import { createJiti } from "jiti/static";
8
+ import { access, readdir, readFile } from "node:fs/promises";
9
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
10
+ import { pathToFileURL } from "node:url";
11
+ import * as typeboxModule from "typebox";
12
+ import { Type, type StaticDecode } from "typebox";
13
+ import * as typeboxCompileModule from "typebox/compile";
14
+ import * as typeboxSchemaModule from "typebox/schema";
15
+ import * as typeboxValueModule from "typebox/value";
16
+ import { AssertError, Value } from "typebox/value";
13
17
  import { errorMessage, NornProjectLoadError } from "./internal/errors.ts";
14
- import { isNodeError } from "@vimhead.dev/norn-core/errors";
15
18
  import { NornMemoryWorkflowState } from "./internal/state-store.ts";
16
- import { NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
17
- import { schemaType, unwrapSchema } from "@vimhead.dev/norn/schema";
18
- import { resolveSeerModeConfig, type NornResolvedSeerModeConfig } from "@vimhead.dev/norn/seer";
19
+ import { decodePluginConfiguration, NornWorkflowRegistry, type NornRegisteredWorkflow } from "./internal/workflow-registry.ts";
19
20
 
20
21
  export const NORN_PROJECT_FILE_NAME = "norn.project.json";
21
22
 
22
- const seerModeConfigSchema = z.object({
23
- writableRoots: z.array(z.string().min(1)).min(1),
23
+ const nornConfigSchema = Type.Object({
24
+ plugins: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
25
+ includes: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
26
+ config: Type.Record(Type.String(), Type.Unknown(), { default: {} }),
24
27
  });
25
- const nornConfigSchema = z.object({
26
- plugins: z.array(z.string().min(1)).default([]),
27
- includes: z.array(z.string().min(1)).default([]),
28
- config: z.record(z.string(), z.unknown()).default({}),
29
- });
30
- const nornProjectConfigSchema = nornConfigSchema.extend({
31
- version: z.literal(1).default(1),
32
- seerMode: seerModeConfigSchema.optional(),
28
+ const nornProjectConfigSchema = Type.Object({
29
+ ...nornConfigSchema.properties,
30
+ version: Type.Literal(1, { default: 1 }),
33
31
  });
34
32
 
35
- type NornConfig = z.output<typeof nornConfigSchema> & { readonly seerMode?: never };
36
- type NornProjectConfig = z.output<typeof nornProjectConfigSchema>;
33
+ type NornConfig = StaticDecode<typeof nornConfigSchema>;
34
+ type NornProjectConfig = StaticDecode<typeof nornProjectConfigSchema>;
37
35
 
38
36
  type NornConfigFile = {
39
37
  readonly path: string;
@@ -50,7 +48,6 @@ export type NornProject = {
50
48
  readonly config: NornProjectConfig;
51
49
  readonly configFiles: readonly NornConfigFile[];
52
50
  readonly projectConfig: Record<string, unknown>;
53
- readonly seerMode?: NornResolvedSeerModeConfig;
54
51
  };
55
52
 
56
53
  export type NornLoadedProject = NornProject & {
@@ -101,7 +98,6 @@ function buildProjectInfo(project: NornLoadedProject): NornProjectInfo {
101
98
  cwd: project.cwd, projectPath: project.projectPath, projectRoot: project.projectRoot,
102
99
  configPath: project.configPath, configRoot: project.configRoot,
103
100
  configFiles: project.configFiles.map(file => file.path), plugins: project.pluginInfos,
104
- seerMode: project.seerMode ?? null,
105
101
  };
106
102
  }
107
103
 
@@ -118,7 +114,6 @@ export async function findNornProject(cwd: string): Promise<NornProject> {
118
114
  config: projectRootConfig.config,
119
115
  configFiles,
120
116
  projectConfig: mergeProjectConfig(configFiles),
121
- seerMode: resolveSeerModeConfig({ configPath: projectRootConfig.path, configRoot: projectRootConfig.root, seerMode: projectRootConfig.config.seerMode }),
122
117
  };
123
118
  }
124
119
 
@@ -165,12 +160,12 @@ async function loadNornConfigTree(configFile: NornConfigFile, visitedPaths: Set<
165
160
  }
166
161
 
167
162
  async function readNornProjectConfigFile(path: string): Promise<NornProjectConfigFile> {
168
- const config = nornProjectConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
163
+ const config = Value.Parse(nornProjectConfigSchema, Value.Default(nornProjectConfigSchema, JSON.parse(await readFile(path, "utf8"))));
169
164
  return { path, root: dirname(path), config };
170
165
  }
171
166
 
172
167
  async function readNornConfigFile(path: string): Promise<NornConfigFile> {
173
- const config = nornConfigSchema.parse(JSON.parse(await readFile(path, "utf8")));
168
+ const config = Value.Parse(nornConfigSchema, Value.Default(nornConfigSchema, JSON.parse(await readFile(path, "utf8"))));
174
169
  return { path, root: dirname(path), config };
175
170
  }
176
171
 
@@ -275,12 +270,11 @@ function registerProjectPlugin(input: {
275
270
  let stage: NornPluginDiagnostic["stage"] = "config";
276
271
  try {
277
272
  const configInput = input.project.projectConfig[plugin.manifest.id];
278
- if (!plugin.manifest.config && configInput !== undefined) throw new Error(`Norn config provided for plugin without config schema: ${plugin.manifest.id}`);
279
- const config = plugin.manifest.config ? plugin.manifest.config.parse(defaultConfigInput(plugin.manifest.config, configInput)) : undefined;
273
+ const configuration = decodePluginConfiguration({ pluginId: plugin.manifest.id, schema: plugin.manifest.config, value: configInput });
280
274
  stage = "schema";
281
275
  const info: NornProjectPluginInfo = {
282
276
  id: plugin.manifest.id, path: source.pluginPath, configPath: source.configPath,
283
- configSchema: plugin.manifest.config ? z.toJSONSchema(plugin.manifest.config, { io: "input" }) : null, config,
277
+ configSchema: plugin.manifest.config ? inspectSchema(plugin.manifest.config) : null, config: configuration?.value,
284
278
  };
285
279
  stage = "implementation";
286
280
  const implementation = typeof plugin.implementation === "function" ? plugin.implementation({ cwd: input.project.cwd, state: input.state }) : plugin.implementation;
@@ -292,9 +286,9 @@ function registerProjectPlugin(input: {
292
286
  stage = "duplicate";
293
287
  if (input.registry.workflowById(workflow.id)) throw new Error(`Workflow already registered: ${workflow.id}`);
294
288
  stage = "declaration";
295
- unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info), configSchema: plugin.manifest.config, config }));
289
+ unregister.push(input.registry.register(workflow, workflowImplementation, { plugin: workflowPluginInfo(info), configuration }));
296
290
  } catch (error) {
297
- diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage: error instanceof z.ZodError ? "config" : stage, error }));
291
+ diagnostics.push(createPluginDiagnostic({ source, workflowId: workflow.id, stage, error }));
298
292
  }
299
293
  }
300
294
  if (diagnostics.length === 0) return { loaded: { plugin, info }, diagnostics };
@@ -313,9 +307,7 @@ function createPluginDiagnostic(input: {
313
307
  }): NornPluginDiagnostic {
314
308
  return {
315
309
  ...input.source, workflowId: input.workflowId, stage: input.stage, message: errorMessage(input.error),
316
- issues: input.error instanceof z.ZodError ? input.error.issues.map(issue => ({
317
- path: issue.path.map(part => typeof part === "symbol" ? String(part) : part), code: issue.code, message: issue.message,
318
- })) : [],
310
+ issues: input.error instanceof AssertError ? input.error.cause.errors : [],
319
311
  };
320
312
  }
321
313
 
@@ -328,9 +320,10 @@ function nornWorkflowVirtualModules(): Record<string, unknown> {
328
320
  "@vimhead.dev/norn": nornModule,
329
321
  "@vimhead.dev/norn/files": nornFilesModule,
330
322
  "@vimhead.dev/norn/schema": nornSchemaModule,
331
- "@vimhead.dev/norn/seer": nornSeerModule,
332
323
  typebox: typeboxModule,
333
- zod: zodModule,
324
+ "typebox/value": typeboxValueModule,
325
+ "typebox/compile": typeboxCompileModule,
326
+ "typebox/schema": typeboxSchemaModule,
334
327
  };
335
328
  }
336
329
 
@@ -373,11 +366,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
373
366
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
374
367
  }
375
368
 
376
- function defaultConfigInput(configSchema: z.ZodType, config: unknown): unknown {
377
- if (config !== undefined) return config;
378
- return schemaType(unwrapSchema(configSchema)) === "object" ? {} : undefined;
379
- }
380
-
381
369
  async function expandIncludePath(configRoot: string, includePath: string): Promise<string[]> {
382
370
  const resolvedIncludePath = resolveConfigPath(configRoot, includePath);
383
371
  if (!hasGlobSegment(includePath)) return [resolvedIncludePath];
@@ -1,18 +1,20 @@
1
+ import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
2
+ import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
3
+ import { isNodeError } from "@vimhead.dev/norn-core/errors";
4
+ import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
5
+ import { jsonValueSchema } from "@vimhead.dev/norn/schema";
1
6
  import { mkdir, readFile } from "node:fs/promises";
2
7
  import { join, resolve } from "node:path";
3
8
  import { isDeepStrictEqual } from "node:util";
4
- import { z } from "zod";
5
- import { createRunFileCoordinator, type NornFileCoordinator } from "@vimhead.dev/norn/files";
6
- import type { NornResourceDefinition, NornResources } from "@vimhead.dev/norn";
7
- import { isNodeError } from "@vimhead.dev/norn-core/errors";
8
- import { writeJsonAtomically } from "@vimhead.dev/norn-core/atomic-files";
9
+ import { Type, type StaticDecode } from "typebox";
10
+ import { Value } from "typebox/value";
9
11
 
10
- const definitionSchema = z.strictObject({ name: z.string(), kind: z.string().min(1), configuration: z.json() });
11
- const recordSchema = definitionSchema.extend({ isInitialized: z.boolean() });
12
+ const definitionSchema = Type.Object({ name: Type.String(), kind: Type.String({ minLength: 1 }), configuration: jsonValueSchema }, { additionalProperties: false });
13
+ const recordSchema = Type.Object({ ...definitionSchema.properties, isInitialized: Type.Boolean() }, { additionalProperties: false });
12
14
 
13
15
  export class NornRunResources implements NornResources {
14
16
  readonly files: NornFileCoordinator;
15
- private readonly entries = new Map<string, { readonly definition: z.output<typeof definitionSchema>; readonly value: Promise<unknown> }>();
17
+ private readonly entries = new Map<string, { readonly definition: StaticDecode<typeof definitionSchema>; readonly value: Promise<unknown> }>();
16
18
 
17
19
  private constructor(private readonly runRoot: string) {
18
20
  this.files = createRunFileCoordinator(runRoot);
@@ -26,7 +28,7 @@ export class NornRunResources implements NornResources {
26
28
 
27
29
  async ensure<T>(definition: NornResourceDefinition<T>): Promise<T> {
28
30
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(definition.name)) throw new Error(`Invalid resource name: ${definition.name}`);
29
- const identity = definitionSchema.parse({ name: definition.name, kind: definition.kind, configuration: definition.configuration });
31
+ const identity = structuredClone(Value.Parse(definitionSchema, { name: definition.name, kind: definition.kind, configuration: definition.configuration }));
30
32
  const existing = this.entries.get(definition.name);
31
33
  if (existing) {
32
34
  if (!isDeepStrictEqual(existing.definition, identity)) throw new Error(`Incompatible resource definition: ${definition.name}`);
@@ -43,13 +45,13 @@ export class NornRunResources implements NornResources {
43
45
  }
44
46
  }
45
47
 
46
- private async initializeResource<T>(definition: NornResourceDefinition<T>, identity: z.output<typeof definitionSchema>): Promise<T> {
48
+ private async initializeResource<T>(definition: NornResourceDefinition<T>, identity: StaticDecode<typeof definitionSchema>): Promise<T> {
47
49
  const directory = join(this.runRoot, "current", "resources", definition.name);
48
50
  await mkdir(directory, { recursive: true });
49
51
  return this.files.withExclusiveLock(join(directory, "definition.json"), async (path) => {
50
- let stored: z.output<typeof recordSchema> | undefined;
52
+ let stored: StaticDecode<typeof recordSchema> | undefined;
51
53
  try {
52
- stored = recordSchema.parse(JSON.parse(await readFile(path, "utf8")));
54
+ stored = Value.Parse(recordSchema, JSON.parse(await readFile(path, "utf8")));
53
55
  } catch (error) {
54
56
  if (!isNodeError(error) || error.code !== "ENOENT") throw error;
55
57
  }
@@ -0,0 +1,4 @@
1
+ export function createWorkflowTransition(input: { readonly workflowId: string; readonly params: unknown }): { readonly type: "next"; readonly workflowId: string; readonly params: unknown } {
2
+ if (typeof input.workflowId !== "string" || input.workflowId.trim().length === 0) throw new Error("Workflow transition requires a nonempty workflow ID");
3
+ return { type: "next", workflowId: input.workflowId, params: input.params };
4
+ }