@unispechq/unispec-core 0.1.0 → 0.1.2

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/.windsurfrules DELETED
@@ -1,138 +0,0 @@
1
- # WindSurf Rules for `unispec-core`
2
- # ---------------------------------
3
- # This ruleset defines how AI assistants, contributors, and reviewers must
4
- # interact with the UniSpec Core Engine repository.
5
- # The Core Engine provides validation, parsing, normalization, diffing,
6
- # and conversion logic for UniSpec specifications.
7
-
8
- repository:
9
- name: "unispec-core"
10
- description: "Runtime implementation of the UniSpec Core Engine (loader, validator, normalizer, diff engine, converters, shared types). This repository contains only the Core Engine used by other UniSpec platform components (CLI, Registry, Portal, adapters, SDKs)."
11
- visibility: public
12
- criticality: core
13
-
14
- goals:
15
- - Implement the runtime mechanics of the UniSpec format defined in `unispec-spec`.
16
- - Provide parsing, validation, normalization, and diffing for UniSpec documents.
17
- - Provide reusable shared types and utilities for CLI, adapters, registry, and portal.
18
- - Ensure strict alignment with the specification and JSON Schemas.
19
- - Consume organization-wide context through MCP GitHub server without violating boundaries.
20
- - Remain lightweight, stable, deterministic, and fully spec-compliant.
21
-
22
- assistant_instructions:
23
- allowed_actions:
24
- - Read and reference content from other UniSpec repositories via MCP GitHub.
25
- - Generate and update logic within this repository related to:
26
- - parsing UniSpec documents,
27
- - JSON Schema validation,
28
- - normalization,
29
- - diffing,
30
- - conversions (OpenAPI, GraphQL SDL, WebSocket models),
31
- - shared types and interfaces.
32
- - Suggest architectural improvements to core modules.
33
- - Assist in writing documentation, API descriptions, comments, and design notes.
34
- - Validate behavior using actual spec files from `unispec-spec`.
35
-
36
- forbidden_actions:
37
- - Modify any files in external repositories.
38
- - Introduce changes to the UniSpec language itself (belongs to `unispec-spec`).
39
- - Implement features that are not grounded in the official spec.
40
- - Add framework-specific or platform-specific logic (belongs to adapters).
41
- - Add server code, API endpoints, frontend components, or CLI commands.
42
- - Introduce non-deterministic behavior or global mutable state.
43
- - Modify specification version numbers (belongs to `unispec-spec`).
44
-
45
- escalation_policy:
46
- - Any behavior that deviates from the official UniSpec spec must require maintainer confirmation.
47
- - If MCP data reveals discrepancies between spec and code, assistant must warn and suggest alignment.
48
- - For potentially breaking API changes, assistant must halt and request maintainer approval.
49
-
50
- context_model:
51
- mcp_github_usage:
52
- allowed:
53
- - Access organization repositories for context (read-only).
54
- - Inspect:
55
- - UniSpec format (`unispec-spec`)
56
- - CLI behavior (`unispec-platform`)
57
- - Adapters (`unispec-js-adapters`, `unispec-python-adapters`)
58
- - Registry and Portal APIs
59
- - Use cross-repo information to ensure Core Engine behavior matches platform expectations.
60
- forbidden:
61
- - Editing or mutating files in other repositories.
62
- - Making assumptions not supported by spec.
63
- - Introducing coupling that makes core depend on implementation details.
64
- behaviors:
65
- - Treat external repos purely as reference sources.
66
- - Maintain strict alignment with the spec repo.
67
- - Do not derive new behaviors not defined by the spec.
68
-
69
- file_structure:
70
- required_directories:
71
- src: "Source code for the Core Engine logic"
72
- src/loader: "Spec loader and YAML/JSON reading utilities"
73
- src/validator: "Schema validation logic using official UniSpec JSON Schemas"
74
- src/normalizer: "Canonical normalization of UniSpec documents"
75
- src/diff: "Diff engine and breaking change detection"
76
- src/converters: "Converters for OpenAPI, GraphQL, WebSocket, etc."
77
- src/types: "Shared public-facing TypeScript types/interfaces"
78
- docs: "Developer documentation and design notes"
79
-
80
- required_files:
81
- - "README.md"
82
- - "package.json"
83
- - "src/index.ts"
84
- - "src/types/index.ts"
85
-
86
- content_guidelines:
87
- code_requirements:
88
- - Must be deterministic, pure, and side-effect-free where possible.
89
- - Must strictly follow the UniSpec JSON Schema definitions.
90
- - Must use official schemas from `unispec-spec` (via MCP reference or local import).
91
- - Should be modular, composable, and testable.
92
- - Must not depend on frameworks (Express, Nest, FastAPI, etc).
93
- - Must not include platform logic (Registry/Portal).
94
-
95
- documentation:
96
- - All modules must have clear API descriptions and rationale.
97
- - Example inputs and outputs should reference real UniSpec examples.
98
- - Must reflect the latest UniSpec specification.
99
-
100
- testing:
101
- - Core behavior must be validated using examples from `unispec-spec/examples`.
102
- - All changes must include tests for validation, normalization, diffing, and conversions.
103
- - Tests must cover edge cases and backward compatibility.
104
-
105
- versioning_policy:
106
- rules:
107
- - Core versioning follows the platform, not the spec.
108
- - Breaking API changes require:
109
- - Maintainer approval
110
- - Minor versions may add new converters or metadata fields if spec permits.
111
- - Patch versions fix issues, improve accuracy, or correct types.
112
-
113
- pull_request_rules:
114
- - PRs must describe:
115
- - intent,
116
- - expected behavior,
117
- - impact on users,
118
- - compatibility implications.
119
- - PRs affecting validation, types, or normalization must include:
120
- - tests,
121
- - updated docs,
122
- - verification against real examples.
123
- - Schema-dependent logic must be checked against `unispec-spec` via MCP.
124
-
125
- tone_and_style:
126
- - Code and docs must be clean, consistent, and professional.
127
- - Naming conventions must follow spec terminology.
128
- - No noisy logs, no temporary/debug code.
129
-
130
- license:
131
- - Must remain open-source.
132
- - Core engine logic must be available for public use.
133
-
134
- notes:
135
- - This repository implements the *mechanics* of UniSpec, not the language definition.
136
- - The UniSpec format itself lives in `unispec-spec`.
137
- - Adapters, CLI, Registry, and Portal depend on core — keep APIs stable and predictable.
138
- - MCP GitHub context may be used for reasoning but not for cross-repo modifications.
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { execSync } from 'node:child_process';
4
- import { readFileSync, writeFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
-
7
- const bumpType = process.argv[2];
8
-
9
- if (!['patch', 'minor', 'major'].includes(bumpType)) {
10
- console.error('Usage: npm run release:<patch|minor|major>');
11
- process.exit(1);
12
- }
13
-
14
- const root = process.cwd();
15
- const pkgPath = join(root, 'package.json');
16
-
17
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
18
-
19
- function bumpVersion(version, type) {
20
- const [major, minor, patch] = version.split('.').map(Number);
21
-
22
- if (type === 'patch') return `${major}.${minor}.${patch + 1}`;
23
- if (type === 'minor') return `${major}.${minor + 1}.0`;
24
- if (type === 'major') return `${major + 1}.0.0`;
25
-
26
- throw new Error(`Unsupported bump type: ${type}`);
27
- }
28
-
29
- const oldVersion = pkg.version;
30
- if (!oldVersion) {
31
- console.error('package.json has no version field');
32
- process.exit(1);
33
- }
34
-
35
- const newVersion = bumpVersion(oldVersion, bumpType);
36
- pkg.version = newVersion;
37
-
38
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
39
-
40
- const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
41
-
42
- execSync('git add package.json', { stdio: 'inherit' });
43
- execSync(`git commit -m "chore: release unispec-core v${newVersion}"`, { stdio: 'inherit' });
44
-
45
- const tagName = `unispec-core-v${newVersion}`;
46
- execSync(`git tag ${tagName}`, { stdio: 'inherit' });
47
-
48
- execSync(`git push origin ${currentBranch}`, { stdio: 'inherit' });
49
- execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
50
-
51
- console.log(`\nPushed ${currentBranch} and tag ${tagName} to origin.`);
@@ -1,120 +0,0 @@
1
- import { UniSpecDocument, UniSpecWebSocketProtocol, UniSpecWebSocketChannel } from "../types";
2
-
3
- export interface OpenAPIDocument {
4
- [key: string]: unknown;
5
- }
6
-
7
- export interface GraphQLSDLOutput {
8
- sdl: string;
9
- }
10
-
11
- export interface WebSocketModel {
12
- [key: string]: unknown;
13
- }
14
-
15
- export function toOpenAPI(doc: UniSpecDocument): OpenAPIDocument {
16
- const service = doc.service;
17
- const rest = service.protocols?.rest as any | undefined;
18
-
19
- const info = {
20
- title: service.title ?? service.name,
21
- description: service.description,
22
- };
23
-
24
- const servers = rest?.servers ?? [];
25
- const paths = rest?.paths ?? {};
26
-
27
- // Transparently forward additional REST protocol fields into the OpenAPI document.
28
- // This allows users to describe components, security, tags and other structures
29
- // without forcing a specific REST model at the core layer.
30
- const { servers: _omitServers, paths: _omitPaths, ...restExtras } = rest ?? {};
31
-
32
- return {
33
- openapi: "3.1.0",
34
- info,
35
- servers,
36
- paths,
37
- ...restExtras,
38
- "x-unispec": doc,
39
- };
40
- }
41
-
42
- export function toGraphQLSDL(doc: UniSpecDocument): GraphQLSDLOutput {
43
- // Minimal implementation: generate a basic SDL that exposes service metadata
44
- // via a Query field. This does not attempt to interpret the full GraphQL
45
- // protocol structure yet, but provides a stable, deterministic SDL shape
46
- // based on top-level UniSpec document fields.
47
-
48
- const graphql = doc.service.protocols?.graphql;
49
- const customSDL = graphql?.schema?.sdl;
50
-
51
- if (typeof customSDL === "string" && customSDL.trim()) {
52
- return { sdl: customSDL };
53
- }
54
-
55
- const service = doc.service;
56
- const title = service.title ?? service.name;
57
- const description = service.description ?? "";
58
-
59
- const lines: string[] = [];
60
-
61
- if (title || description) {
62
- lines.push("\"\"");
63
- if (title) {
64
- lines.push(title);
65
- }
66
- if (description) {
67
- lines.push("");
68
- lines.push(description);
69
- }
70
- lines.push("\"\"");
71
- }
72
-
73
- lines.push("schema {");
74
- lines.push(" query: Query");
75
- lines.push("}");
76
- lines.push("");
77
- lines.push("type Query {");
78
- lines.push(" _serviceInfo: String!\n");
79
- lines.push("}");
80
-
81
- const sdl = lines.join("\n");
82
- return { sdl };
83
- }
84
-
85
- export function toWebSocketModel(doc: UniSpecDocument): WebSocketModel {
86
- // Base WebSocket model intended for a modern, dashboard-oriented UI.
87
- // It exposes service metadata, a normalized list of channels and the raw
88
- // websocket protocol object, while also embedding the original UniSpec
89
- // document under a technical key for debugging and introspection.
90
-
91
- const service = doc.service;
92
- const websocket = (service.protocols?.websocket ?? {}) as UniSpecWebSocketProtocol;
93
-
94
- const channelsRecord = (websocket && typeof websocket === "object" && websocket.channels && typeof websocket.channels === "object")
95
- ? (websocket.channels as Record<string, UniSpecWebSocketChannel>)
96
- : {};
97
-
98
- const channels = Object.keys(channelsRecord).sort().map((name) => {
99
- const channel = channelsRecord[name] ?? {};
100
- return {
101
- name,
102
- summary: channel.summary ?? channel.title,
103
- description: channel.description,
104
- direction: channel.direction,
105
- messages: channel.messages,
106
- raw: channel,
107
- };
108
- });
109
-
110
- return {
111
- service: {
112
- name: service.name,
113
- title: service.title,
114
- description: service.description,
115
- },
116
- channels,
117
- rawProtocol: websocket,
118
- "x-unispec-ws": doc,
119
- };
120
- }
package/src/diff/index.ts DELETED
@@ -1,235 +0,0 @@
1
- import { UniSpecDocument } from "../types";
2
-
3
- export type ChangeSeverity = "breaking" | "non-breaking" | "unknown";
4
-
5
- export interface UniSpecChange {
6
- path: string;
7
- description: string;
8
- severity: ChangeSeverity;
9
- protocol?: "rest" | "graphql" | "websocket";
10
- kind?: string;
11
- }
12
-
13
- export interface DiffResult {
14
- changes: UniSpecChange[];
15
- }
16
-
17
- function isPlainObject(value: unknown): value is Record<string, unknown> {
18
- return Object.prototype.toString.call(value) === "[object Object]";
19
- }
20
-
21
- function diffValues(oldVal: unknown, newVal: unknown, basePath: string, out: UniSpecChange[]): void {
22
- if (oldVal === newVal) {
23
- return;
24
- }
25
-
26
- // Both plain objects → recurse by keys
27
- if (isPlainObject(oldVal) && isPlainObject(newVal)) {
28
- const oldKeys = new Set(Object.keys(oldVal));
29
- const newKeys = new Set(Object.keys(newVal));
30
-
31
- // Removed keys
32
- for (const key of oldKeys) {
33
- if (!newKeys.has(key)) {
34
- out.push({
35
- path: `${basePath}/${key}`,
36
- description: "Field removed",
37
- severity: "unknown",
38
- });
39
- }
40
- }
41
-
42
- // Added / changed keys
43
- for (const key of newKeys) {
44
- const childPath = `${basePath}/${key}`;
45
- if (!oldKeys.has(key)) {
46
- out.push({
47
- path: childPath,
48
- description: "Field added",
49
- severity: "unknown",
50
- });
51
- continue;
52
- }
53
-
54
- diffValues((oldVal as Record<string, unknown>)[key], (newVal as Record<string, unknown>)[key], childPath, out);
55
- }
56
-
57
- return;
58
- }
59
-
60
- // Arrays → shallow compare by index for now
61
- if (Array.isArray(oldVal) && Array.isArray(newVal)) {
62
- const maxLen = Math.max(oldVal.length, newVal.length);
63
- for (let i = 0; i < maxLen; i++) {
64
- const childPath = `${basePath}/${i}`;
65
- if (i >= oldVal.length) {
66
- out.push({
67
- path: childPath,
68
- description: "Item added",
69
- severity: "unknown",
70
- });
71
- } else if (i >= newVal.length) {
72
- out.push({
73
- path: childPath,
74
- description: "Item removed",
75
- severity: "unknown",
76
- });
77
- } else {
78
- diffValues(oldVal[i], newVal[i], childPath, out);
79
- }
80
- }
81
- return;
82
- }
83
-
84
- // Primitive or mismatched types → treat as value change
85
- out.push({
86
- path: basePath,
87
- description: "Value changed",
88
- severity: "unknown",
89
- });
90
- }
91
-
92
- function annotateRestChange(change: UniSpecChange): UniSpecChange {
93
- if (!change.path.startsWith("/service/protocols/rest/paths/")) {
94
- return change;
95
- }
96
-
97
- const segments = change.path.split("/").filter(Boolean);
98
- // Expected shape: ["service", "protocols", "rest", "paths", pathKey?, method?]
99
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "rest" || segments[3] !== "paths") {
100
- return change;
101
- }
102
-
103
- const pathKey = segments[4];
104
- const method = segments[5];
105
-
106
- const httpMethods = new Set(["get", "head", "options", "post", "put", "patch", "delete"]);
107
-
108
- const annotated: UniSpecChange = {
109
- ...change,
110
- protocol: "rest",
111
- };
112
-
113
- if (change.description === "Field removed") {
114
- if (pathKey && !method) {
115
- annotated.kind = "rest.path.removed";
116
- annotated.severity = "breaking";
117
- } else if (pathKey && method && httpMethods.has(method)) {
118
- annotated.kind = "rest.operation.removed";
119
- annotated.severity = "breaking";
120
- }
121
- } else if (change.description === "Field added") {
122
- if (pathKey && !method) {
123
- annotated.kind = "rest.path.added";
124
- annotated.severity = "non-breaking";
125
- } else if (pathKey && method && httpMethods.has(method)) {
126
- annotated.kind = "rest.operation.added";
127
- annotated.severity = "non-breaking";
128
- }
129
- }
130
-
131
- return annotated;
132
- }
133
-
134
- function annotateWebSocketChange(change: UniSpecChange): UniSpecChange {
135
- if (!change.path.startsWith("/service/protocols/websocket/channels/")) {
136
- return change;
137
- }
138
-
139
- const segments = change.path.split("/").filter(Boolean);
140
- // Expected: ["service","protocols","websocket","channels", channelName, ...]
141
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "websocket" || segments[3] !== "channels") {
142
- return change;
143
- }
144
-
145
- const channelName = segments[4];
146
- const next = segments[5];
147
-
148
- const annotated: UniSpecChange = {
149
- ...change,
150
- protocol: "websocket",
151
- };
152
-
153
- if (!channelName) {
154
- return annotated;
155
- }
156
-
157
- // Channel-level changes
158
- if (!next) {
159
- if (change.description === "Field removed") {
160
- annotated.kind = "websocket.channel.removed";
161
- annotated.severity = "breaking";
162
- } else if (change.description === "Field added") {
163
- annotated.kind = "websocket.channel.added";
164
- annotated.severity = "non-breaking";
165
- }
166
- return annotated;
167
- }
168
-
169
- // Message-level changes (channels/{channelName}/messages/{index})
170
- if (next === "messages") {
171
- const index = segments[6];
172
- if (typeof index === "undefined") {
173
- return annotated;
174
- }
175
-
176
- if (change.description === "Item removed") {
177
- annotated.kind = "websocket.message.removed";
178
- annotated.severity = "breaking";
179
- } else if (change.description === "Item added") {
180
- annotated.kind = "websocket.message.added";
181
- annotated.severity = "non-breaking";
182
- }
183
- }
184
-
185
- return annotated;
186
- }
187
-
188
- function annotateGraphQLChange(change: UniSpecChange): UniSpecChange {
189
- if (!change.path.startsWith("/service/protocols/graphql/operations/")) {
190
- return change;
191
- }
192
-
193
- const segments = change.path.split("/").filter(Boolean);
194
- // Expected: ["service","protocols","graphql","operations", kind, opName?]
195
- if (segments[0] !== "service" || segments[1] !== "protocols" || segments[2] !== "graphql" || segments[3] !== "operations") {
196
- return change;
197
- }
198
-
199
- const opKind = segments[4];
200
- const opName = segments[5];
201
-
202
- if (!opKind || !opName) {
203
- return change;
204
- }
205
-
206
- const annotated: UniSpecChange = {
207
- ...change,
208
- protocol: "graphql",
209
- };
210
-
211
- if (change.description === "Field removed") {
212
- annotated.kind = "graphql.operation.removed";
213
- annotated.severity = "breaking";
214
- } else if (change.description === "Field added") {
215
- annotated.kind = "graphql.operation.added";
216
- annotated.severity = "non-breaking";
217
- }
218
-
219
- return annotated;
220
- }
221
-
222
- /**
223
- * Compute a structural diff between two UniSpec documents.
224
- *
225
- * Current behavior:
226
- * - Tracks added, removed, and changed fields and array items.
227
- * - Uses JSON Pointer-like paths rooted at "" (e.g., "/info/title").
228
- * - Marks all changes with severity "unknown" for now.
229
- */
230
- export function diffUniSpec(oldDoc: UniSpecDocument, newDoc: UniSpecDocument): DiffResult {
231
- const changes: UniSpecChange[] = [];
232
- diffValues(oldDoc, newDoc, "", changes);
233
- const annotated = changes.map((change) => annotateWebSocketChange(annotateGraphQLChange(annotateRestChange(change))));
234
- return { changes: annotated };
235
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from "./types/index.js";
2
- export * from "./loader/index.js";
3
- export * from "./validator/index.js";
4
- export * from "./normalizer/index.js";
5
- export * from "./diff/index.js";
6
- export * from "./converters/index.js";
@@ -1,25 +0,0 @@
1
- import { UniSpecDocument } from "../types";
2
-
3
- export interface LoadOptions {
4
- filename?: string;
5
- }
6
-
7
- /**
8
- * Load a UniSpec document from a raw input value.
9
- * Currently supports:
10
- * - JavaScript objects (treated as already parsed UniSpec)
11
- * - JSON strings
12
- *
13
- * YAML and filesystem helpers will be added later, keeping this API stable.
14
- */
15
- export async function loadUniSpec(input: string | object, _options: LoadOptions = {}): Promise<UniSpecDocument> {
16
- if (typeof input === "string") {
17
- const trimmed = input.trim();
18
- if (!trimmed) {
19
- throw new Error("Cannot load UniSpec: input string is empty");
20
- }
21
- // For now we assume JSON; YAML support will be added later.
22
- return JSON.parse(trimmed) as UniSpecDocument;
23
- }
24
- return input as UniSpecDocument;
25
- }