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
FsError's tag:"flows/fs/FsError"→"FsError".FlowInvoker's key:"/fs/FlowInvoker"→"@smthrs/fs/src/FlowInvoker#FlowInvoker".FlowInvoker.Serviceand theFsError.Codeschema value are gone. Nothing outside the package used them.
Language gaps
Five need your call on syntax. Five are compiler or doc defects I'm fixing.
| Gap | Proposed | |
|---|---|---|
| Your call | error 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 call | No schema-backed error: error always lowers to Data.TaggedError. | schema error FsError { … } → Schema.TaggedError |
| Your call | schema declares only structs, so a literal union stays a TypeScript type. | schema Code = "root_missing" | "read_failed" | … |
| Your call | A service whose implementation is a plain value has no service form. | service Prefix: string |
| Your call | A 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) |
| Fixing | A 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. |
| Fixing | A catch expression doesn't pass its contextual type to its operands, so literal _tags widen. | Type both sides from the contextual type. |
| Fixing | An exhaustive match without default emits an unreachable branch, so 100% branch coverage is impossible. | Lower the last arm of an exhaustive match unconditionally. |
| Fixing | EK2001 garbles the member name in its message when the member is typed by context. | Print the member name. |
| Fixing | Cause.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 soa CLI or an agent can report the origin without a stack trace. Raw argv,input values, output values, schema issues, and implementation causes aredeliberately 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) breaknode = 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: unknownconst 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 = falselet 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] : undefinedoptions[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 = 0const 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 = falseconst 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:offthrow 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 soa CLI or an agent can report the origin without a stack trace. Raw argv,input values, output values, schema issues, and implementation causes aredeliberately 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. Anythingthe 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/viteversion: 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.0version: 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/vitevitest:specifier: 5.0.0version: 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