@fungi.computer/gills 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # Gills (`@fungi.computer/gills`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Gills defines compact, plain-text App views shared by browser and terminal
4
+ hosts. It owns the row wire format, Gill declarations, bounded requests and
5
+ responses, and an authenticated-fetch client. Your host owns admission,
6
+ navigation and read-method admission.
7
+
8
+ ## Install or set up
9
+
10
+ ```sh
11
+ npm install @fungi.computer/gills effect@4.0.2
12
+ ```
13
+
14
+ Gills is ESM. Effect is its only runtime dependency. All exports are available
15
+ from `@fungi.computer/gills` in Node, browsers and Workers.
16
+
17
+ ## Quick start
18
+
19
+ Decode a row page and the selectors a caller may send:
20
+
21
+ ```ts
22
+ import * as Schema from "effect/Schema";
23
+ import { rowPageSchemaV1, gillRequestSchema } from "@fungi.computer/gills";
24
+
25
+ const rows = Schema.decodeUnknownSync(rowPageSchemaV1)([
26
+ { id: "session-a", title: "Review the draft", tone: "attention" },
27
+ ]);
28
+ const request = Schema.decodeUnknownSync(gillRequestSchema, {
29
+ onExcessProperty: "error",
30
+ })({ viewId: "inbox" });
31
+ console.log(rows[0]?.title, request.viewId);
32
+ ```
33
+
34
+ ## Core concepts
35
+
36
+ - A **declaration** records the Gill's App-local identity, source read method,
37
+ optional placement parameters and their options read methods, change feed and
38
+ desktop sizes. Agar generates it from the backend; Puffball includes it in the
39
+ Release digest.
40
+ - A **request** contains a view ID, placement parameters and optional source
41
+ cursor. The host admits the method and Computer before invoking the App.
42
+ - A **response** is an empty page, a full page of text rows, or an error. A
43
+ request with `options: <parameter key>` returns a choice page instead.
44
+ `decodeDeclaredGillResponse` rejects another view's identity.
45
+ - A **row** carries display text and optional navigation parameters. The host
46
+ opens the Gill's own App; that App interprets the parameters. Rows cannot name
47
+ a different App, a URL, or a Session-specific target. Navigation values retain
48
+ up to 4,096 characters, including a Computer file path; display shortening
49
+ never changes navigation values.
50
+ - Optional `row.status` carries the App's plain label and semantic tone for a
51
+ compact status indicator. The App maps its state; the host does not infer it.
52
+ - An error can offer `recovery: { kind: "configure", param: "computer" }` or
53
+ `recovery: { kind: "open" }`. Configure must name a declared parameter and
54
+ opens that placement's existing picker. Open uses the host's launcher for the
55
+ same App. Neither form contains an App ID, URL or command. App names and icons
56
+ remain host directory presentation, outside this wire.
57
+
58
+ ## API reference
59
+
60
+ See the [generated API](docs/api/index.md) for the schemas, inferred types and
61
+ `createGillClient`.
62
+
63
+ ## Examples
64
+
65
+ `createGillClient({ endpoint, fetch, declaration })` sends bounded selectors
66
+ with PUT and decodes the result. Supply an authenticated `fetch` pinned to the
67
+ selected App and Release. An optional `decodeRefusal(raw, declaration)` handles
68
+ recognized HTTP 503 bodies using the host's own decoder. Unrecognized refusals
69
+ throw; the client does not choose host policy.
70
+
71
+ Agar's App execution refusal decoder remains in `@fungi.computer/agar/gills`.
72
+ App producers and renderers import the shared contract directly from this
73
+ package.
74
+
75
+ ## Limits and errors
76
+
77
+ Pages contain at most 200 rows. Row labels allow 4,096 characters, identities
78
+ 513 and details 1,024. A navigation intent has at most 16 parameters, with
79
+ 64-character keys and 4,096-character values. Requests bound cursors to 1,024
80
+ characters and placement context to four keys with 256-character values.
81
+ Declarations offer up to four unique parameters of kind Computer, repository or
82
+ branch, and unique desktop sizes from S, M and L. An optional watch names an
83
+ existing storage marker or binding change feed. Resource envelopes and row
84
+ actions are not part of the contract.
85
+
86
+ Decoders reject invalid wire values. Gill output is text data, never executable
87
+ UI. Loading is host state. The client throws on HTTP errors unless the injected
88
+ host decoder recognizes the refusal.
89
+
90
+ ## Contribute and license
91
+
92
+ Run `pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm test`, `pnpm docs` and
93
+ `pnpm stranger` in this package. Regenerate API docs with `pnpm docs:write`.
94
+ Licensed under [MIT](LICENSE).
95
+
96
+ ### Shared row time copy
97
+
98
+ `relativeTime` from `@fungi.computer/gills/presentation` formats a reported
99
+ millisecond timestamp using the Apps' existing English relative-time copy. This
100
+ pure entry does not load the Gill decoder. Source methods keep their timestamps
101
+ unchanged; a projection only formats timestamps its source returns.
@@ -0,0 +1,55 @@
1
+ import * as Schema from "effect/Schema";
2
+ /** Sizes the App explicitly offers for a desktop Gill. */
3
+ export type AppGillDesktopSize = "S" | "M" | "L";
4
+ /** One App-owned placement selector. */
5
+ export type AppGillParam = Readonly<{
6
+ key: string;
7
+ label: string;
8
+ kind: "computer" | "repository" | "branch";
9
+ required: boolean;
10
+ /** This App's declared read method returning picker choices; Computers are host-owned. */
11
+ options?: string | undefined;
12
+ }>;
13
+ /** An existing change feed used to refresh a Gill. */
14
+ export type AppGillWatch = Readonly<{
15
+ storage: string;
16
+ } | {
17
+ binding: string;
18
+ }>;
19
+ /** A digest-covered projection of one declared read method. */
20
+ export type AppDeclaredGill = Readonly<{
21
+ id: string;
22
+ source: string;
23
+ title: string;
24
+ params?: readonly AppGillParam[] | undefined;
25
+ watch?: AppGillWatch | undefined;
26
+ desktop?: Readonly<{
27
+ sizes: readonly AppGillDesktopSize[];
28
+ }> | undefined;
29
+ }>;
30
+ /** Bounded, App-owned parameter key. */
31
+ export declare const appGillParamKeySchema: Schema.String;
32
+ /** Placement parameter metadata, included in the Release digest. */
33
+ export declare const appGillParamSchema: Schema.Struct<{
34
+ readonly key: Schema.String;
35
+ readonly label: Schema.String;
36
+ readonly kind: Schema.Literals<readonly ["computer", "repository", "branch"]>;
37
+ readonly required: Schema.Boolean;
38
+ readonly options: Schema.optional<Schema.String>;
39
+ }>;
40
+ /** The bounded, uniquely keyed placement parameters shared with desktop catalogs. */
41
+ export declare const appGillParamsSchema: Schema.$Array<Schema.Struct<{
42
+ readonly key: Schema.String;
43
+ readonly label: Schema.String;
44
+ readonly kind: Schema.Literals<readonly ["computer", "repository", "branch"]>;
45
+ readonly required: Schema.Boolean;
46
+ readonly options: Schema.optional<Schema.String>;
47
+ }>>;
48
+ /** A storage marker or host binding's existing change feed. */
49
+ export declare const appGillWatchSchema: Schema.Union<readonly [Schema.Struct<{
50
+ readonly storage: Schema.String;
51
+ }>, Schema.Struct<{
52
+ readonly binding: Schema.String;
53
+ }>]>;
54
+ /** Digest-covered identity, source, context and desktop sizes of a Gill. */
55
+ export declare const appDeclaredGillSchema: Schema.Codec<AppDeclaredGill>;
@@ -0,0 +1,41 @@
1
+ import * as Schema from "effect/Schema";
2
+ const isUnique = (values) => new Set(values).size === values.length;
3
+ const meaningful = Schema.String.pipe(Schema.check(Schema.isMaxLength(256)), Schema.check(Schema.makeFilter((value) => value.trim().length > 0, {
4
+ message: "Declared command text must not be blank",
5
+ })));
6
+ /** Bounded, App-owned parameter key. */
7
+ export const appGillParamKeySchema = meaningful.pipe(Schema.check(Schema.isMaxLength(64)));
8
+ /** Placement parameter metadata, included in the Release digest. */
9
+ export const appGillParamSchema = Schema.Struct({
10
+ key: appGillParamKeySchema,
11
+ label: meaningful,
12
+ kind: Schema.Literals(["computer", "repository", "branch"]),
13
+ required: Schema.Boolean,
14
+ options: Schema.optional(meaningful),
15
+ }).check(Schema.makeFilter((param) => param.kind !== "computer" || param.options === undefined, {
16
+ message: "Computer options are host-owned",
17
+ }));
18
+ /** The bounded, uniquely keyed placement parameters shared with desktop catalogs. */
19
+ export const appGillParamsSchema = Schema.Array(appGillParamSchema).check(Schema.isMaxLength(4), Schema.makeFilter((params) => isUnique(params.map((param) => param.key)), {
20
+ message: "Gill parameter keys must be unique",
21
+ }));
22
+ /** A storage marker or host binding's existing change feed. */
23
+ export const appGillWatchSchema = Schema.Union([
24
+ Schema.Struct({ storage: meaningful }),
25
+ Schema.Struct({ binding: meaningful }),
26
+ ]);
27
+ /** Digest-covered identity, source, context and desktop sizes of a Gill. */
28
+ export const appDeclaredGillSchema = Schema.Struct({
29
+ id: meaningful.pipe(Schema.check(Schema.makeFilter((id) => !id.includes(":"), {
30
+ message: "Gill IDs are App-local",
31
+ }))),
32
+ title: meaningful,
33
+ source: meaningful,
34
+ params: Schema.optional(appGillParamsSchema),
35
+ watch: Schema.optional(appGillWatchSchema),
36
+ desktop: Schema.optional(Schema.Struct({
37
+ sizes: Schema.Array(Schema.Literals(["S", "M", "L"])).check(Schema.isMinLength(1), Schema.isMaxLength(3), Schema.makeFilter(isUnique, {
38
+ message: "Desktop Gill sizes must be unique",
39
+ })),
40
+ })),
41
+ });
package/dist/gill.d.ts ADDED
@@ -0,0 +1,248 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { type AppDeclaredGill } from "./declaration.js";
3
+ /** Placement values are bounded; the admitting host checks declared keys. */
4
+ export declare const gillParamsSchema: Schema.$Record<Schema.String, Schema.String>;
5
+ /** The caller supplies placement context and a source-owned continuation. */
6
+ export declare const gillRequestSchema: Schema.Struct<{
7
+ readonly viewId: Schema.String;
8
+ readonly params: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
9
+ readonly after: Schema.optional<Schema.String>;
10
+ readonly options: Schema.optional<Schema.String>;
11
+ }>;
12
+ /** Selectors supplied by a Gill caller. */
13
+ export type GillRequest = Schema.Schema.Type<typeof gillRequestSchema>;
14
+ /** Runtime call selected by the host after admitting a declared source. */
15
+ export declare const gillCallSchema: Schema.Struct<{
16
+ readonly id: Schema.String;
17
+ readonly params: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
18
+ readonly after: Schema.optional<Schema.String>;
19
+ readonly options: Schema.optional<Schema.String>;
20
+ }>;
21
+ /** Input to the generated backend Gill entry. */
22
+ export type GillCall = Schema.Schema.Type<typeof gillCallSchema>;
23
+ /** Shared response codec. Rows are plain text and cannot carry executable UI. */
24
+ export declare const gillResponseSchema: Schema.Union<readonly [Schema.Struct<{
25
+ readonly viewId: Schema.String;
26
+ readonly title: Schema.String;
27
+ readonly state: Schema.Literal<"empty">;
28
+ readonly rows: Schema.Tuple<readonly []>;
29
+ readonly next: Schema.optional<Schema.String>;
30
+ }>, Schema.Struct<{
31
+ readonly viewId: Schema.String;
32
+ readonly title: Schema.String;
33
+ readonly state: Schema.Literal<"error">;
34
+ readonly rows: Schema.Tuple<readonly []>;
35
+ /** Host-owned permanent execution refusal; an ordinary error may still be refreshed. */
36
+ readonly reason: Schema.optional<Schema.Literals<readonly ["exec_isolation_required", "params_required"]>>;
37
+ readonly error: Schema.String;
38
+ /** The host supplies the same-placement picker and this App's launch. */
39
+ readonly recovery: Schema.optional<Schema.Union<readonly [Schema.Struct<{
40
+ readonly kind: Schema.Literal<"configure">;
41
+ readonly param: Schema.String;
42
+ }>, Schema.Struct<{
43
+ readonly kind: Schema.Literal<"open">;
44
+ }>]>>;
45
+ }>, Schema.Struct<{
46
+ readonly viewId: Schema.String;
47
+ readonly title: Schema.String;
48
+ readonly state: Schema.Literal<"full">;
49
+ readonly rows: Schema.$Array<Schema.Struct<{
50
+ readonly id: Schema.String;
51
+ readonly title: Schema.String;
52
+ readonly detail: Schema.optional<Schema.String>;
53
+ readonly tone: Schema.optional<Schema.Literals<readonly ["attention", "running", "error", "settled", "muted"]>>;
54
+ readonly status: Schema.optional<Schema.Struct<{
55
+ readonly label: Schema.String;
56
+ readonly tone: Schema.Literals<readonly ["neutral", "primary", "secondary", "accent", "success", "info", "warning", "error"]>;
57
+ }>>;
58
+ readonly open: Schema.optional<Schema.Struct<{
59
+ readonly params: Schema.$Array<Schema.Struct<{
60
+ readonly key: Schema.String;
61
+ readonly value: Schema.String;
62
+ }>>;
63
+ }>>;
64
+ }>>;
65
+ readonly next: Schema.optional<Schema.String>;
66
+ }>]>;
67
+ /** A decoded page of plain-text Gill rows or a bounded error. */
68
+ export type GillResponse = Schema.Schema.Type<typeof gillResponseSchema>;
69
+ /** Decode backend output once at the App host boundary. */
70
+ export declare const decodeGillResponse: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
71
+ readonly state: "empty";
72
+ readonly title: string;
73
+ readonly viewId: string;
74
+ readonly rows: readonly [];
75
+ readonly next?: string | undefined;
76
+ } | {
77
+ readonly error: string;
78
+ readonly state: "error";
79
+ readonly title: string;
80
+ readonly viewId: string;
81
+ readonly rows: readonly [];
82
+ readonly reason?: "exec_isolation_required" | "params_required" | undefined;
83
+ readonly recovery?: {
84
+ readonly kind: "configure";
85
+ readonly param: string;
86
+ } | {
87
+ readonly kind: "open";
88
+ } | undefined;
89
+ } | {
90
+ readonly state: "full";
91
+ readonly title: string;
92
+ readonly viewId: string;
93
+ readonly rows: readonly {
94
+ readonly id: string;
95
+ readonly title: string;
96
+ readonly detail?: string | undefined;
97
+ readonly tone?: "error" | "running" | "attention" | "settled" | "muted" | undefined;
98
+ readonly open?: {
99
+ readonly params: readonly {
100
+ readonly key: string;
101
+ readonly value: string;
102
+ }[];
103
+ } | undefined;
104
+ readonly status?: {
105
+ readonly label: string;
106
+ readonly tone: "accent" | "error" | "success" | "neutral" | "primary" | "secondary" | "info" | "warning";
107
+ } | undefined;
108
+ }[];
109
+ readonly next?: string | undefined;
110
+ };
111
+ /** A bounded choice page returned by an App-owned options read method. */
112
+ export declare const gillOptionsResponseSchema: Schema.Struct<{
113
+ readonly options: Schema.$Array<Schema.Struct<{
114
+ readonly value: Schema.String;
115
+ readonly label: Schema.String;
116
+ readonly detail: Schema.optional<Schema.String>;
117
+ }>>;
118
+ readonly next: Schema.optional<Schema.String>;
119
+ }>;
120
+ /** Choices remain within the selected App; the host supplies Computer choices. */
121
+ export type GillOptionsResponse = Schema.Schema.Type<typeof gillOptionsResponseSchema>;
122
+ /** Shared wire for a rows read or a placement-options read. */
123
+ export declare const gillReplySchema: Schema.Union<readonly [Schema.Union<readonly [Schema.Struct<{
124
+ readonly viewId: Schema.String;
125
+ readonly title: Schema.String;
126
+ readonly state: Schema.Literal<"empty">;
127
+ readonly rows: Schema.Tuple<readonly []>;
128
+ readonly next: Schema.optional<Schema.String>;
129
+ }>, Schema.Struct<{
130
+ readonly viewId: Schema.String;
131
+ readonly title: Schema.String;
132
+ readonly state: Schema.Literal<"error">;
133
+ readonly rows: Schema.Tuple<readonly []>;
134
+ /** Host-owned permanent execution refusal; an ordinary error may still be refreshed. */
135
+ readonly reason: Schema.optional<Schema.Literals<readonly ["exec_isolation_required", "params_required"]>>;
136
+ readonly error: Schema.String;
137
+ /** The host supplies the same-placement picker and this App's launch. */
138
+ readonly recovery: Schema.optional<Schema.Union<readonly [Schema.Struct<{
139
+ readonly kind: Schema.Literal<"configure">;
140
+ readonly param: Schema.String;
141
+ }>, Schema.Struct<{
142
+ readonly kind: Schema.Literal<"open">;
143
+ }>]>>;
144
+ }>, Schema.Struct<{
145
+ readonly viewId: Schema.String;
146
+ readonly title: Schema.String;
147
+ readonly state: Schema.Literal<"full">;
148
+ readonly rows: Schema.$Array<Schema.Struct<{
149
+ readonly id: Schema.String;
150
+ readonly title: Schema.String;
151
+ readonly detail: Schema.optional<Schema.String>;
152
+ readonly tone: Schema.optional<Schema.Literals<readonly ["attention", "running", "error", "settled", "muted"]>>;
153
+ readonly status: Schema.optional<Schema.Struct<{
154
+ readonly label: Schema.String;
155
+ readonly tone: Schema.Literals<readonly ["neutral", "primary", "secondary", "accent", "success", "info", "warning", "error"]>;
156
+ }>>;
157
+ readonly open: Schema.optional<Schema.Struct<{
158
+ readonly params: Schema.$Array<Schema.Struct<{
159
+ readonly key: Schema.String;
160
+ readonly value: Schema.String;
161
+ }>>;
162
+ }>>;
163
+ }>>;
164
+ readonly next: Schema.optional<Schema.String>;
165
+ }>]>, Schema.Struct<{
166
+ readonly options: Schema.$Array<Schema.Struct<{
167
+ readonly value: Schema.String;
168
+ readonly label: Schema.String;
169
+ readonly detail: Schema.optional<Schema.String>;
170
+ }>>;
171
+ readonly next: Schema.optional<Schema.String>;
172
+ }>]>;
173
+ /** A rows read or a placement-options read. */
174
+ export type GillReply = Schema.Schema.Type<typeof gillReplySchema>;
175
+ /** Decode the selected host's bounded reply before rendering or navigation. */
176
+ export declare const decodeGillReply: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
177
+ readonly state: "empty";
178
+ readonly title: string;
179
+ readonly viewId: string;
180
+ readonly rows: readonly [];
181
+ readonly next?: string | undefined;
182
+ } | {
183
+ readonly error: string;
184
+ readonly state: "error";
185
+ readonly title: string;
186
+ readonly viewId: string;
187
+ readonly rows: readonly [];
188
+ readonly reason?: "exec_isolation_required" | "params_required" | undefined;
189
+ readonly recovery?: {
190
+ readonly kind: "configure";
191
+ readonly param: string;
192
+ } | {
193
+ readonly kind: "open";
194
+ } | undefined;
195
+ } | {
196
+ readonly state: "full";
197
+ readonly title: string;
198
+ readonly viewId: string;
199
+ readonly rows: readonly {
200
+ readonly id: string;
201
+ readonly title: string;
202
+ readonly detail?: string | undefined;
203
+ readonly tone?: "error" | "running" | "attention" | "settled" | "muted" | undefined;
204
+ readonly open?: {
205
+ readonly params: readonly {
206
+ readonly key: string;
207
+ readonly value: string;
208
+ }[];
209
+ } | undefined;
210
+ readonly status?: {
211
+ readonly label: string;
212
+ readonly tone: "accent" | "error" | "success" | "neutral" | "primary" | "secondary" | "info" | "warning";
213
+ } | undefined;
214
+ }[];
215
+ readonly next?: string | undefined;
216
+ } | {
217
+ readonly options: readonly {
218
+ readonly value: string;
219
+ readonly label: string;
220
+ readonly detail?: string | undefined;
221
+ }[];
222
+ readonly next?: string | undefined;
223
+ };
224
+ /** Decode an options method's result without trusting extra fields. */
225
+ export declare const decodeGillOptionsResponse: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
226
+ readonly options: readonly {
227
+ readonly value: string;
228
+ readonly label: string;
229
+ readonly detail?: string | undefined;
230
+ }[];
231
+ readonly next?: string | undefined;
232
+ };
233
+ /** App output must match its digest-covered view identity. */
234
+ export declare function decodeDeclaredGillResponse(response: GillResponse, declaration: Pick<AppDeclaredGill, "id" | "params">): GillResponse;
235
+ /** Authenticated host Fetch stays outside App code and is pinned to one selection. */
236
+ export declare function createGillClient(options: Readonly<{
237
+ /** Decode a host's HTTP 503 body into its Gill refusal, if recognized. */
238
+ decodeRefusal?: (raw: Schema.Json, declaration: Readonly<{
239
+ id: string;
240
+ title: string;
241
+ }>) => GillResponse | undefined;
242
+ endpoint: string;
243
+ fetch: typeof globalThis.fetch;
244
+ declaration: Pick<AppDeclaredGill, "id" | "title" | "params">;
245
+ }>): {
246
+ /** Fetch one decoded page using this client's pinned declaration. */
247
+ request(selectors: GillRequest, signal?: AbortSignal): Promise<GillReply>;
248
+ };
package/dist/gill.js ADDED
@@ -0,0 +1,134 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { appGillParamKeySchema } from "./declaration.js";
3
+ import { rowLabelSchemaV1 as label, rowIdentitySchemaV1 as identity, rowPageSchemaV1 as rows, } from "./rows.js";
4
+ /** Placement values are bounded; the admitting host checks declared keys. */
5
+ export const gillParamsSchema = Schema.Record(appGillParamKeySchema, Schema.String.check(Schema.isMaxLength(256))).check(Schema.makeFilter((params) => Object.keys(params).length <= 4, {
6
+ message: "Gill placement supports at most four parameters",
7
+ }));
8
+ /** The caller supplies placement context and a source-owned continuation. */
9
+ export const gillRequestSchema = Schema.Struct({
10
+ viewId: identity,
11
+ params: Schema.optional(gillParamsSchema),
12
+ after: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
13
+ options: Schema.optional(appGillParamKeySchema),
14
+ });
15
+ /** Runtime call selected by the host after admitting a declared source. */
16
+ export const gillCallSchema = Schema.Struct({
17
+ id: identity,
18
+ params: Schema.optional(gillParamsSchema),
19
+ after: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
20
+ options: Schema.optional(appGillParamKeySchema),
21
+ });
22
+ /** Shared response codec. Rows are plain text and cannot carry executable UI. */
23
+ export const gillResponseSchema = Schema.Union([
24
+ Schema.Struct({
25
+ viewId: identity,
26
+ title: label,
27
+ state: Schema.Literal("empty"),
28
+ rows: Schema.Tuple([]),
29
+ next: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMaxLength(1_024)))),
30
+ }),
31
+ Schema.Struct({
32
+ viewId: identity,
33
+ title: label,
34
+ state: Schema.Literal("error"),
35
+ rows: Schema.Tuple([]),
36
+ /** Host-owned permanent execution refusal; an ordinary error may still be refreshed. */
37
+ reason: Schema.optional(Schema.Literals(["exec_isolation_required", "params_required"])),
38
+ error: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(512))),
39
+ /** The host supplies the same-placement picker and this App's launch. */
40
+ recovery: Schema.optional(Schema.Union([
41
+ Schema.Struct({
42
+ kind: Schema.Literal("configure"),
43
+ param: appGillParamKeySchema,
44
+ }),
45
+ Schema.Struct({ kind: Schema.Literal("open") }),
46
+ ])),
47
+ }),
48
+ Schema.Struct({
49
+ viewId: identity,
50
+ title: label,
51
+ state: Schema.Literal("full"),
52
+ rows: rows.pipe(Schema.check(Schema.isMinLength(1))),
53
+ next: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMaxLength(1_024)))),
54
+ }),
55
+ ]);
56
+ /** Decode backend output once at the App host boundary. */
57
+ export const decodeGillResponse = Schema.decodeUnknownSync(gillResponseSchema, {
58
+ onExcessProperty: "error",
59
+ });
60
+ /** A bounded choice page returned by an App-owned options read method. */
61
+ export const gillOptionsResponseSchema = Schema.Struct({
62
+ options: Schema.Array(Schema.Struct({
63
+ value: Schema.String.check(Schema.isMaxLength(256)),
64
+ label,
65
+ detail: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
66
+ })).check(Schema.isMaxLength(200)),
67
+ next: Schema.optional(Schema.String.check(Schema.isMaxLength(1_024))),
68
+ });
69
+ /** Shared wire for a rows read or a placement-options read. */
70
+ export const gillReplySchema = Schema.Union([
71
+ gillResponseSchema,
72
+ gillOptionsResponseSchema,
73
+ ]);
74
+ /** Decode the selected host's bounded reply before rendering or navigation. */
75
+ export const decodeGillReply = Schema.decodeUnknownSync(gillReplySchema, {
76
+ onExcessProperty: "error",
77
+ });
78
+ /** Decode an options method's result without trusting extra fields. */
79
+ export const decodeGillOptionsResponse = Schema.decodeUnknownSync(gillOptionsResponseSchema, { onExcessProperty: "error" });
80
+ /** App output must match its digest-covered view identity. */
81
+ export function decodeDeclaredGillResponse(response, declaration) {
82
+ if (response.viewId !== declaration.id)
83
+ throw new Error("Gill response does not match its Release declaration");
84
+ const recovery = response.state === "error" ? response.recovery : undefined;
85
+ if (recovery?.kind === "configure" &&
86
+ !declaration.params?.some((param) => param.key === recovery.param))
87
+ throw new Error("Gill recovery parameter is not declared");
88
+ return response;
89
+ }
90
+ /** Authenticated host Fetch stays outside App code and is pinned to one selection. */
91
+ export function createGillClient(options) {
92
+ return {
93
+ /** Fetch one decoded page using this client's pinned declaration. */
94
+ async request(selectors, signal) {
95
+ if (selectors.viewId !== options.declaration.id)
96
+ throw new Error("Gill request does not match its Release declaration");
97
+ const response = await options.fetch(options.endpoint, {
98
+ method: "PUT",
99
+ redirect: "error",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify(Schema.encodeSync(gillRequestSchema)(selectors)),
102
+ signal: signal ?? null,
103
+ });
104
+ if (!response.ok) {
105
+ if (response.status === 503) {
106
+ let raw;
107
+ try {
108
+ raw = Schema.decodeUnknownSync(Schema.Json)(await response.json());
109
+ }
110
+ catch (cause) {
111
+ throw new Error("Gill request refused with HTTP 503", { cause });
112
+ }
113
+ const refusal = options.decodeRefusal?.(raw, options.declaration);
114
+ if (refusal !== undefined)
115
+ return refusal;
116
+ }
117
+ else {
118
+ await response.body?.cancel();
119
+ }
120
+ throw new Error(`Gill request refused with HTTP ${String(response.status)}`);
121
+ }
122
+ const raw = await response.json();
123
+ const reply = decodeGillReply(raw);
124
+ if ("options" in reply) {
125
+ if (selectors.options === undefined)
126
+ throw new Error("Gill returned choices for a row read");
127
+ return reply;
128
+ }
129
+ if (selectors.options !== undefined && reply.state !== "error")
130
+ throw new Error("Gill returned rows for an options read");
131
+ return decodeDeclaredGillResponse(reply, options.declaration);
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./rows.js";
2
+ export * from "./gill.js";
3
+ export * from "./declaration.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./rows.js";
2
+ export * from "./gill.js";
3
+ export * from "./declaration.js";
@@ -0,0 +1,2 @@
1
+ /** Render App timestamps using Bonsai's existing relative-time presentation. */
2
+ export declare function relativeTime(at: number, now?: number): string;
@@ -0,0 +1,20 @@
1
+ const units = [
2
+ ["year", 31_536_000_000],
3
+ ["month", 2_592_000_000],
4
+ ["week", 604_800_000],
5
+ ["day", 86_400_000],
6
+ ["hour", 3_600_000],
7
+ ["minute", 60_000],
8
+ ];
9
+ const relative = new Intl.RelativeTimeFormat("en", {
10
+ numeric: "auto",
11
+ style: "narrow",
12
+ });
13
+ /** Render App timestamps using Bonsai's existing relative-time presentation. */
14
+ export function relativeTime(at, now = Date.now()) {
15
+ const delta = at - now;
16
+ for (const [unit, size] of units)
17
+ if (Math.abs(delta) >= size)
18
+ return relative.format(Math.round(delta / size), unit);
19
+ return "just now";
20
+ }