@acid-cat/spec-ts 0.0.0-stage → 0.1.0

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 Hebilicious
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,20 @@
1
- # Temporary Holding Version
1
+ # @acid-cat/spec-ts
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
+ The fixed details of the [AcidCat specification](../../spec/README.md), as TypeScript data and pure functions. npm package: `@acid-cat/spec-ts`. It has no dependencies.
4
+
5
+ This package is part of the TypeScript reference implementation. [`@acid-cat/core-effect`](../core-effect) and [`@acid-cat/core-ts`](../core-ts) use it, so that both cores agree on each detail. An implementation in another language does not use it.
6
+
7
+ ```sh
8
+ pnpm add @acid-cat/spec-ts
9
+ ```
10
+
11
+ ## Contents
12
+
13
+ | Export | Use |
14
+ | --- | --- |
15
+ | `ok`, `fail`, `callFailure`, `isCallFailure` | Make and test [outcomes](../../spec/invocation.md#outcomes) |
16
+ | `CALL_FAILURE_KINDS`, `CallFailureError` | The kinds of call failure, and the JavaScript error for one |
17
+ | `AVAILABLE`, `unavailable` | Make [availability](../../spec/invocation.md#availability) values |
18
+ | `viewOf`, `bundle` | The [view](../../spec/catalog.md#views) of a target, and a schema with its shared types |
19
+
20
+ The other exports give the names, flags, exit codes and tool shapes of each [binding](../../spec/bindings/README.md).
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The fixed details of the AcidCat specification, as TypeScript data and pure functions, for the TypeScript reference
3
+ * implementation. This package has no dependencies.
4
+ *
5
+ * - Outcomes and call failures: spec/invocation.md
6
+ * - Names, views and bundling: spec/catalog.md
7
+ * - Exit codes, status codes and MCP results: spec/bindings/
8
+ */
9
+ /** The kinds of call failure, in order of precedence: when more than one applies, the first one is used. */
10
+ export declare const CALL_FAILURE_KINDS: readonly ["cancelled", "unknown-capability", "unavailable", "invalid-input", "internal"];
11
+ export type CallFailureKind = (typeof CALL_FAILURE_KINDS)[number];
12
+ /** A call that did not give a result. */
13
+ export interface CallFailure {
14
+ readonly kind: CallFailureKind;
15
+ readonly message: string;
16
+ }
17
+ /** A value or a failure. */
18
+ export type Result<Value, Failure> = {
19
+ readonly ok: true;
20
+ readonly value: Value;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly error: Failure;
24
+ };
25
+ /** The end of a call: a value, a failure or a call failure. */
26
+ export type Outcome<Value, Failure> = Result<Value, Failure> | CallFailure;
27
+ export declare const ok: <Value>(value: Value) => Result<Value, never>;
28
+ /** A failure with its tag, as a result. */
29
+ export declare const fail: <Tag extends string, Data extends object = {}>(tag: Tag, data?: Data) => Result<never, {
30
+ readonly _tag: Tag;
31
+ } & Data>;
32
+ export declare const callFailure: (kind: CallFailureKind, message: string) => CallFailure;
33
+ export declare const isCallFailure: (outcome: Outcome<unknown, unknown>) => outcome is CallFailure;
34
+ /** The JavaScript error for a call failure (spec/bindings/typescript.md#handles). */
35
+ export declare class CallFailureError extends Error {
36
+ readonly kind: CallFailureKind;
37
+ constructor(failure: CallFailure);
38
+ }
39
+ export type Availability = {
40
+ readonly available: true;
41
+ } | {
42
+ readonly available: false;
43
+ readonly reason: string;
44
+ };
45
+ export declare const AVAILABLE: Availability;
46
+ export declare const unavailable: (reason: string) => Availability;
47
+ export declare const ID_PATTERN: RegExp;
48
+ export declare const isId: (id: string) => boolean;
49
+ /** The path of the CLI command for an id: `aircon.power.set` → `["aircon", "power", "set"]`. */
50
+ export declare const commandPath: (id: string) => readonly string[];
51
+ export declare const CLI_EXIT_CODES: {
52
+ readonly value: 0;
53
+ readonly failure: 1;
54
+ readonly "unknown-capability": 2;
55
+ readonly "invalid-input": 3;
56
+ readonly unavailable: 4;
57
+ readonly internal: 5;
58
+ readonly cancelled: 130;
59
+ };
60
+ export declare const cliExitCode: (outcome: Outcome<unknown, unknown>) => number;
61
+ export declare const HTTP_STATUS: {
62
+ readonly "unknown-capability": 404;
63
+ readonly "invalid-input": 422;
64
+ readonly unavailable: 503;
65
+ readonly cancelled: 409;
66
+ readonly internal: 500;
67
+ };
68
+ export declare const httpStatus: (outcome: Outcome<unknown, unknown>) => number;
69
+ export type Rejection = "method" | "origin" | "media-type" | "too-large" | "not-utf8";
70
+ /**
71
+ * The first rule that a request breaks, before its body is read: method, origin, media type, declared length.
72
+ * The body itself is checked as it arrives, with `readBounded`.
73
+ */
74
+ export declare function screenRequest(request: Request, rules: {
75
+ readonly methods: readonly string[];
76
+ readonly allowedOrigins: readonly string[];
77
+ readonly mediaType: string;
78
+ readonly maximumBytes: number;
79
+ }): Rejection | undefined;
80
+ /** Read a request body as UTF-8 text. Count the bytes as they arrive, and stop at the limit. */
81
+ export declare function readBounded(request: Request, maximumBytes: number): Promise<{
82
+ readonly text: string;
83
+ } | {
84
+ readonly rejection: Rejection;
85
+ }>;
86
+ export declare const MCP_PROTOCOL_VERSION = "2026-07-28";
87
+ export declare const MCP_UNKNOWN_TOOL = -32602;
88
+ /** The HTTP status and JSON-RPC error code for each rejected MCP request. */
89
+ export declare const MCP_REJECTIONS: {
90
+ readonly [R in Rejection]: {
91
+ readonly status: number;
92
+ readonly code?: number;
93
+ };
94
+ };
95
+ export interface McpToolResult {
96
+ readonly content: readonly [{
97
+ readonly type: "text";
98
+ readonly text: string;
99
+ }];
100
+ readonly isError?: true;
101
+ readonly structuredContent?: unknown;
102
+ }
103
+ /** The MCP tool result for an outcome that is not `unknown-capability`. */
104
+ export declare function mcpToolResult(outcome: Outcome<unknown, unknown>, hasOutputSchema: boolean): McpToolResult;
105
+ export type JsonObject = {
106
+ readonly [key: string]: unknown;
107
+ };
108
+ export interface CatalogCapability extends JsonObject {
109
+ readonly description: string;
110
+ readonly audience?: readonly string[];
111
+ readonly input?: JsonObject;
112
+ readonly output?: JsonObject;
113
+ readonly failures?: {
114
+ readonly [tag: string]: JsonObject;
115
+ };
116
+ readonly readOnly?: boolean;
117
+ readonly state?: JsonObject;
118
+ }
119
+ export interface CatalogDocument extends JsonObject {
120
+ readonly acidcat: string;
121
+ readonly title?: string;
122
+ readonly description?: string;
123
+ readonly targets: {
124
+ readonly [name: string]: JsonObject & {
125
+ readonly bindings: readonly string[];
126
+ };
127
+ };
128
+ readonly $defs?: {
129
+ readonly [name: string]: JsonObject;
130
+ };
131
+ readonly capabilities: {
132
+ readonly [id: string]: CatalogCapability;
133
+ };
134
+ }
135
+ export declare const REFERENCE: RegExp;
136
+ /** The schemas of a capability, each with the member that holds it. */
137
+ export declare function schemasOf(capability: CatalogCapability): readonly (readonly [string, JsonObject])[];
138
+ /** Every object in a JSON value, at all levels. */
139
+ export declare function visitObjects(value: unknown, onObject: (node: JsonObject) => void): void;
140
+ /** The types that a schema uses, directly or through other types. */
141
+ export declare function typesUsedBy(schema: unknown, types: {
142
+ readonly [name: string]: JsonObject;
143
+ }): Map<string, JsonObject>;
144
+ /** Bundle a schema (spec/catalog.md#bundling). */
145
+ export declare function bundle(schema: JsonObject, types: {
146
+ readonly [name: string]: JsonObject;
147
+ }): JsonObject;
148
+ /** The view of a target (spec/catalog.md#views). */
149
+ export declare function viewOf(catalog: CatalogDocument, target: string): CatalogDocument;
package/dist/index.js ADDED
@@ -0,0 +1,174 @@
1
+ // src/index.ts
2
+ var CALL_FAILURE_KINDS = ["cancelled", "unknown-capability", "unavailable", "invalid-input", "internal"];
3
+ var ok = (value) => ({ ok: true, value });
4
+ var fail = (tag, data) => ({ ok: false, error: { _tag: tag, ...data ?? {} } });
5
+ var callFailure = (kind, message) => ({ kind, message });
6
+ var isCallFailure = (outcome) => !("ok" in outcome);
7
+ var CallFailureError = class extends Error {
8
+ kind;
9
+ constructor(failure) {
10
+ super(failure.message);
11
+ this.kind = failure.kind;
12
+ this.name = failure.kind === "cancelled" ? "AbortError" : failure.kind === "invalid-input" ? "TypeError" : "Error";
13
+ }
14
+ };
15
+ var AVAILABLE = { available: true };
16
+ var unavailable = (reason) => ({ available: false, reason });
17
+ var ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)+$/;
18
+ var isId = (id) => id.length <= 128 && ID_PATTERN.test(id);
19
+ var commandPath = (id) => id.split(".");
20
+ var CLI_EXIT_CODES = {
21
+ value: 0,
22
+ failure: 1,
23
+ "unknown-capability": 2,
24
+ "invalid-input": 3,
25
+ unavailable: 4,
26
+ internal: 5,
27
+ cancelled: 130
28
+ };
29
+ var cliExitCode = (outcome) => isCallFailure(outcome) ? CLI_EXIT_CODES[outcome.kind] : outcome.ok ? CLI_EXIT_CODES.value : CLI_EXIT_CODES.failure;
30
+ var HTTP_STATUS = {
31
+ "unknown-capability": 404,
32
+ "invalid-input": 422,
33
+ unavailable: 503,
34
+ cancelled: 409,
35
+ internal: 500
36
+ };
37
+ var httpStatus = (outcome) => isCallFailure(outcome) ? HTTP_STATUS[outcome.kind] : 200;
38
+ function screenRequest(request, rules) {
39
+ if (!rules.methods.includes(request.method)) return "method";
40
+ const origin = request.headers.get("Origin");
41
+ if (origin !== null && !rules.allowedOrigins.includes(origin)) return "origin";
42
+ if (request.method === "GET" || request.method === "HEAD") return void 0;
43
+ const mediaType = (request.headers.get("Content-Type") ?? "").split(";")[0].trim().toLowerCase();
44
+ if (mediaType !== rules.mediaType) return "media-type";
45
+ const declared = Number(request.headers.get("Content-Length") ?? "");
46
+ if (Number.isFinite(declared) && declared > rules.maximumBytes) return "too-large";
47
+ return void 0;
48
+ }
49
+ async function readBounded(request, maximumBytes) {
50
+ if (request.body === null) return { text: "" };
51
+ const reader = request.body.getReader();
52
+ const chunks = [];
53
+ let length = 0;
54
+ for (; ; ) {
55
+ const { done, value } = await reader.read();
56
+ if (done) break;
57
+ length += value.byteLength;
58
+ if (length > maximumBytes) {
59
+ await reader.cancel().catch(() => void 0);
60
+ return { rejection: "too-large" };
61
+ }
62
+ chunks.push(value);
63
+ }
64
+ const bytes = new Uint8Array(length);
65
+ let offset = 0;
66
+ for (const chunk of chunks) {
67
+ bytes.set(chunk, offset);
68
+ offset += chunk.byteLength;
69
+ }
70
+ try {
71
+ return { text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
72
+ } catch {
73
+ return { rejection: "not-utf8" };
74
+ }
75
+ }
76
+ var MCP_PROTOCOL_VERSION = "2026-07-28";
77
+ var MCP_UNKNOWN_TOOL = -32602;
78
+ var MCP_REJECTIONS = {
79
+ method: { status: 405 },
80
+ origin: { status: 403 },
81
+ "media-type": { status: 415 },
82
+ "too-large": { status: 413, code: -32600 },
83
+ "not-utf8": { status: 400, code: -32700 }
84
+ };
85
+ function mcpToolResult(outcome, hasOutputSchema) {
86
+ const text = (value) => [{ type: "text", text: JSON.stringify(value) }];
87
+ if (isCallFailure(outcome)) return { content: text({ kind: outcome.kind, message: outcome.message }), isError: true };
88
+ if (!outcome.ok) return { content: text({ ok: false, error: outcome.error }), isError: true };
89
+ return hasOutputSchema ? { content: text(outcome.value), structuredContent: outcome.value } : { content: text(outcome.value) };
90
+ }
91
+ var REFERENCE = /^#\/\$defs\/([A-Za-z][A-Za-z0-9_]*)$/;
92
+ function schemasOf(capability) {
93
+ const members = [
94
+ ["input", capability.input],
95
+ ["output", capability.output],
96
+ ["state", capability.state],
97
+ ...Object.entries(capability.failures ?? {}).map(([tag, schema]) => [`failure ${tag}`, schema])
98
+ ];
99
+ return members.filter((entry) => entry[1] !== void 0);
100
+ }
101
+ function visitObjects(value, onObject) {
102
+ if (Array.isArray(value)) {
103
+ value.forEach((item) => visitObjects(item, onObject));
104
+ } else if (value !== null && typeof value === "object") {
105
+ onObject(value);
106
+ Object.values(value).forEach((item) => visitObjects(item, onObject));
107
+ }
108
+ }
109
+ function typesUsedBy(schema, types) {
110
+ const used = /* @__PURE__ */ new Map();
111
+ const collect = (node) => visitObjects(node, (inner) => {
112
+ const name = typeof inner.$ref === "string" ? REFERENCE.exec(inner.$ref)?.[1] : void 0;
113
+ const type = name === void 0 ? void 0 : types[name];
114
+ if (name !== void 0 && type !== void 0 && !used.has(name)) {
115
+ used.set(name, type);
116
+ collect(type);
117
+ }
118
+ });
119
+ collect(schema);
120
+ return used;
121
+ }
122
+ function bundle(schema, types) {
123
+ const only = Object.keys(schema).length === 1 ? REFERENCE.exec(String(schema.$ref))?.[1] : void 0;
124
+ const base = only === void 0 ? schema : types[only] ?? schema;
125
+ const used = typesUsedBy(base, types);
126
+ return used.size === 0 ? base : { ...base, $defs: Object.fromEntries(used) };
127
+ }
128
+ function viewOf(catalog, target) {
129
+ const types = catalog.$defs ?? {};
130
+ const capabilities = Object.fromEntries(
131
+ Object.entries(catalog.capabilities).filter(([, capability]) => (capability.audience ?? []).includes(target)).map(([id, capability]) => [id, { ...capability, audience: [target] }])
132
+ );
133
+ const used = /* @__PURE__ */ new Map();
134
+ for (const capability of Object.values(capabilities)) {
135
+ for (const [, schema] of schemasOf(capability)) for (const [name, type] of typesUsedBy(schema, types)) used.set(name, type);
136
+ }
137
+ const { $defs: _, ...rest } = catalog;
138
+ const declared = catalog.targets[target];
139
+ return {
140
+ ...rest,
141
+ targets: declared === void 0 ? {} : { [target]: declared },
142
+ ...used.size > 0 ? { $defs: Object.fromEntries(used) } : {},
143
+ capabilities
144
+ };
145
+ }
146
+ export {
147
+ AVAILABLE,
148
+ CALL_FAILURE_KINDS,
149
+ CLI_EXIT_CODES,
150
+ CallFailureError,
151
+ HTTP_STATUS,
152
+ ID_PATTERN,
153
+ MCP_PROTOCOL_VERSION,
154
+ MCP_REJECTIONS,
155
+ MCP_UNKNOWN_TOOL,
156
+ REFERENCE,
157
+ bundle,
158
+ callFailure,
159
+ cliExitCode,
160
+ commandPath,
161
+ fail,
162
+ httpStatus,
163
+ isCallFailure,
164
+ isId,
165
+ mcpToolResult,
166
+ ok,
167
+ readBounded,
168
+ schemasOf,
169
+ screenRequest,
170
+ typesUsedBy,
171
+ unavailable,
172
+ viewOf,
173
+ visitObjects
174
+ };
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@acid-cat/spec-ts",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The fixed details of the AcidCat specification as TypeScript data and pure functions, with no dependencies. Part of the TypeScript reference implementation.",
5
+ "keywords": [
6
+ "acidcat",
7
+ "specification"
8
+ ],
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Hebilicious/acidcat.git",
13
+ "directory": "packages/spec-ts"
14
+ },
15
+ "homepage": "https://github.com/Hebilicious/acidcat/tree/main/packages/spec-ts#readme",
16
+ "bugs": "https://github.com/Hebilicious/acidcat/issues",
17
+ "type": "module",
18
+ "engines": {
19
+ "node": ">=24"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "26.6.5",
35
+ "typescript": "7.0.2"
36
+ }
6
37
  }