@cosmicdrift/kumiko-framework 0.48.0 → 0.48.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.48.0",
3
+ "version": "0.48.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ConfigScopes } from "../constants";
3
+ import { buildManifestFromRegistry, createRegistry, defineFeature } from "../index";
4
+
5
+ const boolKey = {
6
+ type: "boolean",
7
+ scope: ConfigScopes.system,
8
+ access: { read: ["anonymous"], write: ["anonymous"] },
9
+ } as const;
10
+
11
+ describe("buildManifestFromRegistry — deterministic codepoint sort (#330)", () => {
12
+ // `bZeta` vs `balpha`: localeCompare orders these case-insensitively
13
+ // (balpha < bZeta), but a codepoint sort puts uppercase 'Z' (U+005A) ahead of
14
+ // lowercase 'a' (U+0061) — the two comparators DISAGREE. The manifest is
15
+ // serialized to byte-exact JSON and must not depend on the runner's ICU
16
+ // locale (macOS-dev vs Linux-CI). This assertion fails the instant anyone
17
+ // reverts buildManifestFromRegistry to localeCompare. (Feature names skip the
18
+ // kebab normalization that config/secret short-names go through, so they are
19
+ // the one place a case-based disagreement survives into the sorted output.)
20
+ test("features are ordered by codepoint name, not locale", () => {
21
+ const registry = createRegistry([
22
+ defineFeature("bZeta", () => {}),
23
+ defineFeature("balpha", () => {}),
24
+ ]);
25
+
26
+ const manifest = buildManifestFromRegistry(registry, { source: "test" });
27
+
28
+ expect(manifest.features.map((f) => f.name)).toEqual(["bZeta", "balpha"]);
29
+ });
30
+
31
+ test("config keys within a feature come out sorted by qualified name", () => {
32
+ const feature = defineFeature("demo", (r) => {
33
+ r.config({ keys: { "z-flag": boolKey, "a-flag": boolKey } });
34
+ });
35
+ const registry = createRegistry([feature]);
36
+
37
+ const manifest = buildManifestFromRegistry(registry, { source: "test" });
38
+ const demo = manifest.features.find((f) => f.name === "demo");
39
+
40
+ expect(demo?.configKeys.map((k) => k.qualifiedName)).toEqual([
41
+ "demo:config:a-flag",
42
+ "demo:config:z-flag",
43
+ ]);
44
+ });
45
+ });
@@ -59,6 +59,13 @@ export type FeatureManifest = {
59
59
 
60
60
  const CONFIG_SEGMENT = ":config:";
61
61
 
62
+ // Codepoint order, not localeCompare: the manifest is serialized to byte-exact
63
+ // JSON, and localeCompare's ordering depends on the runner's ICU locale → drift
64
+ // between macOS-dev and Linux-CI (#330).
65
+ function compareByCodepoint(a: string, b: string): number {
66
+ return a < b ? -1 : a > b ? 1 : 0;
67
+ }
68
+
62
69
  export type BuildManifestOptions = {
63
70
  /** Herkunfts-Beschreibung fürs Manifest (landet 1:1 im JSON). */
64
71
  readonly source: string;
@@ -111,8 +118,8 @@ export function buildManifestFromRegistry(
111
118
  });
112
119
  }
113
120
 
114
- configKeys.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
115
- secrets.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
121
+ configKeys.sort((a, b) => compareByCodepoint(a.qualifiedName, b.qualifiedName));
122
+ secrets.sort((a, b) => compareByCodepoint(a.qualifiedName, b.qualifiedName));
116
123
 
117
124
  manifestFeatures.push({
118
125
  name: feature.name,
@@ -133,7 +140,7 @@ export function buildManifestFromRegistry(
133
140
  });
134
141
  }
135
142
 
136
- manifestFeatures.sort((a, b) => a.name.localeCompare(b.name));
143
+ manifestFeatures.sort((a, b) => compareByCodepoint(a.name, b.name));
137
144
 
138
145
  return {
139
146
  source: options.source,