@openpkg-ts/spec 0.3.1 → 0.4.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/dist/index.d.ts CHANGED
@@ -123,6 +123,21 @@ type OpenPkg = {
123
123
  extensions?: SpecExtension;
124
124
  };
125
125
  declare function dereference(spec: OpenPkg): OpenPkg;
126
+ type BreakingSeverity = "high" | "medium" | "low";
127
+ interface CategorizedBreaking {
128
+ id: string;
129
+ name: string;
130
+ kind: SpecExportKind;
131
+ severity: BreakingSeverity;
132
+ reason: string;
133
+ }
134
+ /** Minimal member change info for categorization (avoids circular dep with SDK) */
135
+ interface MemberChangeInfo {
136
+ className: string;
137
+ memberName: string;
138
+ memberKind: "method" | "property" | "accessor" | "constructor";
139
+ changeType: "added" | "removed" | "signature-changed";
140
+ }
126
141
  type SpecDiff = {
127
142
  breaking: string[];
128
143
  nonBreaking: string[];
@@ -137,18 +152,51 @@ type SpecDiff = {
137
152
  driftResolved: number;
138
153
  };
139
154
  declare function diffSpec(oldSpec: OpenPkg, newSpec: OpenPkg): SpecDiff;
155
+ /**
156
+ * Categorize breaking changes by severity
157
+ *
158
+ * @param breaking - Array of breaking change IDs
159
+ * @param oldSpec - Previous spec version
160
+ * @param newSpec - Current spec version
161
+ * @param memberChanges - Optional member-level changes for classes
162
+ * @returns Categorized breaking changes sorted by severity (high first)
163
+ */
164
+ declare function categorizeBreakingChanges(breaking: string[], oldSpec: OpenPkg, newSpec: OpenPkg, memberChanges?: MemberChangeInfo[]): CategorizedBreaking[];
140
165
  declare function normalize(spec: OpenPkg): OpenPkg;
166
+ /** Supported schema versions */
167
+ type SchemaVersion = "0.1.0" | "0.2.0" | "latest";
141
168
  type SpecError = {
142
169
  instancePath: string;
143
170
  message: string;
144
171
  keyword: string;
145
172
  };
146
- declare function validateSpec(spec: unknown): {
173
+ /**
174
+ * Validate a spec against a specific schema version.
175
+ *
176
+ * @param spec - The spec object to validate
177
+ * @param version - Schema version to validate against (default: 'latest')
178
+ * @returns Validation result
179
+ */
180
+ declare function validateSpec(spec: unknown, version?: SchemaVersion): {
147
181
  ok: true;
148
182
  } | {
149
183
  ok: false;
150
184
  errors: SpecError[];
151
185
  };
152
- declare function assertSpec(spec: unknown): asserts spec is OpenPkg;
153
- declare function getValidationErrors(spec: unknown): SpecError[];
154
- export { validateSpec, normalize, getValidationErrors, diffSpec, dereference, assertSpec, SpecVisibility, SpecTypeParameter, SpecTypeKind, SpecType, SpecTag, SpecSource, SpecSignatureReturn, SpecSignatureParameter, SpecSignature, SpecSchema, SpecMember, SpecExtension, SpecExportKind, SpecExport, SpecExample, SpecDocsMetadata, SpecDocSignal, SpecDocDrift, SCHEMA_VERSION, SCHEMA_URL, OpenPkgMeta, OpenPkg, JSON_SCHEMA_DRAFT };
186
+ /**
187
+ * Assert that a value is a valid OpenPkg spec.
188
+ * Throws an error with details if validation fails.
189
+ *
190
+ * @param spec - The value to validate
191
+ * @param version - Schema version to validate against (default: 'latest')
192
+ */
193
+ declare function assertSpec(spec: unknown, version?: SchemaVersion): asserts spec is OpenPkg;
194
+ /**
195
+ * Get validation errors for a spec.
196
+ *
197
+ * @param spec - The spec to validate
198
+ * @param version - Schema version to validate against (default: 'latest')
199
+ * @returns Array of validation errors (empty if valid)
200
+ */
201
+ declare function getValidationErrors(spec: unknown, version?: SchemaVersion): SpecError[];
202
+ export { validateSpec, normalize, getValidationErrors, diffSpec, dereference, categorizeBreakingChanges, assertSpec, SpecVisibility, SpecTypeParameter, SpecTypeKind, SpecType, SpecTag, SpecSource, SpecSignatureReturn, SpecSignatureParameter, SpecSignature, SpecSchema, SpecMember, SpecExtension, SpecExportKind, SpecExport, SpecExample, SpecDocsMetadata, SpecDocSignal, SpecDocDrift, SpecDiff, SCHEMA_VERSION, SCHEMA_URL, OpenPkgMeta, OpenPkg, MemberChangeInfo, JSON_SCHEMA_DRAFT, CategorizedBreaking, BreakingSeverity };
package/dist/index.js CHANGED
@@ -159,7 +159,21 @@ function toMap(items) {
159
159
  }
160
160
  return map;
161
161
  }
162
- var DOC_KEYS = new Set(["description", "examples", "tags", "source", "rawComments"]);
162
+ var DOC_KEYS = new Set([
163
+ "description",
164
+ "examples",
165
+ "tags",
166
+ "rawComments",
167
+ "source",
168
+ "docs",
169
+ "displayName",
170
+ "slug",
171
+ "importPath",
172
+ "category",
173
+ "coverageScore",
174
+ "missing",
175
+ "drift"
176
+ ]);
163
177
  function isDocOnlyChange(a, b) {
164
178
  const structuralA = normalizeForComparison(removeDocFields(a));
165
179
  const structuralB = normalizeForComparison(removeDocFields(b));
@@ -204,6 +218,70 @@ function sortKeys(value) {
204
218
  }
205
219
  return result;
206
220
  }
221
+ function categorizeBreakingChanges(breaking, oldSpec, newSpec, memberChanges) {
222
+ const oldExportMap = toExportMap(oldSpec.exports);
223
+ const newExportMap = toExportMap(newSpec.exports);
224
+ const categorized = [];
225
+ for (const id of breaking) {
226
+ const oldExport = oldExportMap.get(id);
227
+ const newExport = newExportMap.get(id);
228
+ if (!newExport) {
229
+ const kind = oldExport?.kind ?? "variable";
230
+ categorized.push({
231
+ id,
232
+ name: oldExport?.name ?? id,
233
+ kind,
234
+ severity: kind === "function" || kind === "class" ? "high" : "medium",
235
+ reason: "removed"
236
+ });
237
+ continue;
238
+ }
239
+ if (oldExport?.kind === "class" && memberChanges?.length) {
240
+ const classChanges = memberChanges.filter((mc) => mc.className === id);
241
+ if (classChanges.length > 0) {
242
+ const hasConstructorChange = classChanges.some((mc) => mc.memberKind === "constructor");
243
+ const hasMethodRemoval = classChanges.some((mc) => mc.changeType === "removed" && mc.memberKind === "method");
244
+ categorized.push({
245
+ id,
246
+ name: oldExport.name,
247
+ kind: "class",
248
+ severity: hasConstructorChange || hasMethodRemoval ? "high" : "medium",
249
+ reason: hasConstructorChange ? "constructor changed" : hasMethodRemoval ? "methods removed" : "methods changed"
250
+ });
251
+ continue;
252
+ }
253
+ }
254
+ if (oldExport?.kind === "interface" || oldExport?.kind === "type") {
255
+ categorized.push({
256
+ id,
257
+ name: oldExport.name,
258
+ kind: oldExport.kind,
259
+ severity: "medium",
260
+ reason: "type definition changed"
261
+ });
262
+ continue;
263
+ }
264
+ if (oldExport?.kind === "function") {
265
+ categorized.push({
266
+ id,
267
+ name: oldExport.name,
268
+ kind: "function",
269
+ severity: "high",
270
+ reason: "signature changed"
271
+ });
272
+ continue;
273
+ }
274
+ categorized.push({
275
+ id,
276
+ name: oldExport?.name ?? id,
277
+ kind: oldExport?.kind ?? "variable",
278
+ severity: "low",
279
+ reason: "changed"
280
+ });
281
+ }
282
+ const severityOrder = { high: 0, medium: 1, low: 2 };
283
+ return categorized.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
284
+ }
207
285
  // src/normalize.ts
208
286
  var DEFAULT_ECOSYSTEM = "js/ts";
209
287
  var arrayFieldsByExport = ["signatures", "members", "examples", "tags"];
@@ -419,7 +497,16 @@ var openpkg_schema_default = {
419
497
  kind: {
420
498
  type: "string",
421
499
  description: "Kind of export",
422
- enum: ["function", "class", "variable", "interface", "type", "enum", "namespace", "external"]
500
+ enum: [
501
+ "function",
502
+ "class",
503
+ "variable",
504
+ "interface",
505
+ "type",
506
+ "enum",
507
+ "namespace",
508
+ "external"
509
+ ]
423
510
  },
424
511
  description: {
425
512
  type: "string",
@@ -660,8 +747,315 @@ var openpkg_schema_default = {
660
747
  }
661
748
  }
662
749
  };
750
+ // schemas/v0.1.0/openpkg.schema.json
751
+ var openpkg_schema_default2 = {
752
+ $schema: "https://json-schema.org/draft/2020-12/schema",
753
+ $id: "https://unpkg.com/@openpkg-ts/spec/schemas/v0.1.0/openpkg.schema.json",
754
+ title: "OpenPkg Specification",
755
+ description: "Schema for OpenPkg specification files",
756
+ type: "object",
757
+ required: ["openpkg", "meta", "exports"],
758
+ properties: {
759
+ $schema: {
760
+ type: "string",
761
+ description: "Reference to the OpenPkg schema version",
762
+ pattern: "^(https://raw\\.githubusercontent\\.com/ryanwaits/openpkg/main/schemas/v[0-9]+\\.[0-9]+\\.[0-9]+/openpkg\\.schema\\.json|https://unpkg\\.com/@openpkg-ts/spec/schemas/v[0-9]+\\.[0-9]+\\.[0-9]+/openpkg\\.schema\\.json)$"
763
+ },
764
+ openpkg: {
765
+ type: "string",
766
+ description: "OpenPkg specification version",
767
+ pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$",
768
+ const: "0.1.0"
769
+ },
770
+ meta: {
771
+ type: "object",
772
+ description: "Package metadata",
773
+ required: ["name"],
774
+ properties: {
775
+ name: {
776
+ type: "string",
777
+ description: "Package name"
778
+ },
779
+ version: {
780
+ type: "string",
781
+ description: "Package version"
782
+ },
783
+ description: {
784
+ type: "string",
785
+ description: "Package description"
786
+ },
787
+ license: {
788
+ type: "string",
789
+ description: "Package license"
790
+ },
791
+ repository: {
792
+ type: "string",
793
+ description: "Repository URL"
794
+ },
795
+ ecosystem: {
796
+ type: "string",
797
+ description: "Package ecosystem"
798
+ }
799
+ }
800
+ },
801
+ exports: {
802
+ type: "array",
803
+ description: "List of exported items",
804
+ items: {
805
+ $ref: "#/$defs/export"
806
+ }
807
+ },
808
+ types: {
809
+ type: "array",
810
+ description: "List of type definitions",
811
+ items: {
812
+ $ref: "#/$defs/typeDef"
813
+ }
814
+ }
815
+ },
816
+ $defs: {
817
+ export: {
818
+ type: "object",
819
+ required: ["id", "name", "kind"],
820
+ properties: {
821
+ id: {
822
+ type: "string",
823
+ description: "Unique identifier for the export"
824
+ },
825
+ name: {
826
+ type: "string",
827
+ description: "Export name"
828
+ },
829
+ slug: {
830
+ type: "string",
831
+ description: "Stable slug for linking"
832
+ },
833
+ displayName: {
834
+ type: "string",
835
+ description: "UI-friendly label"
836
+ },
837
+ category: {
838
+ type: "string",
839
+ description: "Grouping hint for navigation"
840
+ },
841
+ importPath: {
842
+ type: "string",
843
+ description: "Recommended import path"
844
+ },
845
+ kind: {
846
+ type: "string",
847
+ description: "Kind of export",
848
+ enum: ["function", "class", "variable", "interface", "type", "enum"]
849
+ },
850
+ description: {
851
+ type: "string",
852
+ description: "JSDoc/TSDoc description"
853
+ },
854
+ examples: {
855
+ type: "array",
856
+ description: "Usage examples from documentation",
857
+ items: {
858
+ type: "string"
859
+ }
860
+ },
861
+ signatures: {
862
+ type: "array",
863
+ description: "Function/method signatures",
864
+ items: {
865
+ $ref: "#/$defs/signature"
866
+ }
867
+ },
868
+ type: {
869
+ description: "Type reference or inline schema for variables",
870
+ oneOf: [{ type: "string" }, { $ref: "#/$defs/schema" }]
871
+ },
872
+ members: {
873
+ type: "array",
874
+ description: "Class/interface/enum members",
875
+ items: { type: "object" }
876
+ },
877
+ tags: {
878
+ type: "array",
879
+ description: "JSDoc/TSDoc tags",
880
+ items: {
881
+ type: "object",
882
+ required: ["name", "text"],
883
+ properties: {
884
+ name: { type: "string" },
885
+ text: { type: "string" }
886
+ },
887
+ additionalProperties: false
888
+ }
889
+ },
890
+ source: {
891
+ $ref: "#/$defs/sourceLocation"
892
+ }
893
+ }
894
+ },
895
+ typeDef: {
896
+ type: "object",
897
+ required: ["id", "name", "kind"],
898
+ properties: {
899
+ id: {
900
+ type: "string",
901
+ description: "Unique identifier for the type"
902
+ },
903
+ name: {
904
+ type: "string",
905
+ description: "Type name"
906
+ },
907
+ slug: {
908
+ type: "string",
909
+ description: "Stable slug for linking"
910
+ },
911
+ displayName: {
912
+ type: "string",
913
+ description: "UI-friendly label"
914
+ },
915
+ category: {
916
+ type: "string",
917
+ description: "Grouping hint for navigation"
918
+ },
919
+ importPath: {
920
+ type: "string",
921
+ description: "Recommended import path"
922
+ },
923
+ kind: {
924
+ type: "string",
925
+ description: "Kind of type definition",
926
+ enum: ["interface", "type", "enum", "class"]
927
+ },
928
+ description: {
929
+ type: "string",
930
+ description: "JSDoc/TSDoc description"
931
+ },
932
+ schema: {
933
+ $ref: "#/$defs/schema"
934
+ },
935
+ type: {
936
+ type: "string",
937
+ description: "Type expression for type aliases"
938
+ },
939
+ members: {
940
+ type: "array",
941
+ description: "Members for classes/interfaces/enums",
942
+ items: { type: "object" }
943
+ },
944
+ tags: {
945
+ type: "array",
946
+ description: "JSDoc/TSDoc tags",
947
+ items: {
948
+ type: "object",
949
+ required: ["name", "text"],
950
+ properties: {
951
+ name: { type: "string" },
952
+ text: { type: "string" }
953
+ },
954
+ additionalProperties: false
955
+ }
956
+ },
957
+ source: {
958
+ $ref: "#/$defs/sourceLocation"
959
+ }
960
+ }
961
+ },
962
+ signature: {
963
+ type: "object",
964
+ properties: {
965
+ parameters: {
966
+ type: "array",
967
+ items: {
968
+ $ref: "#/$defs/parameter"
969
+ }
970
+ },
971
+ returns: {
972
+ $ref: "#/$defs/returns"
973
+ },
974
+ description: {
975
+ type: "string",
976
+ description: "Signature-level description"
977
+ }
978
+ }
979
+ },
980
+ parameter: {
981
+ type: "object",
982
+ required: ["name", "required"],
983
+ properties: {
984
+ name: {
985
+ type: "string",
986
+ description: "Parameter name"
987
+ },
988
+ required: {
989
+ type: "boolean",
990
+ description: "Whether the parameter is required"
991
+ },
992
+ schema: {
993
+ $ref: "#/$defs/schema"
994
+ }
995
+ }
996
+ },
997
+ returns: {
998
+ type: "object",
999
+ properties: {
1000
+ schema: {
1001
+ $ref: "#/$defs/schema"
1002
+ },
1003
+ description: {
1004
+ type: "string",
1005
+ description: "Return value description"
1006
+ }
1007
+ }
1008
+ },
1009
+ schema: {
1010
+ anyOf: [
1011
+ {
1012
+ type: "boolean"
1013
+ },
1014
+ {
1015
+ type: "object",
1016
+ properties: {
1017
+ $ref: {
1018
+ type: "string",
1019
+ description: "Reference to another type",
1020
+ pattern: "^#/types/[A-Za-z0-9_.-]+$"
1021
+ }
1022
+ },
1023
+ required: ["$ref"],
1024
+ additionalProperties: false
1025
+ },
1026
+ {
1027
+ type: "object",
1028
+ not: {
1029
+ required: ["$ref"]
1030
+ },
1031
+ additionalProperties: true
1032
+ }
1033
+ ]
1034
+ },
1035
+ sourceLocation: {
1036
+ type: "object",
1037
+ required: ["file", "line"],
1038
+ properties: {
1039
+ file: {
1040
+ type: "string",
1041
+ description: "Source file path"
1042
+ },
1043
+ line: {
1044
+ type: "integer",
1045
+ description: "Line number in source file",
1046
+ minimum: 1
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+ };
663
1052
 
664
1053
  // src/validate.ts
1054
+ var LATEST_VERSION = "0.2.0";
1055
+ var schemas = {
1056
+ "0.1.0": openpkg_schema_default2,
1057
+ "0.2.0": openpkg_schema_default
1058
+ };
665
1059
  var ajv = new Ajv({
666
1060
  strict: false,
667
1061
  allErrors: true,
@@ -669,8 +1063,23 @@ var ajv = new Ajv({
669
1063
  $data: true
670
1064
  });
671
1065
  addFormats(ajv);
672
- var validate = ajv.compile(openpkg_schema_default);
673
- function validateSpec(spec) {
1066
+ var validatorCache = new Map;
1067
+ function getValidator(version = "latest") {
1068
+ const resolvedVersion = version === "latest" ? LATEST_VERSION : version;
1069
+ let validator = validatorCache.get(resolvedVersion);
1070
+ if (validator) {
1071
+ return validator;
1072
+ }
1073
+ const schema = schemas[resolvedVersion];
1074
+ if (!schema) {
1075
+ throw new Error(`Unknown schema version: ${resolvedVersion}. Available: ${Object.keys(schemas).join(", ")}`);
1076
+ }
1077
+ validator = ajv.compile(schema);
1078
+ validatorCache.set(resolvedVersion, validator);
1079
+ return validator;
1080
+ }
1081
+ function validateSpec(spec, version = "latest") {
1082
+ const validate = getValidator(version);
674
1083
  const ok = validate(spec);
675
1084
  if (ok) {
676
1085
  return { ok: true };
@@ -685,8 +1094,8 @@ function validateSpec(spec) {
685
1094
  errors
686
1095
  };
687
1096
  }
688
- function assertSpec(spec) {
689
- const result = validateSpec(spec);
1097
+ function assertSpec(spec, version = "latest") {
1098
+ const result = validateSpec(spec, version);
690
1099
  if (!result.ok) {
691
1100
  const details = result.errors.map((error) => `- ${error.instancePath || "/"} ${error.message}`).join(`
692
1101
  `);
@@ -694,8 +1103,8 @@ function assertSpec(spec) {
694
1103
  ${details}`);
695
1104
  }
696
1105
  }
697
- function getValidationErrors(spec) {
698
- const result = validateSpec(spec);
1106
+ function getValidationErrors(spec, version = "latest") {
1107
+ const result = validateSpec(spec, version);
699
1108
  return result.ok ? [] : result.errors;
700
1109
  }
701
1110
  export {
@@ -704,6 +1113,7 @@ export {
704
1113
  getValidationErrors,
705
1114
  diffSpec,
706
1115
  dereference,
1116
+ categorizeBreakingChanges,
707
1117
  assertSpec,
708
1118
  SCHEMA_VERSION,
709
1119
  SCHEMA_URL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpkg-ts/spec",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Shared schema, validation, and diff utilities for OpenPkg specs",
5
5
  "keywords": [
6
6
  "openpkg",
@@ -173,7 +173,16 @@
173
173
  "kind": {
174
174
  "type": "string",
175
175
  "description": "Kind of export",
176
- "enum": ["function", "class", "variable", "interface", "type", "enum", "namespace", "external"]
176
+ "enum": [
177
+ "function",
178
+ "class",
179
+ "variable",
180
+ "interface",
181
+ "type",
182
+ "enum",
183
+ "namespace",
184
+ "external"
185
+ ]
177
186
  },
178
187
  "description": {
179
188
  "type": "string",