Idiomatic VibeLang Review

@smthrs/fs and @smthrs/errors rewritten from Effect TypeScript forms into VibeLang's own forms, after reading all 115 user-doc chapters.

Workspace idiom on nyc-01 (~/vibe/idiom/smithers), five commits on the T2 base, nothing pushed. FsError is now error FsError { … } and FlowInvoker is a service. SmithersError stays a class because error can't express it yet (gap 1).

197 / 197@smthrs/fs tests, 100% coverage
67 / 67@smthrs/errors tests, 100% coverage
646 pass@smthrs/integrations (GitHub live test skipped without a token)
All greencheck, checkTypes, lib, circular, test, fmt, lint, docs

Changed identities

Language gaps

Five need your call on syntax. Five are compiler or doc defects I'm fixing.

GapProposed
Your callerror can't express SmithersError: a custom constructor, a tag chosen at construction (generic Tag), hidden name/_tag/cause, and a toJSON override.error SmithersError<Tag extends string = "SmithersError"> { code: …; constructor(…) { super({…}, { tag: options.name ?? "SmithersError" }) } override toJSON() {…} }
Your callNo schema-backed error: error always lowers to Data.TaggedError.schema error FsError { … } → Schema.TaggedError
Your callschema declares only structs, so a literal union stays a TypeScript type.schema Code = "root_missing" | "read_failed" | …
Your callA service whose implementation is a plain value has no service form.service Prefix: string
Your callA hatch can't call a plain function from the same file (D229, EK4405), so try((project, schema) => project(schema), mapper, projectSchema, schema).try(projectSchema, mapper, schema)
FixingA throw expression in a plain-contract function becomes an Effect (EK2003), against D263, so a match arm in plain code can't throw.Compile it as a JavaScript throw, in do and match arms too.
FixingA catch expression doesn't pass its contextual type to its operands, so literal _tags widen.Type both sides from the contextual type.
FixingAn exhaustive match without default emits an unreachable branch, so 100% branch coverage is impossible.Lower the last arm of an exhaustive match unconditionally.
FixingEK2001 garbles the member name in its message when the member is typed by context.Print the member name.
FixingCause.findErrorOption can't be called as a method (its first parameter is input, not self); the docs show trusted() only for .ts.Add it to the data-methods table; document trusted() in .vibe.

Source and examples

apps/site/src/content/docs/docs/reference/api/fs.mdx+26 −26
@@ -300,26 +300,26 @@
| `flow` | `Flow.Any` | The loaded flow, from [@smthrs/core](/docs/reference/api/core/). |
| `input` | `unknown` | The decoded input; see `CommandSurface` for snapshot semantics. |
-### `Service`
-
-Executes a materialized flow.
-
-| Field | Type | Description |
+### `FlowInvoker`
+
+```ts
+service FlowInvoker {
+ invoke(invocation: Invocation): unknown!FsError
+}
+```
+
+Executes a materialized flow. The VibeLang `service` declaration is both the
+service key, `"@smthrs/fs/src/FlowInvoker#FlowInvoker"`, and, as a type, the
+implementation's shape.
+
+| Member | Type | Description |
| -------- | ------------------------------------------------------------- | --------------------------- |
| `invoke` | `(invocation: Invocation) => Effect.Effect<unknown, FsError>` | Runs one materialized flow. |
-### `FlowInvoker`
-
-```ts
-class FlowInvoker extends Context.Service<FlowInvoker, Service>()("/fs/FlowInvoker")
-```
-
-The flow invocation service tag, keyed `"/fs/FlowInvoker"`.
-
### `FlowInvoker.make`
```ts
-;((implementation: Service) => Service)
+;((implementation: FlowInvoker) => FlowInvoker)
```
Constructs a frozen flow invoker from an implementation. The implementation
@@ -330,7 +330,7 @@
### `FlowInvoker.makeNoop`
```ts
-;((overrides?: Partial<Service>) => Service)
+;((overrides?: Partial<FlowInvoker>) => FlowInvoker)
```
Constructs an invoker that fails every invocation with
@@ -341,7 +341,7 @@
### `FlowInvoker.layerNoop`
```ts
-;((overrides?: Partial<Service>) => Layer.Layer<FlowInvoker>)
+;((overrides?: Partial<FlowInvoker>) => Layer.Layer<FlowInvoker>)
```
Provides `makeNoop` as the `FlowInvoker` layer.
@@ -371,23 +371,23 @@
| "invocation_unavailable"
```
-Stable failure codes for routing, parsing, loading, and decoding, also
-exported as a `Schema.Literals` value for decoding. For the meaning and
-trigger of each code, see the
+Stable failure codes for routing, parsing, loading, and decoding. For the
+meaning and trigger of each code, see the
[error codes table](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/fs/docs/contract.md#error-codes).
### `FsError`
```ts
-class FsError extends Schema.TaggedError<FsError>()("flows/fs/FsError", {
- code: Code,
- method: Schema.String,
- description: Schema.String,
- path: Schema.optional(Schema.String)
-})
+error FsError {
+ code: Code
+ method: string
+ description: string
+ path?: string | undefined
+}
```
-A recoverable file-routing failure. `method` names the surface that failed so
+A VibeLang `error` declaration: a tagged error class whose `_tag` is
+`"FsError"`, with readonly fields. A recoverable file-routing failure. `method` names the surface that failed so
a CLI or an agent can report the origin without a stack trace. Raw argv,
input values, output values, schema issues, and implementation causes are
deliberately not retained at this boundary.
 
packages/errors/examples/handling.vibe+2 −2
@@ -34,11 +34,11 @@
}
/**
- * Recovers a failure by its tag and reads its machine-readable code.
+ * Recovers the failure and reads its machine-readable code.
*
* @returns The classified code; nothing escapes.
* @since 1.0.0
*/
export function recoverFailure(): string!! {
- return Effect.catchTag(raiseFailure(), "SmithersError", (failure) => `Recovered ${failure.code}`)
+ return raiseFailure() catch (failure) => `Recovered ${failure.code}`
}
 
packages/errors/src/SmithersError.vibe+7 −8
@@ -12,7 +12,6 @@
* @since 1.0.0
*/
-import * as Data from "effect/Data"
import { ERROR_REFERENCE_URL, isSmithersErrorCode, type SmithersErrorCode } from "./ErrorCode.ts"
/**
@@ -29,16 +28,16 @@
* always include the key. Redact provider text such as a bot token in a URL
* or an API key in a message before attaching it.
*/
- readonly cause?: unknown
+ cause?: unknown
/** Set `false` to leave the documentation URL out of the message. */
- readonly includeDocsUrl?: boolean
+ includeDocsUrl?: boolean
/**
* The `name` the error reports and its Effect `_tag`. Defaults to
* `"SmithersError"`. Both are installed as non-enumerable own properties,
* like `Error.prototype.name`, and `details` is an own property only when
* the caller supplies one.
*/
- readonly name?: Tag
+ name?: Tag
}
/**
@@ -47,11 +46,11 @@
*/
interface SmithersErrorFields {
/** The machine-readable classification. */
- readonly code: SmithersErrorCode
+ code: SmithersErrorCode
/** The message without the appended documentation URL. */
- readonly summary: string
+ summary: string
/** Where the code is documented. */
- readonly docsUrl: string
+ docsUrl: string
/**
* Caller-supplied context. The top-level record is copied and frozen at
* construction, so adding, removing, or replacing a top-level key on the
@@ -60,7 +59,7 @@
* nested record. Callers must redact credentials. Absent as an own property
* when the caller supplies none.
*/
- readonly details?: Readonly<Record<string, unknown>> | undefined
+ details?: Readonly<Record<string, unknown>> | undefined
}
const hidden = (target: object, key: PropertyKey, value: unknown): void => {
 
packages/smithers/agent/fs/src/Command.vibe+4 −8
@@ -80,7 +80,7 @@
function validateDecoded(schema: Schema.Top, value: unknown, field: "input" | "output"): unknown!FsError {
const decoded = SchemaBridge.recover(
- () => Schema.decodeUnknownEffect(Schema.toType(schema))(snapshotDecoded(value)),
+ Schema.decodeUnknownEffect(Schema.toType(schema))(snapshotDecoded(value)),
() =>
new FsError({
code: "decode_failed",
@@ -91,8 +91,6 @@
return snapshotDecoded(decoded)
}
-const descriptionOf = (route: Route.Route): string | undefined => Option.getOrUndefined(route.description)
-
const routeArgv = (argv: ReadonlyArray<string>): ReadonlyArray<string> => {
const first = argv[0]
if (first === undefined || !first.includes("/")) return argv
@@ -123,16 +121,14 @@
return Object.freeze({ route: resolved.route, flow, argv, input })
}
- const invoke = (route: Route.Route, flow: Loaded, input: unknown): unknown!FsError!FlowInvoker.FlowInvoker => {
- const invoker = FlowInvoker.FlowInvoker()
- return invoker.invoke(Object.freeze({ name: route.name, flow, input }))
- }
+ const invoke = (route: Route.Route, flow: Loaded, input: unknown) =>
+ FlowInvoker.FlowInvoker.invoke(Object.freeze({ name: route.name, flow, input }))
const listed = Object.freeze(
CommandTree.traverse(tree).map((route) =>
Object.freeze({
name: route.name,
- description: descriptionOf(route)
+ description: route.description.getOrUndefined()
})
)
)
 
packages/smithers/agent/fs/src/CommandTree.vibe+11 −11
@@ -5,6 +5,7 @@
*/
import * as ReadonlyMap from "@smthrs/canonical/ReadonlyMap"
+import { trusted } from "vibelang-runtime"
import { FsError } from "./FsError.vibe"
import * as Boundary from "./internal/Boundary.ts"
import * as CommandLine from "./internal/CommandLine.vibe"
@@ -69,7 +70,9 @@
const resourceLimit = (method: string, description: string): FsError =>
new FsError({ code: "resource_limit", method, description })
-const routeReferences = (input: ReadonlyArray<Route.Route>): ReadonlyArray<Route.Route> => {
+// Every reflective operation is inside the try, and every throw leaves it as an
+// FsError, so the trusted wrapper's only expected failure is FsError.
+const routeReferences = trusted((input: ReadonlyArray<Route.Route>): ReadonlyArray<Route.Route> => {
try {
if (!Array.isArray(input) || Object.getPrototypeOf(input) !== Array.prototype) {
throw new FsError({
@@ -103,14 +106,13 @@
}
return Object.freeze(output)
} catch (cause) {
- if (cause instanceof FsError) throw cause
- throw new FsError({
+ throw cause instanceof FsError ? cause : new FsError({
code: "invalid_route",
method: "CommandTree.make",
description: "Routes could not be inspected without executing user code"
})
}
-}
+}, [FsError])
const freezeTree = (root: MutableTree): CommandTree => {
const frozen = new Map<MutableTree, CommandTree>()
@@ -126,7 +128,7 @@
frozen.set(
frame.node,
Object.freeze({
- route: Option.isSome(frame.node.route) ? Object.freeze(Option.some(frame.node.route.value)) : Option.none(),
+ route: frame.node.route.isSome() ? Object.freeze(Option.some(frame.node.route.value)) : Option.none(),
children: ReadonlyMap.make(entries)
})
)
@@ -144,9 +146,7 @@
* @since 0.1.0
*/
export function make(input: ReadonlyArray<Route.Route>): CommandTree!FsError {
- // `routeReferences` catches every reflective operation and normalizes
- // every throw before it leaves the synchronous boundary.
- const references = try((references, input) => references(input), (cause) => cause as FsError, routeReferences, input)
+ const references = routeReferences(input)
const routes = forEach(references, Route.snapshot)
const totalSegments = routes.reduce((total, route) => total + route.segments.length, 0)
if (totalSegments > maximumTotalSegments) {
@@ -164,7 +164,7 @@
}
node = child
}
- if (Option.isSome(node.route)) {
+ if (node.route.isSome()) {
throw new FsError({
code: "duplicate_route",
method: "CommandTree.make",
@@ -212,7 +212,7 @@
const child = node.children.get(segment.normalize("NFC"))
if (child === undefined) break
node = child
- if (Option.isSome(child.route)) matched = { route: child.route.value, consumed: index + 1 }
+ if (child.route.isSome()) matched = { route: child.route.value, consumed: index + 1 }
}
if (matched === undefined) {
throw new FsError({
@@ -253,7 +253,7 @@
const stack: Array<CommandTree> = [tree]
while (stack.length > 0) {
const node = stack.pop()!
- if (Option.isSome(node.route)) routes.push(node.route.value)
+ if (node.route.isSome()) routes.push(node.route.value)
const children = Array.from(node.children.keys()).sort().reverse()
for (const segment of children) stack.push(node.children.get(segment)!)
}
 
packages/smithers/agent/fs/src/FileRouter.vibe+8 −16
@@ -54,21 +54,13 @@
const failure = (code: FsError["code"], description: string): FsError =>
new FsError({ code, method: "FileRouter.scan", description })
-const discoveryFailure = (cause: unknown): FsError => {
- if (cause instanceof DiscoveryError) {
- switch (cause.code) {
- case "root_missing":
- return failure("root_missing", "The flow root does not exist")
- case "invalid_root":
- return failure("invalid_root", "The flow root is not a readable directory")
- case "read_failed":
- return failure("read_failed", "The flow root could not be read")
- case "unknown":
- return failure("discovery_failed", "Flow discovery failed")
- }
+const discoveryFailure = (cause: unknown): FsError =>
+ match (cause) {
+ when DiscoveryError and { code: "root_missing" }: failure("root_missing", "The flow root does not exist");
+ when DiscoveryError and { code: "invalid_root" }: failure("invalid_root", "The flow root is not a readable directory");
+ when DiscoveryError and { code: "read_failed" }: failure("read_failed", "The flow root could not be read");
+ default: failure("discovery_failed", "Flow discovery failed");
}
- return failure("discovery_failed", "Flow discovery failed")
-}
const readFailure = (): FsError => failure("read_failed", "A route companion could not be inspected")
@@ -113,7 +105,7 @@
// Discovery confines entries, not companions: a UI module is recorded only
// when its real path stays inside the root's real path.
const withinRoot = (location: string): boolean!FsError => {
- const [realRoot, real] = all([fileSystem.realPath(root), fileSystem.realPath(location)]) catch () => throw readFailure()
+ const [realRoot, real] = all([fileSystem.realPath(root), fileSystem.realPath(location)]) catch throw readFailure()
return real.startsWith(path.join(realRoot, path.sep))
}
@@ -137,7 +129,7 @@
}
const directory = path.dirname(sourcePath)
const uiPath = path.join(directory, "ui.tsx")
- const hasUi = (fileSystem.exists(uiPath) catch () => throw readFailure()) && withinRoot(uiPath)
+ const hasUi = (fileSystem.exists(uiPath) catch throw readFailure()) && withinRoot(uiPath)
const route = Route.snapshot({
name,
segments,
 
packages/smithers/agent/fs/src/FlowInvoker.vibe+8 −16
@@ -25,22 +25,14 @@
* The projections in this package never execute a flow themselves: the harness
* owns the run loop, permissions, and durability, so it supplies this service.
*
- * @category models
- * @since 0.1.0
- */
-export interface Service {
- invoke: (invocation: Invocation) => unknown!FsError
-}
-
-/**
- * The flow invocation service.
- *
* @category services
* @since 0.1.0
*/
-export class FlowInvoker extends Context.Service<FlowInvoker, Service>()("/fs/FlowInvoker") {}
+export service FlowInvoker {
+ invoke(invocation: Invocation): unknown!FsError
+}
-const ownInvoke = (input: unknown, description: string): Service["invoke"] => {
+const ownInvoke = (input: unknown, description: string): FlowInvoker["invoke"] => {
if (typeof input !== "object" || input === null) throw new TypeError(description)
const descriptor = Object.getOwnPropertyDescriptor(input, "invoke")
if (descriptor === undefined || !("value" in descriptor) || typeof descriptor.value !== "function") {
@@ -55,9 +47,9 @@
* @category constructors
* @since 0.1.0
*/
-export const make = (implementation: Service): Service => {
+export const make = (implementation: FlowInvoker): FlowInvoker => {
const invoke = ownInvoke(implementation, "FlowInvoker implementations require an own invoke function")
- return Object.freeze(FlowInvoker.of({ invoke }))
+ return Object.freeze({ invoke })
}
/**
@@ -66,7 +58,7 @@
* @category constructors
* @since 0.1.0
*/
-export const makeNoop = (overrides: Partial<Service> = {}): Service => {
+export const makeNoop = (overrides: Partial<FlowInvoker> = {}): FlowInvoker => {
const descriptor = Object.getOwnPropertyDescriptor(overrides, "invoke")
if (descriptor !== undefined && (!("value" in descriptor) || typeof descriptor.value !== "function")) {
throw new TypeError("FlowInvoker overrides require an own invoke function")
@@ -89,5 +81,5 @@
* @category layers
* @since 0.1.0
*/
-export const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<FlowInvoker> =>
+export const layerNoop = (overrides: Partial<FlowInvoker> = {}): Layer.Layer<FlowInvoker> =>
Layer.succeed(FlowInvoker, makeNoop(overrides))
 
packages/smithers/agent/fs/src/FsError.vibe+22 −31
@@ -10,31 +10,22 @@
* @category models
* @since 0.1.0
*/
-export const Code = Schema.Literals([
- "root_missing",
- "read_failed",
- "invalid_root",
- "discovery_failed",
- "parse_failed",
- "unknown_command",
- "duplicate_route",
- "invalid_route",
- "resource_limit",
- "load_failed",
- "unsupported_body",
- "unsupported_schema",
- "decode_failed",
- "encode_failed",
- "invocation_unavailable"
-])
-
-/**
- * Stable failure codes for routing, parsing, loading, and decoding.
- *
- * @category models
- * @since 0.1.0
- */
-export type Code = typeof Code.Type
+export type Code =
+ | "root_missing"
+ | "read_failed"
+ | "invalid_root"
+ | "discovery_failed"
+ | "parse_failed"
+ | "unknown_command"
+ | "duplicate_route"
+ | "invalid_route"
+ | "resource_limit"
+ | "load_failed"
+ | "unsupported_body"
+ | "unsupported_schema"
+ | "decode_failed"
+ | "encode_failed"
+ | "invocation_unavailable"
/**
* A recoverable file-routing failure.
@@ -46,9 +37,9 @@
* @category errors
* @since 0.1.0
*/
-export class FsError extends Schema.TaggedError<FsError>()("flows/fs/FsError", {
- code: Code,
- method: Schema.String,
- description: Schema.String,
- path: Schema.optional(Schema.String)
-}) {}
+export error FsError {
+ code: Code
+ method: string
+ description: string
+ path?: string | undefined
+}
 
packages/smithers/agent/fs/src/Incur.vibe+21 −29
@@ -20,28 +20,24 @@
// Incur's ok and error callbacks throw transport control signals, so each
// settles the call as a defect, outside the program whose failures are
// sanitized.
-function execute(selected: IncurHost.SelectedRoute, context: IncurHost.Context): unknown!never!FlowInvoker.FlowInvoker {
- const outcome = exit(() => {
+function execute(selected: IncurHost.SelectedRoute, context: IncurHost.Context): unknown!!FlowInvoker.FlowInvoker {
+ const outcome = exit {
const input = selected.schema.decode(selected.schema.assemble(context.args, context.options))
- const invoker = FlowInvoker.FlowInvoker()
- const output = invoker.invoke(Object.freeze({
+ const output = FlowInvoker.FlowInvoker.invoke(Object.freeze({
name: selected.route.name,
flow: selected.flow,
input
}))
return SchemaBridge.encodeOutput(selected.flow.output, output)
- })
+ }
if (outcome is Exit.Success(let output)) return context.ok(output)
const cause = outcome.cause
- const reason = cause.reasons[0]!
- if (cause.reasons.length === 1 && reason._tag === "Fail" && reason.error instanceof FsError) {
- return mapError(context, reason.error)
- }
+ if (cause.reasons is [{ _tag: "Fail", error: FsError and let error }]) return mapError(context, error)
// Host diagnostics stay in Effect's logger, never in Incur's response.
provide Logger.LogToStderr = true {
logDebug("Incur invocation failed", cause)
}
- if (Cause.hasInterrupts(cause)) throw Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason))
+ if (cause.hasInterrupts()) throw Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason))
return mapError(
context,
new FsError({
@@ -58,24 +54,20 @@
function project(route: Route.Route): IncurHost.Projection!! {
try {
- const selected = Effect.timeoutOrElse(
- () => {
- const flow = Route.load(route)
- const schema = SchemaBridge.toCommandSchema(route.input, flow.input)
- return { route, flow, schema }
- },
- {
- duration: "5 seconds",
- orElse: () => {
- throw new FsError({
- code: "load_failed",
- method: "Incur.project",
- description: "The route metadata load timed out",
- path: route.name
- })
- }
- }
- )
+ const selected = Effect.timeoutOrElse({
+ duration: "5 seconds",
+ orElse: () =>
+ throw new FsError({
+ code: "load_failed",
+ method: "Incur.project",
+ description: "The route metadata load timed out",
+ path: route.name
+ })
+ }) {
+ const flow = Route.load(route)
+ const schema = SchemaBridge.toCommandSchema(route.input, flow.input)
+ return { route, flow, schema }
+ }
return { _tag: "Ready", selected }
} catch (error) {
return { _tag: "Failed", error }
@@ -93,7 +85,7 @@
const stack: Array<CommandTree.CommandTree> = [tree]
while (stack.length > 0) {
const node = stack.pop()!
- const route = Option.getOrUndefined(node.route)
+ const route = node.route.getOrUndefined()
if (route !== undefined && node.children.size > 0) {
if (node.children.has(IncurHost.selfSegment)) {
throw new FsError({
 
packages/smithers/agent/fs/src/Route.vibe+20 −29
@@ -8,6 +8,7 @@
import * as RuntimeFlow from "@smthrs/flow/Flow"
import * as Descriptor from "@smthrs/registry/Descriptor"
import { fileSpecifier } from "@smthrs/registry/Executable"
+import { trusted } from "vibelang-runtime"
import { FsError } from "./FsError.vibe"
import * as Boundary from "./internal/Boundary.ts"
@@ -118,7 +119,7 @@
* @since 0.1.0
*/
export type Input<N extends Name> = N extends keyof Manifest
- ? Manifest[N] extends { readonly input: infer I } ? I : unknown
+ ? Manifest[N] extends { input: infer I } ? I : unknown
: unknown
/**
@@ -128,7 +129,7 @@
* @since 0.1.0
*/
export type Output<N extends Name> = N extends keyof Manifest
- ? Manifest[N] extends { readonly output: infer O } ? O : unknown
+ ? Manifest[N] extends { output: infer O } ? O : unknown
: unknown
const invalidRoute = (path: string, description = "Route metadata violates the command contract"): FsError =>
@@ -136,7 +137,7 @@
const text = (
value: unknown,
- options: { readonly path: string; readonly maximum: number; readonly empty?: boolean | undefined }
+ options: { path: string; maximum: number; empty?: boolean | undefined }
): string => {
if (
typeof value !== "string" || (options.empty !== true && value.length === 0) ||
@@ -186,8 +187,7 @@
}
return Object.freeze(output)
} catch (cause) {
- if (cause instanceof FsError) throw cause
- throw invalidRoute("$")
+ throw cause instanceof FsError ? cause : invalidRoute("$")
}
}
@@ -198,31 +198,28 @@
): Option.Option<string> => {
try {
if (!Option.isOption(input)) throw invalidRoute(path)
- if (Option.isNone(input)) return Option.none()
- return Option.some(text(input.value, { path, maximum }))
+ return input.map((value) => text(value, { path, maximum }))
} catch (cause) {
- if (cause instanceof FsError) throw cause
- throw invalidRoute(path)
+ throw cause instanceof FsError ? cause : invalidRoute(path)
}
}
const optionPlacement = (input: unknown): Option.Option<Descriptor.Placement> => {
try {
if (!Option.isOption(input)) throw invalidRoute("$.placement")
- if (Option.isNone(input)) return Option.none()
+ if (input.isNone()) return Option.none()
const placement = input.value
- if (placement !== "client" && placement !== "local" && placement !== "sandbox" && placement !== "remote") {
- throw invalidRoute("$.placement")
- }
+ if (!(placement is "client" or "local" or "sandbox" or "remote")) throw invalidRoute("$.placement")
return Option.some(placement)
} catch (cause) {
- if (cause instanceof FsError) throw cause
- throw invalidRoute("$.placement")
+ throw cause instanceof FsError ? cause : invalidRoute("$.placement")
}
}
const schemaRef = (input: unknown, path: string): Descriptor.SchemaRef => {
const tagged = schemaFields(input, ["_tag"], ["path", "field", "document"])
+ // This stays a switch: a match arm can only throw with a throw expression,
+ // and VibeLang makes a function with one an Effect, not plain JavaScript.
switch (tagged._tag) {
case "MarkdownArgs":
return new Descriptor.SchemaRefMarkdownArgs({})
@@ -232,7 +229,7 @@
return new Descriptor.SchemaRefNone({})
case "Module": {
const source = text(tagged.path, { path: `${path}.path`, maximum: maximumPathLength })
- if (tagged.field !== "input" && tagged.field !== "output") throw invalidRoute(`${path}.field`)
+ if (!(tagged.field is "input" or "output")) throw invalidRoute(`${path}.field`)
return new Descriptor.SchemaRefModule({ path: source, field: tagged.field })
}
case "Inline": {
@@ -257,13 +254,9 @@
})
if (!reads.ok) throw invalidRoute(`$.effects.reads${reads.path.slice(1)}`)
if (!writes.ok) throw invalidRoute(`$.effects.writes${writes.path.slice(1)}`)
- if (data.mode !== "hermetic" && data.mode !== "expected") throw invalidRoute("$.effects.mode")
- if (data.onConflict !== "serialize" && data.onConflict !== "lane" && data.onConflict !== "fail") {
- throw invalidRoute("$.effects.onConflict")
- }
- if (data.tier !== "sealed" && data.tier !== "compensable" && data.tier !== "irreversible") {
- throw invalidRoute("$.effects.tier")
- }
+ if (!(data.mode is "hermetic" or "expected")) throw invalidRoute("$.effects.mode")
+ if (!(data.onConflict is "serialize" or "lane" or "fail")) throw invalidRoute("$.effects.onConflict")
+ if (!(data.tier is "sealed" or "compensable" or "irreversible")) throw invalidRoute("$.effects.tier")
return Object.freeze({
reads: reads.value,
writes: writes.value,
@@ -277,7 +270,7 @@
value.startsWith("file:///") || value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)
// Every reflective helper above catches foreign throws and normalizes them, so
-// this throws only FsError.
+// this throws only FsError; anything else would be a defect.
const copyRoute = (input: Route): Route => {
const data = fields(input, [
"name",
@@ -310,7 +303,7 @@
if (name !== normalized.join("/")) {
throw invalidRoute("$.name", "Route name must equal its slash-joined segments")
}
- if (data.kind !== "module" && data.kind !== "markdown" && data.kind !== "skill") throw invalidRoute("$.kind")
+ if (!(data.kind is "module" or "markdown" or "skill")) throw invalidRoute("$.kind")
const sourcePath = text(data.sourcePath, { path: "$.sourcePath", maximum: maximumPathLength })
if (!isAbsolutePath(sourcePath)) throw invalidRoute("$.sourcePath", "Route source paths must be absolute")
const capabilities = Boundary.stringArray(data.capabilities, {
@@ -342,9 +335,7 @@
* @category constructors
* @since 0.1.0
*/
-export function snapshot(input: Route): Route!FsError {
- return try((copy, input) => copy(input), (cause) => cause as FsError, copyRoute, input)
-}
+export const snapshot = trusted(copyRoute, [FsError])
/**
* True only for routes the agent and Incur command surfaces may execute.
@@ -390,7 +381,7 @@
})
}
const module = try(
- async (specifier) => import(/* @vite-ignore */ specifier) as Promise<{ readonly default?: unknown }>,
+ async (specifier) => import(/* @vite-ignore */ specifier) as Promise<{ default?: unknown }>,
importFailed,
fileSpecifier(route.sourcePath)
)
 
packages/smithers/agent/fs/src/internal/CommandLine.vibe+13 −21
@@ -77,14 +77,14 @@
let started = false
let escaping = false
- const push = (): FsError | undefined => {
+ const push = () => {
if (current.length > maximumTokenLength) {
- return resourceLimit(`A command token may contain at most ${maximumTokenLength} characters`)
+ throw resourceLimit(`A command token may contain at most ${maximumTokenLength} characters`)
}
argv.push(current)
- return argv.length > maximumCommandTokens
- ? resourceLimit(`A command may contain at most ${maximumCommandTokens} tokens`)
- : undefined
+ if (argv.length > maximumCommandTokens) {
+ throw resourceLimit(`A command may contain at most ${maximumCommandTokens} tokens`)
+ }
}
for (const character of commandString) {
@@ -119,8 +119,7 @@
started = true
} else if (/\s/.test(character)) {
if (started) {
- const failure = push()
- if (failure) throw failure
+ push()
current = ""
started = false
}
@@ -132,17 +131,14 @@
if (escaping) current += "\\"
if (quote !== undefined) throw parseError("The command contains an unterminated quote")
- if (started) {
- const failure = push()
- if (failure) throw failure
- }
+ if (started) push()
return Object.freeze(argv)
}
const validFlagName = (name: string): boolean => flagName.test(name) && !reservedNames.has(name)
-const append = (options: Record<string, unknown>, name: string, value: unknown): FsError | undefined => {
- if (!validFlagName(name)) return parseError("The command contains an invalid option name", `$.options.${name}`)
+const append = (options: Record<string, unknown>, name: string, value: unknown): void!FsError => {
+ if (!validFlagName(name)) throw parseError("The command contains an invalid option name", `$.options.${name}`)
const current = Object.hasOwn(options, name) ? options[name] : undefined
options[name] = current === undefined ? value : Array.isArray(current) ? [...current, value] : [current, value]
}
@@ -181,24 +177,20 @@
const flag = value.slice(2)
if (flag.startsWith("no-") && !flag.includes("=")) {
- const failure = append(options, flag.slice(3), false)
- if (failure) throw failure
+ append(options, flag.slice(3), false)
continue
}
const equals = flag.indexOf("=")
if (equals !== -1) {
- const failure = append(options, flag.slice(0, equals), flag.slice(equals + 1))
- if (failure) throw failure
+ append(options, flag.slice(0, equals), flag.slice(equals + 1))
continue
}
const next = captured.value[index + 1]
if (next !== undefined && !next.startsWith("--")) {
- const failure = append(options, flag, next)
- if (failure) throw failure
+ append(options, flag, next)
index += 1
} else {
- const failure = append(options, flag, true)
- if (failure) throw failure
+ append(options, flag, true)
}
}
 
packages/smithers/agent/fs/src/internal/SchemaBridge.vibe+5 −4
@@ -61,11 +61,12 @@
* @private
* @since 0.1.0
*/
-export function recover<A>(program: Effect.Effect<A, unknown, unknown>, failure: () => FsError!!): A!FsError {
+export function recover<A>(program: A!unknown!unknown, failure: () => FsError!!): A!FsError {
// @vibelang-diagnostics-next-line EK4810:off
- const outcome = exit(program as Effect.Effect<A, unknown>)
- if (outcome is Exit.Failure) throw failure()
- return outcome.value
+ return match (exit(program as A!unknown)) {
+ when Exit.Success(let value): value;
+ default: throw failure();
+ }
}
const jsonValue = (value: unknown): unknown => {
 

Tests

packages/errors/test/TaggedFailures.test.vibe+14 −8
@@ -8,16 +8,16 @@
}
}
-function raise(error: SmithersError<"SmithersError"> | AdapterError): never!(SmithersError<"SmithersError"> | AdapterError) {
+function raise<E>(error: E): never!E {
throw error
}
+// Routing by tag is the behavior under test, so these use Effect.catchTag
+// rather than a catch that matches by class.
function recover(error: SmithersError<"SmithersError"> | AdapterError): string!! {
- return Effect.catchTag(
- Effect.catchTag(raise(error), "AdapterError", (caught) => `adapter:${caught.summary}`),
- "SmithersError",
- (caught) => `base:${caught.code}`
- )
+ return raise(error)
+ |> Effect.catchTag(%, "AdapterError", (caught) => `adapter:${caught.summary}`)
+ |> Effect.catchTag(%, "SmithersError", (caught) => `base:${caught.code}`)
}
describe("tagged failures", () => {
@@ -28,8 +28,14 @@
it.effect("tags with the reported name so a renamed instance routes by that name") {
const renamed = new SmithersError("INTEGRATION_ERROR", "x", undefined, { name: "AdapterError" })
- const routed = Effect.catchTag(Effect.fail(renamed), "AdapterError", () => "adapter")
- expect(routed).toBe("adapter")
+ expect(raise(renamed) |> Effect.catchTag(%, "AdapterError", () => "adapter")).toBe("adapter")
+ }
+
+ it.effect("matches a subclass by class in a catch") {
+ const caught = raise(new AdapterError()) catch (error) => match (error) {
+ when AdapterError: error.summary;
+ }
+ expect(caught).toBe("adapter failed")
}
it.effect("fails the surrounding effect with the instance when thrown") {
 
packages/errors/test/helpers.vibe+5 −4
@@ -1,8 +1,9 @@
import { assert } from "@effect/vitest"
/** The expected failure a program fails with; a success or a defect fails the test. */
-export function failureOf<E>(program: Effect.Effect<unknown, E>): E!! {
- const outcome = exit(program)
- if (outcome is Exit.Failure(let cause)) return Option.getOrThrow(Cause.findErrorOption(cause))
- return assert.fail("expected the program to fail")
+export function failureOf<E>(program: unknown!E): E!! {
+ return match (exit(program)) {
+ when Exit.Failure(let cause): Cause.findErrorOption(cause).getOrThrow();
+ default: assert.fail("expected the program to fail");
+ }
}
 
packages/smithers/agent/fs/test/Command.test.vibe+13 −18
@@ -152,20 +152,20 @@
"\ud800"
]
) {
- expect(exit(CommandLine.lex(command))._tag).toBe("Failure")
+ expect(exit(CommandLine.lex(command)) is Exit.Failure).toBe(true)
}
const exact = Array.from({ length: CommandLine.maximumCommandTokens }, () => "x").join(" ")
expect(CommandLine.lex(exact)).toHaveLength(CommandLine.maximumCommandTokens)
const tooMany = `${exact} x`
- expect(exit(CommandLine.lex(tooMany))._tag).toBe("Failure")
+ expect(exit(CommandLine.lex(tooMany)) is Exit.Failure).toBe(true)
const hostile = new Proxy(["--safe=x"], {
ownKeys: () => {
throw new Error("trap")
}
})
- expect(exit(CommandLine.parseFlags(hostile))._tag).toBe("Failure")
+ expect(exit(CommandLine.parseFlags(hostile)) is Exit.Failure).toBe(true)
}
it.effect("bounds command bytes independently of the token bounds") {
@@ -205,7 +205,7 @@
function surfaceFor(input: Schema.Top, output: Schema.Top) {
const flow = { capabilities: [], effects: undefined, name: "scalar", input, output } as const
- vi.spyOn(Route, "load").mockReturnValue(Effect.succeed(flow))
+ vi.spyOn(Route, "load").mockImplementation(() => flow)
return Command.make([makeRoute("scalar")])
}
@@ -219,27 +219,22 @@
it.effect("accepts decoded transforming input in call and encoded input in execute") {
const surface = surfaceFor(Schema.NumberFromString, Schema.Number)
- const invoke = vi.fn(({ input }: FlowInvoker.Invocation) => Exit.succeed(input))
+ const invoke = vi.fn<FlowInvoker.FlowInvoker["invoke"]>(({ input }) => input)
provide FlowInvoker.layerNoop({ invoke }) {
expect(surface.call("scalar", 42)).toBe(42)
expect(surface.execute("scalar 42")).toBe(42)
expect(invoke.mock.calls.map(([invocation]) => invocation.input)).toEqual([42, 42])
- expect(exit(surface.call("scalar", "42"))._tag).toBe("Failure")
+ expect(exit(surface.call("scalar", "42")) is Exit.Failure).toBe(true)
}
expect(invoke).toHaveBeenCalledTimes(2)
}
it.effect("refuses scalar tokens that execute would otherwise drop") {
const surface = surfaceFor(Schema.Number, Schema.NumberFromString)
- const invoke = vi.fn(() => Exit.succeed(42))
+ const invoke = vi.fn<FlowInvoker.FlowInvoker["invoke"]>(() => 42)
provide FlowInvoker.layerNoop({ invoke }) {
for (const command of ["scalar 1 --typo x", "scalar 1 2", "scalar 1 --input 2", "scalar --input 1 --typo x"]) {
- const outcome = exit(surface.execute(command))
- expect(outcome._tag, command).toBe("Failure")
- if (outcome is Exit.Failure(let cause)) {
- const error = Option.getOrThrow(Cause.findErrorOption(cause))
- expect(error, command).toMatchObject({ code: "decode_failed" })
- }
+ expect(failureOf(surface.execute(command)), command).toMatchObject({ code: "decode_failed" })
}
expect(invoke).not.toHaveBeenCalled()
expect(surface.execute("scalar --input 1")).toBe("42")
@@ -248,7 +243,7 @@
it.effect("refuses unsafe decoded input before loading or invoking") {
const surface = Command.make([makeRoute("review")])
- const invoke = vi.fn(({ input }: FlowInvoker.Invocation) => Exit.succeed(input))
+ const invoke = vi.fn<FlowInvoker.FlowInvoker["invoke"]>(({ input }) => input)
let getterCalls = 0
const accessor = Object.defineProperty({}, "number", {
enumerable: true,
@@ -290,7 +285,7 @@
})
for (const output of [accessor, { value: "x".repeat(65_537) }]) {
provide FlowInvoker.layerNoop({ invoke: () => output }) {
- expect(exit(surface.call("scalar", { value: "ok" }))._tag).toBe("Failure")
+ expect(exit(surface.call("scalar", { value: "ok" })) is Exit.Failure).toBe(true)
}
}
expect(getterCalls).toBe(0)
@@ -307,17 +302,17 @@
const loading = Deferred.make<void>()
const release = Deferred.make<typeof flow>()
vi.spyOn(Route, "load").mockImplementation(() => {
- Deferred.succeed(loading, undefined)
+ loading.succeed(undefined)
return release.await()
})
const surface = Command.make([makeRoute("scalar")])
- const invoke = vi.fn(({ input }: FlowInvoker.Invocation) => Exit.succeed((input as { number: number }).number))
+ const invoke = vi.fn<FlowInvoker.FlowInvoker["invoke"]>(({ input }) => (input as { number: number }).number)
const input: { mutable number: number } = { number: 1 }
provide FlowInvoker.layerNoop({ invoke }) {
const pending = fork surface.call("scalar", input)
loading.await()
input.number = 2
- Deferred.succeed(release, flow)
+ release.succeed(flow)
expect(join pending).toBe(1)
}
expect(invoke).toHaveBeenCalledWith(expect.objectContaining({ input: { number: 1 } }))
 
packages/smithers/agent/fs/test/FileRouter.test.vibe+6 −11
@@ -3,6 +3,7 @@
import { expect, layer } from "@effect/vitest"
import * as FileRouter from "../src/FileRouter.vibe"
import { routerRoot as root } from "./fixtureRoots.ts"
+import { failureOf } from "./helpers.vibe"
const platformLayer = Layer.merge(NodeFileSystem.layer, NodePath.layer)
@@ -52,7 +53,7 @@
const result = scan()
const review = result.routes.find((route) => route.name === "review")
- expect(Option.getOrUndefined(review?.ui ?? Option.none())).toBe(path.join(root, "review", "ui.tsx"))
+ expect(review?.ui).toEqual(Option.some(path.join(root, "review", "ui.tsx")))
expect(result.routes.some((route) => path.basename(route.sourcePath) === "ui.tsx")).toBe(false)
expect(result.routes.some((route) => path.basename(route.sourcePath) === "flow.test.ts")).toBe(false)
}
@@ -76,7 +77,7 @@
const config: { mutable root: string } = { root: path.relative(path.resolve(), root) }
// The scan starts at once, as runPromise would start it, so the root is
// read before the caller rewrites its config.
- const pending = Effect.forkChild(FileRouter.scan(config), { startImmediately: true })
+ const pending = fork(FileRouter.scan(config), { startImmediately: true })
config.root = "/"
const result = join pending
@@ -121,12 +122,7 @@
}
it.effect("preserves discovery error codes and refuses hostile config") {
- const missing = exit(FileRouter.scan({ root: "/definitely/missing/smithers-flows" }))
- expect(missing._tag).toBe("Failure")
- if (missing is Exit.Failure(let cause)) {
- const error = Option.getOrThrow(Cause.findErrorOption(cause))
- expect(error.code).toBe("root_missing")
- }
+ expect(failureOf(FileRouter.scan({ root: "/definitely/missing/smithers-flows" })).code).toBe("root_missing")
let called = false
const config = Object.defineProperty({}, "root", {
@@ -136,8 +132,7 @@
return root
}
})
- const hostile = exit(FileRouter.scan(config as FileRouter.ScanConfig))
- expect(hostile._tag).toBe("Failure")
+ expect(failureOf(FileRouter.scan(config as FileRouter.ScanConfig)).code).toBe("invalid_root")
expect(called).toBe(false)
}
it.effect("never routes a flow or UI companion whose real path leaves the root") {
@@ -162,7 +157,7 @@
const result = FileRouter.scan({ root: flows })
expect(result.routes.map((route) => route.name)).toEqual(["inside", "linkedui"])
- expect(Option.isNone(result.routes.find((route) => route.name === "linkedui")!.ui)).toBe(true)
+ expect(result.routes.find((route) => route.name === "linkedui")!.ui.isNone()).toBe(true)
expect(result.warnings).toEqual(expect.arrayContaining([
expect.objectContaining({ code: "outside_root", path: path.join(flows, "dir") }),
expect.objectContaining({ code: "outside_root", path: path.join(flows, "file", "flow.ts") })
 
packages/smithers/agent/fs/test/FileRouterFailures.test.vibe+20 −28
@@ -5,7 +5,7 @@
import { vi } from "vitest"
const discovery = vi.hoisted(() => ({
- scan: undefined as undefined | ((options: unknown) => Effect.Effect<unknown, unknown>)
+ scan: undefined as undefined | ((options: unknown) => unknown!unknown)
}))
vi.mock("@smthrs/registry/Discovery", (importOriginal) =>
@@ -34,13 +34,12 @@
const result = (entries: ReadonlyArray<Descriptor.FlowDescriptor>) => ({ entries, warnings: [] })
-// A stub whose `exists` may fail with a foreign error a real file system never raises.
-const fileSystem = (
- exists: (path: string) => Effect.Effect<boolean, unknown> = () => Effect.succeed(false)
-): FileSystem.FileSystem => ({ exists } as unknown as FileSystem.FileSystem)
+// A stub whose methods may fail with a foreign error a real file system never raises.
+const fileSystem = (methods: { exists: () => boolean!unknown; realPath?: () => string!unknown }) =>
+ methods as unknown as FileSystem.FileSystem
/** Scans `/flows` over the mocked discovery, a stub file system, and the real path service. */
-function scanFlows(files: FileSystem.FileSystem = fileSystem()) {
+function scanFlows(files: FileSystem.FileSystem = fileSystem({ exists: () => false })) {
provide FileSystem.FileSystem = files, NodePath.layer {
return FileRouter.scan({ root: "/flows" })
}
@@ -56,11 +55,12 @@
["unknown", "discovery_failed"]
] as const
) {
- discovery.scan = () => Effect.fail(discoveryError({ code: source, method: "scan" }))
+ discovery.scan = () => throw discoveryError({ code: source, method: "scan" })
expect(failureOf(scanFlows()).code).toBe(expected)
}
- discovery.scan = () => Effect.fail(new Error("TOP-SECRET"))
+ // @vibelang-diagnostics-next-line EK4804:off
+ discovery.scan = () => throw new Error("TOP-SECRET")
const foreign = failureOf(scanFlows())
expect(foreign.code).toBe("discovery_failed")
expect(JSON.stringify(foreign)).not.toContain("TOP-SECRET")
@@ -68,41 +68,33 @@
it.effect("bounds scan results before inspecting route companions") {
discovery.scan = () =>
- Effect.succeed(result(
- Array.from({ length: CommandTree.maximumRoutes + 1 }, (_, index) => descriptor(`/flows/r${index}/flow.ts`))
- ))
+ result(Array.from({ length: CommandTree.maximumRoutes + 1 }, (_, index) => descriptor(`/flows/r${index}/flow.ts`)))
expect(failureOf(scanFlows()).code).toBe("resource_limit")
}
it.effect("skips root entries and refuses paths collapsing to one route") {
- discovery.scan = () =>
- Effect.succeed(result([
- descriptor("/flows/flow.ts"),
- descriptor("/flows/kept/flow.ts")
- ]))
+ discovery.scan = () => result([descriptor("/flows/flow.ts"), descriptor("/flows/kept/flow.ts")])
expect(scanFlows().routes.map((route) => route.name)).toEqual(["kept"])
- discovery.scan = () =>
- Effect.succeed(result([
- descriptor("/flows/duplicate/flow.ts"),
- descriptor("/flows/duplicate/other.ts")
- ]))
+ discovery.scan = () => result([descriptor("/flows/duplicate/flow.ts"), descriptor("/flows/duplicate/other.ts")])
expect(failureOf(scanFlows()).code).toBe("duplicate_route")
}
it.effect("maps companion inspection failures without retaining their cause") {
- discovery.scan = () => Effect.succeed(result([descriptor("/flows/route/flow.ts")]))
- const error = failureOf(scanFlows(fileSystem(() => Effect.fail(new Error("TOP-SECRET")))))
+ discovery.scan = () => result([descriptor("/flows/route/flow.ts")])
+ // @vibelang-diagnostics-next-line EK4804:off
+ const error = failureOf(scanFlows(fileSystem({ exists: () => throw new Error("TOP-SECRET") })))
expect(error.code).toBe("read_failed")
expect(JSON.stringify(error)).not.toContain("TOP-SECRET")
}
it.effect("refuses a UI companion whose real path cannot be resolved") {
- discovery.scan = () => Effect.succeed(result([descriptor("/flows/route/flow.ts")]))
- const unresolvable = {
- exists: () => Effect.succeed(true),
- realPath: () => Effect.fail(new Error("TOP-SECRET"))
- } as unknown as FileSystem.FileSystem
+ discovery.scan = () => result([descriptor("/flows/route/flow.ts")])
+ const unresolvable = fileSystem({
+ exists: () => true,
+ // @vibelang-diagnostics-next-line EK4804:off
+ realPath: () => throw new Error("TOP-SECRET")
+ })
const error = failureOf(scanFlows(unresolvable))
expect(error.code).toBe("read_failed")
expect(JSON.stringify(error)).not.toContain("TOP-SECRET")
 
packages/smithers/agent/fs/test/FlowInvoker.test.vibe+10 −14
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "@effect/vitest"
import * as FlowInvoker from "../src/FlowInvoker.vibe"
import visibleFlow from "./fixtures/command/visible.ts"
+import { failureOf } from "./helpers.vibe"
const invocation = Object.freeze({ name: "visible", flow: visibleFlow, input: Object.freeze({ number: 1 }) })
@@ -20,9 +21,9 @@
}
it("rejects non-record, inherited, accessor, and non-function implementations", () => {
- const getter = vi.fn(() => () => Effect.void)
+ const getter = vi.fn(() => () => undefined)
const accessor = Object.defineProperty({}, "invoke", { enumerable: true, get: getter })
- const inherited = Object.create({ invoke: () => Effect.void })
+ const inherited = Object.create({ invoke: () => undefined })
for (const input of [null, 1, {}, inherited, accessor, { invoke: true }]) {
expect(() => FlowInvoker.make(input as never)).toThrow(TypeError)
}
@@ -31,30 +32,25 @@
it.effect("fails closed without retaining invocation data") {
const service = FlowInvoker.makeNoop()
- const secretInvocation = { ...invocation, name: "TOP-SECRET" }
- const outcome = exit(service.invoke(secretInvocation))
+ const error = failureOf(service.invoke({ ...invocation, name: "TOP-SECRET" }))
expect(Object.isFrozen(service)).toBe(true)
- expect(outcome._tag).toBe("Failure")
- if (outcome is Exit.Failure(let cause)) {
- const error = Option.getOrThrow(Cause.findErrorOption(cause))
- expect(error).toMatchObject({ code: "invocation_unavailable", method: "FlowInvoker.invoke" })
- expect(JSON.stringify(error)).not.toContain("TOP-SECRET")
- }
+ expect(error).toMatchObject({ code: "invocation_unavailable", method: "FlowInvoker.invoke" })
+ expect(JSON.stringify(error)).not.toContain("TOP-SECRET")
}
it.effect("accepts a valid override and provides it through the layer") {
// The type argument makes the mocked lambda an invoker, which returns a program.
- const override = vi.fn<FlowInvoker.Service["invoke"]>(() => "overridden")
+ const override = vi.fn<FlowInvoker.FlowInvoker["invoke"]>(() => "overridden")
const service = FlowInvoker.makeNoop({ invoke: override })
const result = do {
provide FlowInvoker.FlowInvoker = service {
- FlowInvoker.FlowInvoker().invoke(invocation)
+ FlowInvoker.FlowInvoker.invoke(invocation)
}
}
const layered = do {
provide FlowInvoker.layerNoop({ invoke: override }) {
- FlowInvoker.FlowInvoker().invoke(invocation)
+ FlowInvoker.FlowInvoker.invoke(invocation)
}
}
@@ -64,7 +60,7 @@
}
it("rejects accessor and non-function overrides without invoking them", () => {
- const getter = vi.fn(() => () => Effect.void)
+ const getter = vi.fn(() => () => undefined)
const accessor = Object.defineProperty({}, "invoke", { enumerable: true, get: getter })
for (const input of [accessor, { invoke: false }]) {
expect(() => FlowInvoker.makeNoop(input as never)).toThrow(TypeError)
 
packages/smithers/agent/fs/test/Incur.test.vibe+12 −19
@@ -8,7 +8,7 @@
import * as Incur from "../src/Incur.vibe"
import * as Route from "../src/Route.vibe"
import visible from "./fixtures/command/visible.ts"
-import { latchedInvoke, makeRoute, recordedImports, recordedModule, refinedModule } from "./helpers.vibe"
+import { failureOf, latchedInvoke, makeRoute, recordedImports, recordedModule, refinedModule } from "./helpers.vibe"
// Every test runs with `it.live`: a CLI keeps the services of the fiber that
// created it, so a test clock would freeze its metadata deadline and a test
@@ -22,7 +22,7 @@
}
/** Fails the test when a host wait outlasts its real-time deadline. */
-function within<A, E>(program: Effect.Effect<A, E>, milliseconds: number): A!E {
+function within<A, E>(program: A!E, milliseconds: number): A!E {
return Effect.timeoutOrElse(program, {
duration: milliseconds,
orElse: () => die(new Error("Metadata wait exceeded test deadline"))
@@ -48,11 +48,11 @@
awaited(gate)
return visible
} finally (exit) {
- if (exit is Exit.Failure(let cause) && Cause.hasInterrupts(cause)) interrupted?.()
+ if (exit is Exit.Failure(let cause) && cause.hasInterrupts()) interrupted?.()
}
}
-function makeCli(routes: ReadonlyArray<Route.Route> = [makeRoute("review")], invoke?: FlowInvoker.Service["invoke"]) {
+function makeCli(routes: ReadonlyArray<Route.Route> = [makeRoute("review")], invoke?: FlowInvoker.FlowInvoker["invoke"]) {
const seen: Array<FlowInvoker.Invocation> = []
const invoker = FlowInvoker.make({
invoke: invoke ?? ((invocation) => {
@@ -67,7 +67,7 @@
}
/** Creates a CLI whose host logs at debug level through the given logger layer. */
-function debugCli(loggers: Layer.Layer<never>, invoker: FlowInvoker.Service) {
+function debugCli(loggers: Layer.Layer<never>, invoker: FlowInvoker.FlowInvoker) {
provide loggers, References.MinimumLogLevel = "Debug", FlowInvoker.FlowInvoker = invoker {
return Incur.createCli("flows", [makeRoute("review")])
}
@@ -223,7 +223,7 @@
const controller = new AbortController()
const reason = new Error("cancelled during startup")
// The abort runs when the build calls `load`, before its program runs.
- const load = vi.spyOn(Route, "load").mockImplementationOnce((): Effect.Effect<typeof visible, never, never> => {
+ const load = vi.spyOn(Route, "load").mockImplementationOnce((): typeof visible!! => {
controller.abort(reason)
return () => visible
})
@@ -383,7 +383,7 @@
input: Schema.NullOr(Schema.String),
output: Schema.NullOr(Schema.String)
} as const
- vi.spyOn(Route, "load").mockReturnValue(Effect.succeed(flow))
+ vi.spyOn(Route, "load").mockImplementation(() => flow)
try {
const inputs: Array<unknown> = []
const { cli } = makeCli([makeRoute("scalar")], ({ input }) => {
@@ -514,15 +514,8 @@
it.live("refuses a child route that claims the reserved self segment") {
provide FlowInvoker.layerNoop() {
- const outcome = exit(Incur.createCli("flows", [makeRoute("domains"), makeRoute(`domains/${Incur.selfSegment}`)]))
- expect(outcome._tag).toBe("Failure")
- if (outcome is Exit.Failure(let cause)) {
- const failure = Cause.findErrorOption(cause)
- expect(Option.isSome(failure) && failure.value).toMatchObject({
- code: "duplicate_route",
- path: "domains/self"
- })
- }
+ expect(failureOf(Incur.createCli("flows", [makeRoute("domains"), makeRoute(`domains/${Incur.selfSegment}`)])))
+ .toMatchObject({ code: "duplicate_route", path: "domains/self" })
}
}
@@ -744,7 +737,7 @@
it.live.each(["die", "throw", "failure"])("sanitizes an invoker %s", (kind) => {
const sentinel = "SECRET_SENTINEL_credential_from_backend"
- const invoke: FlowInvoker.Service["invoke"] = kind === "throw"
+ const invoke: FlowInvoker.FlowInvoker["invoke"] = kind === "throw"
// A plain function that throws instead of returning a program.
? (): never => {
throw new Error(sentinel)
@@ -755,7 +748,7 @@
: ((): never!Error => {
// @vibelang-diagnostics-next-line EK4804:off
throw new Error(sentinel)
- }) as unknown as FlowInvoker.Service["invoke"]
+ }) as unknown as FlowInvoker.FlowInvoker["invoke"]
const { cli } = makeCli(undefined, invoke)
const response = call(cli, transport)
expect(response.failed).toBe(true)
@@ -806,7 +799,7 @@
expect(response.body).not.toContain(defect.message)
expect(logged).toHaveLength(1)
expect(logged[0]!.message).toEqual(["Incur invocation failed"])
- expect(Cause.squash(logged[0]!.cause)).toBe(defect)
+ expect(logged[0]!.cause.squash()).toBe(defect)
}
it.live("keeps built-in debug diagnostics off CLI stdout") {
 
packages/smithers/agent/fs/test/RouteBoundary.test.vibe+2 −2
@@ -12,8 +12,8 @@
const canonical = sync(async () => (await import("./fixtures/three-projections/flows/review/flow.ts")).default)
expect(loaded.input).toBe(canonical.payloadSchema)
expect(loaded.output).toBe(canonical.successSchema)
- expect(Schema.decodeUnknownSync(canonical.payloadSchema)({ number: 7 })).toEqual({ number: 7 })
- expect(Schema.decodeUnknownSync(canonical.successSchema)({ accepted: true, number: 7 })).toEqual({
+ expect(Schema.decodeUnknownEffect(canonical.payloadSchema)({ number: 7 })).toEqual({ number: 7 })
+ expect(Schema.decodeUnknownEffect(canonical.successSchema)({ accepted: true, number: 7 })).toEqual({
accepted: true,
number: 7
})
 
packages/smithers/agent/fs/test/RuntimeVisibility.test.vibe+1 −1
@@ -26,7 +26,7 @@
})
/** Runs a program with the recording invoker. */
-function invoking<A, E>(program: Effect.Effect<A, E, FlowInvoker.FlowInvoker>): A!E {
+function invoking<A, E>(program: A!E!FlowInvoker.FlowInvoker): A!E {
provide FlowInvoker.FlowInvoker = invoker {
return program()
}
 
packages/smithers/agent/fs/test/SchemaBridge.test.vibe+20 −19
@@ -22,6 +22,11 @@
(importOriginal) => importOriginal<typeof import("effect/Schema")>().then((module) => ({ ...module }))
)
+// A service that a decoding transformation needs and the bridge cannot supply.
+service Prefix {
+ value: string
+}
+
const moduleRef = new Descriptor.SchemaRefModule({ path: "/absolute/flow.ts", field: "input" })
interface Reply {
@@ -485,22 +490,19 @@
nested: Schema.Struct({ mode: Schema.Literals(["fast", "slow"]), count: Schema.Finite })
})
}
- const load = vi.spyOn(Route, "load").mockImplementation((route) =>
- Effect.succeed(
- {
- capabilities: [],
- effects: undefined,
- name: route.name,
- input: inputs[route.name]!,
- output: Schema.Void
- } as const
- )
- )
+ const load = vi.spyOn(Route, "load").mockImplementation((route) => ({
+ capabilities: [],
+ effects: undefined,
+ name: route.name,
+ input: inputs[route.name]!,
+ output: Schema.Void
+ } as const))
try {
- const cli = Effect.provide(
- Incur.createCli("flows", Object.keys(inputs).map((name) => makeRoute(name))),
- FlowInvoker.layerNoop()
- )
+ const cli = do {
+ provide FlowInvoker.layerNoop() {
+ Incur.createCli("flows", Object.keys(inputs).map((name) => makeRoute(name)))
+ }
+ }
for (let attempt = 0; attempt < 2; attempt++) {
const response = request(cli, "http://localhost/openapi.json")
expect(response.status).toBe(200)
@@ -565,20 +567,19 @@
}
it.effect("converts missing schema services into typed refusals") {
- class Prefix extends Context.Service<Prefix, string>()("test/fs/SchemaBridge/Prefix") {}
const ServiceSchema = Schema.String.pipe(
Schema.decodeTo(
Schema.String,
SchemaTransformation.transformEffect({
- decode: (value) => `${Prefix()}${value}`,
- encode: (value) => value.slice(Prefix().length)
+ decode: (value) => `${Prefix().value}${value}`,
+ encode: (value) => value.slice(Prefix().value.length)
})
)
)
expect(failureOf(SchemaBridge.decodeInput(ServiceSchema, "value")).code).toBe("decode_failed")
expect(failureOf(SchemaBridge.encodeOutput(ServiceSchema, "prefix:value")).code).toBe("encode_failed")
- provide Prefix = "prefix:" {
+ provide Prefix = { value: "prefix:" } {
expect(SchemaBridge.decodeInput(ServiceSchema, "value")).toBe("prefix:value")
expect(SchemaBridge.encodeOutput(ServiceSchema, "prefix:value")).toBe("value")
}
 
packages/smithers/agent/fs/test/helpers.vibe+7 −6
@@ -13,10 +13,11 @@
* The expected failure a program fails with; a success or a defect fails the
* test. The program may need services that the enclosing block provides.
*/
-export function failureOf<E, R>(program: Effect.Effect<unknown, E, R>): E!!R {
- const outcome = exit(program)
- if (outcome is Exit.Failure(let cause)) return Option.getOrThrow(Cause.findErrorOption(cause))
- return assert.fail("expected the program to fail")
+export function failureOf<E, R>(program: unknown!E!R): E!!R {
+ return match (exit(program)) {
+ when Exit.Failure(let cause): Cause.findErrorOption(cause).getOrThrow();
+ default: assert.fail("expected the program to fail");
+ }
}
export const makeRoute = (
@@ -45,7 +46,7 @@
* an in-flight fiber without real timers.
*/
export const latchedInvoke = (): {
- invoke: FlowInvoker.Service["invoke"]
+ invoke: FlowInvoker.FlowInvoker["invoke"]
started: Promise<void>
finalized: Promise<void>
release: () => void
@@ -70,7 +71,7 @@
return gate
}, begin, gate)
} finally (exit) {
- if (exit is Exit.Failure(let cause) && Cause.hasInterrupts(cause)) finish()
+ if (exit is Exit.Failure(let cause) && cause.hasInterrupts()) finish()
}
},
started,
 
packages/smithers/agent/integrations/test/BrowserBundle.test.ts+5 −0
@@ -17,11 +17,14 @@
import { createHash, createHmac } from "node:crypto"
import { fileURLToPath } from "node:url"
import { createContext, runInContext } from "node:vm"
+import vibelang from "vibelang-unplugin/esbuild"
import { afterEach, describe, expect, it } from "vitest"
import { type Fixture, json, startFixture } from "./Fixture.ts"
const root = fileURLToPath(new URL("..", import.meta.url))
const conditions = ["workerd", "worker", "browser", "import", "default"]
+// The root reaches @smthrs/errors, whose source is VibeLang.
+const plugins = [vibelang({ root })]
/** The root entry, plus the `effect` modules a caller runs it with. */
type Realm =
@@ -84,6 +87,7 @@
format: "iife",
globalName: "realm",
target: "es2022",
+ plugins,
write: false,
logLevel: "silent"
})
@@ -114,6 +118,7 @@
platform: "browser",
conditions,
format: "esm",
+ plugins,
write: false,
logLevel: "silent",
metafile: true
 
packages/smithers/agent/integrations/test/GitHubProxy.test.ts+9 −1
@@ -235,7 +235,15 @@
}).pipe(Effect.provide(FetchHttpClient.layer)))
`
const spawn = () =>
- promisify(execFile)(process.execPath, ["--input-type=module", "-e", writer, origin], {
+ // The writer imports source that reaches @smthrs/errors, which is VibeLang.
+ promisify(execFile)(process.execPath, [
+ "--import",
+ "vibelang/register",
+ "--input-type=module",
+ "-e",
+ writer,
+ origin
+ ], {
cwd: fileURLToPath(new URL("..", import.meta.url)),
timeout: 30_000
})
 

Config, docs and lockfile

packages/smithers/agent/fs/docs/api.md+26 −26
@@ -291,26 +291,26 @@
| `flow` | `Flow.Any` | The loaded flow, from [@smthrs/core](/api/core). |
| `input` | `unknown` | The decoded input; see `CommandSurface` for snapshot semantics. |
-### `Service`
-
-Executes a materialized flow.
-
-| Field | Type | Description |
+### `FlowInvoker`
+
+```ts
+service FlowInvoker {
+ invoke(invocation: Invocation): unknown!FsError
+}
+```
+
+Executes a materialized flow. The VibeLang `service` declaration is both the
+service key, `"@smthrs/fs/src/FlowInvoker#FlowInvoker"`, and, as a type, the
+implementation's shape.
+
+| Member | Type | Description |
| -------- | ------------------------------------------------------------- | --------------------------- |
| `invoke` | `(invocation: Invocation) => Effect.Effect<unknown, FsError>` | Runs one materialized flow. |
-### `FlowInvoker`
-
-```ts
-class FlowInvoker extends Context.Service<FlowInvoker, Service>()("/fs/FlowInvoker")
-```
-
-The flow invocation service tag, keyed `"/fs/FlowInvoker"`.
-
### `FlowInvoker.make`
```ts
-;((implementation: Service) => Service)
+;((implementation: FlowInvoker) => FlowInvoker)
```
Constructs a frozen flow invoker from an implementation. The implementation
@@ -321,7 +321,7 @@
### `FlowInvoker.makeNoop`
```ts
-;((overrides?: Partial<Service>) => Service)
+;((overrides?: Partial<FlowInvoker>) => FlowInvoker)
```
Constructs an invoker that fails every invocation with
@@ -332,7 +332,7 @@
### `FlowInvoker.layerNoop`
```ts
-;((overrides?: Partial<Service>) => Layer.Layer<FlowInvoker>)
+;((overrides?: Partial<FlowInvoker>) => Layer.Layer<FlowInvoker>)
```
Provides `makeNoop` as the `FlowInvoker` layer.
@@ -362,23 +362,23 @@
| "invocation_unavailable"
```
-Stable failure codes for routing, parsing, loading, and decoding, also
-exported as a `Schema.Literals` value for decoding. For the meaning and
-trigger of each code, see the
+Stable failure codes for routing, parsing, loading, and decoding. For the
+meaning and trigger of each code, see the
[error codes table](./contract.md#error-codes).
### `FsError`
```ts
-class FsError extends Schema.TaggedError<FsError>()("flows/fs/FsError", {
- code: Code,
- method: Schema.String,
- description: Schema.String,
- path: Schema.optional(Schema.String)
-})
+error FsError {
+ code: Code
+ method: string
+ description: string
+ path?: string | undefined
+}
```
-A recoverable file-routing failure. `method` names the surface that failed so
+A VibeLang `error` declaration: a tagged error class whose `_tag` is
+`"FsError"`, with readonly fields. A recoverable file-routing failure. `method` names the surface that failed so
a CLI or an agent can report the origin without a stack trace. Raw argv,
input values, output values, schema issues, and implementation causes are
deliberately not retained at this boundary.
 
packages/smithers/agent/fs/docs/exports.md+4 −6
@@ -1,9 +1,9 @@
---
title: "Exported members"
-description: "The public surface of @smthrs/fs as one index: 8 namespaces and 44 documented members."
+description: "The public surface of @smthrs/fs as one index: 8 namespaces and 42 documented members."
---
-The surface of `@smthrs/fs` is 8 namespaces and 44 documented members. Anything
+The surface of `@smthrs/fs` is 8 namespaces and 42 documented members. Anything
the package exports that this index does not name is internal. For signatures,
behavior, and errors, see the [API reference](./api.md).
@@ -61,8 +61,7 @@
| Export | Kind | Category | Summary |
| ------------- | --------- | ------------ | -------------------------------------------------- |
| `Invocation` | interface | models | One materialized invocation. |
-| `Service` | interface | models | Executes a materialized flow. |
-| `FlowInvoker` | class | services | The flow invocation service. |
+| `FlowInvoker` | service | services | Executes a materialized flow. |
| `make` | const | constructors | Constructs a flow invoker from an implementation. |
| `makeNoop` | const | constructors | Constructs an invoker that fails every invocation. |
| `layerNoop` | const | layers | Provides an invoker that fails every invocation. |
@@ -73,9 +72,8 @@
| Export | Kind | Category | Summary |
| --------- | ----- | -------- | ----------------------------------------------------------------- |
-| `Code` | const | models | Stable failure codes for routing, parsing, loading, and decoding. |
| `Code` | type | models | Stable failure codes for routing, parsing, loading, and decoding. |
-| `FsError` | class | errors | A recoverable file-routing failure. |
+| `FsError` | error | errors | A recoverable file-routing failure. |
## `Incur`
 
packages/smithers/agent/fs/package.json+0 −1
@@ -114,7 +114,6 @@
"typescript-eslint": "8.69.0",
"vibelang": "0.0.2",
"vibelang-vite": "0.0.2",
- "vite": "^8.2.2",
"vitest": "5.0.0"
}
}
 
packages/smithers/agent/integrations/package.json+3 −0
@@ -504,6 +504,9 @@
"madge": "8.0.0",
"typescript": "5.9.3",
"typescript-eslint": "8.69.0",
+ "vibelang": "0.0.2",
+ "vibelang-unplugin": "0.0.2",
+ "vibelang-vite": "0.0.2",
"vitest": "5.0.0"
}
}
 
packages/smithers/agent/integrations/vitest.config.ts+4 −1
@@ -1,9 +1,12 @@
import { tmpdir } from "node:os"
import { join } from "node:path"
-import { defineConfig } from "vitest/config"
+import vibelang from "vibelang-vite"
+import { configDefaults, defineConfig } from "vitest/config"
export default defineConfig({
+ plugins: [vibelang({ project: "tsconfig.test.json" })],
test: {
+ include: [...configDefaults.include, "**/*.{test,spec}.vibe"],
environment: "node",
// House convention (see packages/smithers/flows/journal/vitest.config.ts): a finite 30 s
// wall-clock budget so correct suites survive coverage-instrumented load
 
pnpm-lock.yaml+10 −37
@@ -2012,12 +2012,9 @@
vibelang-vite:
specifier: link:../../../../../effectscript/packages/vite
version: link:../../../../../effectscript/packages/vite
- vite:
- specifier: ^8.2.2
- version: 8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)
vitest:
specifier: 5.0.0
- version: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/coverage-v8@5.0.0)(happy-dom@20.13.2)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))
+ version: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/coverage-v8@5.0.0)(happy-dom@20.13.2)(vite@8.3.4(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))
packages/smithers/agent/harness:
dependencies:
@@ -2301,6 +2298,15 @@
typescript-eslint:
specifier: 8.69.0
version: 8.69.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
+ vibelang:
+ specifier: link:../../../../../effectscript/packages/compiler
+ version: link:../../../../../effectscript/packages/compiler
+ vibelang-unplugin:
+ specifier: link:../../../../../effectscript/packages/unplugin
+ version: link:../../../../../effectscript/packages/unplugin
+ vibelang-vite:
+ specifier: link:../../../../../effectscript/packages/vite
+ version: link:../../../../../effectscript/packages/vite
vitest:
specifier: 5.0.0
version: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/coverage-v8@5.0.0)(happy-dom@20.13.2)(vite@8.3.4(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))
@@ -22350,15 +22356,6 @@
dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1
- '@vitest/mocker@5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))':
- dependencies:
- '@jridgewell/trace-mapping': 0.3.31
- '@vitest/spy': 5.0.0
- estree-walker: 3.0.3
- magic-string: 1.2.3
- optionalDependencies:
- vite: 8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)
-
'@vitest/mocker@5.0.0(vite@8.3.4(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))':
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -27949,30 +27946,6 @@
optionalDependencies:
vite: 8.3.4(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)
- vitest@5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/coverage-v8@5.0.0)(happy-dom@20.13.2)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)):
- dependencies:
- '@types/chai': 5.2.3
- '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))
- chai: 6.2.2
- es-module-lexer: 2.3.2
- expect-type: 1.4.0
- magic-string: 1.2.3
- obug: 2.1.4
- picomatch: 4.0.7
- std-env: 4.2.0
- tinybench: 6.1.4
- tinyexec: 1.3.0
- tinyglobby: 0.2.17
- vite: 8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)
- why-is-node-running: 2.3.0
- optionalDependencies:
- '@opentelemetry/api': 1.9.1
- '@types/node': 26.4.1
- '@vitest/coverage-v8': 5.0.0(vitest@5.0.0)
- happy-dom: 20.13.2
- transitivePeerDependencies:
- - msw
-
vitest@5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/coverage-v8@5.0.0)(happy-dom@20.13.2)(vite@8.3.4(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)):
dependencies:
'@types/chai': 5.2.3