@mpgd/target-config 0.14.1 → 0.15.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 +1 -0
- package/dist/index.js +1 -0
- package/dist/platformVersionAllocation.d.ts +128 -0
- package/dist/platformVersionAllocation.js +610 -0
- package/dist/runtime.js +1 -5
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EffectiveTargetConfigMatrix } from './effective.js';
|
|
2
2
|
import type { TargetConfigMatrix } from './runtime.js';
|
|
3
3
|
export * from './effective.js';
|
|
4
|
+
export * from './platformVersionAllocation.js';
|
|
4
5
|
export * from './releaseIdentity.js';
|
|
5
6
|
export * from './runtime.js';
|
|
6
7
|
export * from './viewport.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressi
|
|
|
3
3
|
const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
|
|
4
4
|
import typia from 'typia';
|
|
5
5
|
export * from './effective.js';
|
|
6
|
+
export * from './platformVersionAllocation.js';
|
|
6
7
|
export * from './releaseIdentity.js';
|
|
7
8
|
export * from './runtime.js';
|
|
8
9
|
export * from './viewport.js';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform version number allocation and verification policies.
|
|
3
|
+
*
|
|
4
|
+
* This module answers "which number means what" for a game release without
|
|
5
|
+
* touching a filesystem, a clock, the environment, git, or the network. It
|
|
6
|
+
* separates four questions that must not collapse into one value:
|
|
7
|
+
*
|
|
8
|
+
* - `gameVersion` — the player-facing game SemVer.
|
|
9
|
+
* - `releaseRevision` — an immutable build of one source/kit/config tuple,
|
|
10
|
+
* shared across targets through the `<gameVersion>-v<revision>` label.
|
|
11
|
+
* - platform upload counters (`android.versionCode`, `ios.buildNumber`) —
|
|
12
|
+
* store upload ordering, never reset, and are independent of the SemVer.
|
|
13
|
+
* - the Microsoft Store numbers — either an independent four-part history
|
|
14
|
+
* (legacy policy) or, behind an explicit opt-in, one `shellRevision` from
|
|
15
|
+
* which both package numbers derive.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is a pure computation over an explicit input ledger: the
|
|
18
|
+
* result is a *candidate* allocation. Nothing is reserved, persisted, or
|
|
19
|
+
* serialized; see docs/PLATFORM_VERSION_ALLOCATION.md for the concurrency
|
|
20
|
+
* boundaries consumers must implement themselves.
|
|
21
|
+
*/
|
|
22
|
+
/** Opt-in Microsoft Store policy deriving both package numbers from one shell revision. */
|
|
23
|
+
export declare const HOSTED_PWA_SHELL_VERSION_POLICY = "hosted-pwa-shell-revision-v1";
|
|
24
|
+
/** Microsoft Store package version components are bounded to 16 bits. */
|
|
25
|
+
export declare const MICROSOFT_STORE_VERSION_COMPONENT_MAX = 65535;
|
|
26
|
+
/** Android `versionCode` upper bound from the official versioning documentation. */
|
|
27
|
+
export declare const ANDROID_VERSION_CODE_MAX = 2100000000;
|
|
28
|
+
/** Targets whose plan entry only carries the shared release identity. */
|
|
29
|
+
export declare const LABEL_ONLY_RELEASE_TARGETS: readonly ['web-preview', 'reddit', 'ait', 'verse8', 'wechat', 'tiktok'];
|
|
30
|
+
/** Every target supported by the allocation policies. */
|
|
31
|
+
export declare const PLATFORM_VERSION_RELEASE_TARGETS: readonly ["web-preview", "reddit", "ait", "verse8", "wechat", "tiktok", "microsoft-store", "android", "ios"];
|
|
32
|
+
export type PlatformVersionReleaseTarget = typeof PLATFORM_VERSION_RELEASE_TARGETS[number];
|
|
33
|
+
/** Whether a release consumes a new platform number or only ships hosted web content. */
|
|
34
|
+
export type PlatformVersionIntent = 'package-upload' | 'hosted-content-only';
|
|
35
|
+
export interface PlatformVersionTargetRequest {
|
|
36
|
+
readonly target: PlatformVersionReleaseTarget;
|
|
37
|
+
/**
|
|
38
|
+
* Defaults to `package-upload`. `hosted-content-only` is only meaningful for
|
|
39
|
+
* `microsoft-store`: a hosted web-content change that does not upload a new
|
|
40
|
+
* Windows package must not consume a Store shell revision.
|
|
41
|
+
*/
|
|
42
|
+
readonly intent?: PlatformVersionIntent;
|
|
43
|
+
}
|
|
44
|
+
/** Legacy independent four-part Microsoft Store history. */
|
|
45
|
+
export interface IndependentMicrosoftStoreLedgerEntry {
|
|
46
|
+
readonly packageVersion: string;
|
|
47
|
+
readonly classicPackageVersion: string;
|
|
48
|
+
}
|
|
49
|
+
/** Opt-in schema-3 entry: package numbers derive from `shellRevision`. */
|
|
50
|
+
export interface HostedPwaShellLedgerEntry {
|
|
51
|
+
readonly versionPolicy: typeof HOSTED_PWA_SHELL_VERSION_POLICY;
|
|
52
|
+
readonly shellRevision: number;
|
|
53
|
+
}
|
|
54
|
+
export type MicrosoftStoreLedgerEntry = IndependentMicrosoftStoreLedgerEntry | HostedPwaShellLedgerEntry;
|
|
55
|
+
export interface PlatformVersionLedger {
|
|
56
|
+
readonly schemaVersion: 2 | 3;
|
|
57
|
+
readonly platforms: {
|
|
58
|
+
readonly 'microsoft-store'?: MicrosoftStoreLedgerEntry;
|
|
59
|
+
readonly android?: {
|
|
60
|
+
readonly versionCode: number;
|
|
61
|
+
};
|
|
62
|
+
readonly ios?: {
|
|
63
|
+
readonly buildNumber: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
readonly releaseRevision: {
|
|
67
|
+
readonly lastAllocated: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export interface PlatformVersionReleasePlan {
|
|
71
|
+
readonly schemaVersion: 2;
|
|
72
|
+
readonly gameId: string;
|
|
73
|
+
readonly gameVersion: string;
|
|
74
|
+
readonly releaseRevision: number;
|
|
75
|
+
readonly releaseLabel: string;
|
|
76
|
+
readonly sourceGitSha: string;
|
|
77
|
+
readonly kitGitSha: string;
|
|
78
|
+
readonly targetConfigDigest: string;
|
|
79
|
+
readonly buildId: string;
|
|
80
|
+
readonly targets: Record<string, Record<string, unknown>>;
|
|
81
|
+
}
|
|
82
|
+
export interface AllocatePlatformVersionsInput {
|
|
83
|
+
readonly gameId: string;
|
|
84
|
+
readonly gameVersion: string;
|
|
85
|
+
readonly sourceGitSha: string;
|
|
86
|
+
readonly kitGitSha: string;
|
|
87
|
+
readonly targetConfigDigest: string;
|
|
88
|
+
readonly ledger: PlatformVersionLedger;
|
|
89
|
+
readonly targets: readonly PlatformVersionTargetRequest[];
|
|
90
|
+
/** An immutable prepared plan to revalidate and extend instead of allocating a new revision. */
|
|
91
|
+
readonly existingPlan?: PlatformVersionReleasePlan;
|
|
92
|
+
}
|
|
93
|
+
export interface AllocatePlatformVersionsResult {
|
|
94
|
+
/** The proposed next ledger. The input ledger object is never modified. */
|
|
95
|
+
readonly ledger: PlatformVersionLedger;
|
|
96
|
+
/** The prepared release plan, reused or newly allocated. */
|
|
97
|
+
readonly plan: PlatformVersionReleasePlan;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Compute the next candidate ledger and release plan for one release.
|
|
101
|
+
*
|
|
102
|
+
* With `existingPlan` absent, the next global `releaseRevision` is allocated
|
|
103
|
+
* and every requested target receives its next platform number (except
|
|
104
|
+
* hosted-content-only Store requests). With `existingPlan` present, the plan
|
|
105
|
+
* is revalidated against its provenance and the ledger; targets it already
|
|
106
|
+
* covers are reused without consuming new numbers, and targets it lacks are
|
|
107
|
+
* allocated — a deliberate extension point of the immutable-plan contract.
|
|
108
|
+
*
|
|
109
|
+
* On any failure the call throws and neither the inputs nor any returned
|
|
110
|
+
* state are partially updated: intermediate work happens on private clones.
|
|
111
|
+
*/
|
|
112
|
+
export declare function allocatePlatformVersions(input: AllocatePlatformVersionsInput): AllocatePlatformVersionsResult;
|
|
113
|
+
/**
|
|
114
|
+
* Derive both Microsoft Store package numbers from one shell revision:
|
|
115
|
+
* `modern = 2.0.<revision>.0` is always greater than `classic = 1.0.<revision>.0`,
|
|
116
|
+
* the fourth component stays `0`, and the third is bounded to 16 bits.
|
|
117
|
+
*/
|
|
118
|
+
export declare function formatHostedPwaShellVersions(shellRevision: number): {
|
|
119
|
+
readonly packageVersion: string;
|
|
120
|
+
readonly classicPackageVersion: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Validate a platform version ledger without allocating anything. Rejects
|
|
124
|
+
* legacy schema-1 ledgers: converting an operational ledger is an explicit
|
|
125
|
+
* migration decision this module never performs.
|
|
126
|
+
*/
|
|
127
|
+
export declare function assertPlatformVersionLedger(value: unknown): PlatformVersionLedger;
|
|
128
|
+
export declare function formatPlatformVersionReleaseLabel(gameVersion: string, releaseRevision: number): string;
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform version number allocation and verification policies.
|
|
3
|
+
*
|
|
4
|
+
* This module answers "which number means what" for a game release without
|
|
5
|
+
* touching a filesystem, a clock, the environment, git, or the network. It
|
|
6
|
+
* separates four questions that must not collapse into one value:
|
|
7
|
+
*
|
|
8
|
+
* - `gameVersion` — the player-facing game SemVer.
|
|
9
|
+
* - `releaseRevision` — an immutable build of one source/kit/config tuple,
|
|
10
|
+
* shared across targets through the `<gameVersion>-v<revision>` label.
|
|
11
|
+
* - platform upload counters (`android.versionCode`, `ios.buildNumber`) —
|
|
12
|
+
* store upload ordering, never reset, and are independent of the SemVer.
|
|
13
|
+
* - the Microsoft Store numbers — either an independent four-part history
|
|
14
|
+
* (legacy policy) or, behind an explicit opt-in, one `shellRevision` from
|
|
15
|
+
* which both package numbers derive.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is a pure computation over an explicit input ledger: the
|
|
18
|
+
* result is a *candidate* allocation. Nothing is reserved, persisted, or
|
|
19
|
+
* serialized; see docs/PLATFORM_VERSION_ALLOCATION.md for the concurrency
|
|
20
|
+
* boundaries consumers must implement themselves.
|
|
21
|
+
*/
|
|
22
|
+
import { createMpgdReleaseIdentity, isMpgdFinalSemVer } from './releaseIdentity.js';
|
|
23
|
+
/** Opt-in Microsoft Store policy deriving both package numbers from one shell revision. */
|
|
24
|
+
export const HOSTED_PWA_SHELL_VERSION_POLICY = 'hosted-pwa-shell-revision-v1';
|
|
25
|
+
/** Microsoft Store package version components are bounded to 16 bits. */
|
|
26
|
+
export const MICROSOFT_STORE_VERSION_COMPONENT_MAX = 65_535;
|
|
27
|
+
/** Android `versionCode` upper bound from the official versioning documentation. */
|
|
28
|
+
export const ANDROID_VERSION_CODE_MAX = 2_100_000_000;
|
|
29
|
+
/** Targets whose plan entry only carries the shared release identity. */
|
|
30
|
+
export const LABEL_ONLY_RELEASE_TARGETS = [
|
|
31
|
+
'web-preview',
|
|
32
|
+
'reddit',
|
|
33
|
+
'ait',
|
|
34
|
+
'verse8',
|
|
35
|
+
'wechat',
|
|
36
|
+
'tiktok',
|
|
37
|
+
];
|
|
38
|
+
/** Every target supported by the allocation policies. */
|
|
39
|
+
export const PLATFORM_VERSION_RELEASE_TARGETS = [
|
|
40
|
+
...LABEL_ONLY_RELEASE_TARGETS,
|
|
41
|
+
'microsoft-store',
|
|
42
|
+
'android',
|
|
43
|
+
'ios',
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Compute the next candidate ledger and release plan for one release.
|
|
47
|
+
*
|
|
48
|
+
* With `existingPlan` absent, the next global `releaseRevision` is allocated
|
|
49
|
+
* and every requested target receives its next platform number (except
|
|
50
|
+
* hosted-content-only Store requests). With `existingPlan` present, the plan
|
|
51
|
+
* is revalidated against its provenance and the ledger; targets it already
|
|
52
|
+
* covers are reused without consuming new numbers, and targets it lacks are
|
|
53
|
+
* allocated — a deliberate extension point of the immutable-plan contract.
|
|
54
|
+
*
|
|
55
|
+
* On any failure the call throws and neither the inputs nor any returned
|
|
56
|
+
* state are partially updated: intermediate work happens on private clones.
|
|
57
|
+
*/
|
|
58
|
+
export function allocatePlatformVersions(input) {
|
|
59
|
+
const targets = normalizeTargetRequests(input.targets);
|
|
60
|
+
const gameId = requireToken(input.gameId, 'game id');
|
|
61
|
+
const gameVersion = requireFinalVersion(input.gameVersion, 'game version');
|
|
62
|
+
const sourceGitSha = requireFullGitSha(input.sourceGitSha, 'game source SHA');
|
|
63
|
+
const kitGitSha = requireFullGitSha(input.kitGitSha, 'kit SHA');
|
|
64
|
+
const targetConfigDigest = requireSha256Digest(input.targetConfigDigest, 'target config digest');
|
|
65
|
+
const ledger = cloneLedger(input.ledger);
|
|
66
|
+
const existingPlan = input.existingPlan === undefined ? undefined : clonePlan(input.existingPlan);
|
|
67
|
+
const releaseRevision = existingPlan === undefined
|
|
68
|
+
? nextReleaseRevision(ledger)
|
|
69
|
+
: requirePositiveInteger(existingPlan.releaseRevision, 'release plan releaseRevision');
|
|
70
|
+
const releaseLabel = formatPlatformVersionReleaseLabel(gameVersion, releaseRevision);
|
|
71
|
+
const buildId = `${gameId}-${releaseLabel}-${sourceGitSha.slice(0, 12)}`;
|
|
72
|
+
const plan = existingPlan ?? {
|
|
73
|
+
schemaVersion: 2,
|
|
74
|
+
gameId,
|
|
75
|
+
gameVersion,
|
|
76
|
+
releaseRevision,
|
|
77
|
+
releaseLabel,
|
|
78
|
+
sourceGitSha,
|
|
79
|
+
kitGitSha,
|
|
80
|
+
targetConfigDigest,
|
|
81
|
+
buildId,
|
|
82
|
+
targets: {},
|
|
83
|
+
};
|
|
84
|
+
if (plan.releaseLabel !== releaseLabel) {
|
|
85
|
+
throw new Error(`Existing release plan label ${plan.releaseLabel} is immutable (${releaseLabel} expected).`);
|
|
86
|
+
}
|
|
87
|
+
if (plan.buildId !== buildId) {
|
|
88
|
+
throw new Error(`Existing release plan buildId ${plan.buildId} is immutable (${buildId} expected).`);
|
|
89
|
+
}
|
|
90
|
+
assertPlanIdentity(plan, { gameId, gameVersion, sourceGitSha, kitGitSha, targetConfigDigest });
|
|
91
|
+
if (existingPlan !== undefined) {
|
|
92
|
+
assertPlanReleaseRevision(plan, ledger);
|
|
93
|
+
assertPlanPlatformVersions(plan, ledger);
|
|
94
|
+
}
|
|
95
|
+
for (const { target, intent } of targets) {
|
|
96
|
+
const existingEntry = plan.targets[target];
|
|
97
|
+
if (existingEntry !== undefined) {
|
|
98
|
+
assertPlanTargetIntent(existingEntry, target, intent);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
plan.targets[target] = allocateTargetVersion(ledger, target, gameVersion, plan.buildId, releaseLabel, intent);
|
|
102
|
+
}
|
|
103
|
+
if (existingPlan === undefined) {
|
|
104
|
+
recordAllocatedReleaseRevision(ledger, releaseRevision);
|
|
105
|
+
}
|
|
106
|
+
return { ledger, plan };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Derive both Microsoft Store package numbers from one shell revision:
|
|
110
|
+
* `modern = 2.0.<revision>.0` is always greater than `classic = 1.0.<revision>.0`,
|
|
111
|
+
* the fourth component stays `0`, and the third is bounded to 16 bits.
|
|
112
|
+
*/
|
|
113
|
+
export function formatHostedPwaShellVersions(shellRevision) {
|
|
114
|
+
const revision = requireShellRevision(shellRevision, 'Microsoft Store shell revision', false);
|
|
115
|
+
return {
|
|
116
|
+
packageVersion: `2.0.${String(revision)}.0`,
|
|
117
|
+
classicPackageVersion: `1.0.${String(revision)}.0`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate a platform version ledger without allocating anything. Rejects
|
|
122
|
+
* legacy schema-1 ledgers: converting an operational ledger is an explicit
|
|
123
|
+
* migration decision this module never performs.
|
|
124
|
+
*/
|
|
125
|
+
export function assertPlatformVersionLedger(value) {
|
|
126
|
+
return cloneLedger(value);
|
|
127
|
+
}
|
|
128
|
+
export function formatPlatformVersionReleaseLabel(gameVersion, releaseRevision) {
|
|
129
|
+
return createMpgdReleaseIdentity({ gameVersion, releaseRevision }).label;
|
|
130
|
+
}
|
|
131
|
+
function cloneLedger(value) {
|
|
132
|
+
const ledger = requireObject(structuredClone(value), 'platform version ledger');
|
|
133
|
+
if (ledger.schemaVersion === 1) {
|
|
134
|
+
throw new Error('Legacy schema-1 platform version ledgers are not converted automatically; '
|
|
135
|
+
+ 'migrate the ledger explicitly before using the allocation policies.');
|
|
136
|
+
}
|
|
137
|
+
if (ledger.schemaVersion !== 2 && ledger.schemaVersion !== 3) {
|
|
138
|
+
throw new Error('Unsupported platform version ledger schema.');
|
|
139
|
+
}
|
|
140
|
+
const platforms = requireObject(ledger.platforms, 'platform version ledger platforms');
|
|
141
|
+
// Preserve unknown top-level fields and unknown platform keys verbatim so a
|
|
142
|
+
// candidate ledger never drops data this core does not model; consumers
|
|
143
|
+
// persist candidates and silent field loss would corrupt their ledgers.
|
|
144
|
+
const normalizedPlatforms = { ...platforms };
|
|
145
|
+
if (platforms.android !== undefined) {
|
|
146
|
+
const android = requireObject(platforms.android, 'Android ledger entry');
|
|
147
|
+
rejectUnknownFields(android, ['versionCode'], 'Android ledger entry');
|
|
148
|
+
const versionCode = requireNonNegativeInteger(android.versionCode, 'Android ledger versionCode');
|
|
149
|
+
if (versionCode > ANDROID_VERSION_CODE_MAX) {
|
|
150
|
+
throw new Error(`Android ledger versionCode must not exceed the documented maximum ${String(ANDROID_VERSION_CODE_MAX)}: ${String(versionCode)}`);
|
|
151
|
+
}
|
|
152
|
+
normalizedPlatforms.android = { versionCode };
|
|
153
|
+
}
|
|
154
|
+
if (platforms.ios !== undefined) {
|
|
155
|
+
const ios = requireObject(platforms.ios, 'iOS ledger entry');
|
|
156
|
+
rejectUnknownFields(ios, ['buildNumber'], 'iOS ledger entry');
|
|
157
|
+
normalizedPlatforms.ios = {
|
|
158
|
+
buildNumber: requireNonNegativeInteger(ios.buildNumber, 'iOS ledger buildNumber'),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const microsoftStore = platforms['microsoft-store'];
|
|
162
|
+
if (microsoftStore !== undefined) {
|
|
163
|
+
normalizedPlatforms['microsoft-store'] = normalizeMicrosoftStoreLedgerEntry(microsoftStore, ledger.schemaVersion);
|
|
164
|
+
}
|
|
165
|
+
else if (ledger.schemaVersion === 3) {
|
|
166
|
+
throw new Error('Schema 3 platform version ledgers must carry a Microsoft Store entry.');
|
|
167
|
+
}
|
|
168
|
+
// Preserve unmodeled releaseRevision metadata (reservation or audit data)
|
|
169
|
+
// the same way unknown top-level fields survive into candidates.
|
|
170
|
+
const releaseRevision = requireObject(ledger.releaseRevision, 'platform version ledger releaseRevision');
|
|
171
|
+
return {
|
|
172
|
+
...ledger,
|
|
173
|
+
platforms: normalizedPlatforms,
|
|
174
|
+
releaseRevision: {
|
|
175
|
+
...releaseRevision,
|
|
176
|
+
lastAllocated: requireNonNegativeInteger(releaseRevision.lastAllocated, 'platform version ledger lastAllocated'),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function normalizeMicrosoftStoreLedgerEntry(value, schemaVersion) {
|
|
181
|
+
const entry = requireObject(value, 'Microsoft Store ledger entry');
|
|
182
|
+
rejectUnknownFields(entry, entry.versionPolicy === undefined
|
|
183
|
+
? ['classicPackageVersion', 'packageVersion']
|
|
184
|
+
: ['shellRevision', 'versionPolicy'], 'Microsoft Store ledger entry');
|
|
185
|
+
if (entry.versionPolicy !== undefined) {
|
|
186
|
+
if (entry.versionPolicy !== HOSTED_PWA_SHELL_VERSION_POLICY) {
|
|
187
|
+
throw new Error(`Unknown Microsoft Store version policy ${String(entry.versionPolicy)}; `
|
|
188
|
+
+ `the only supported opt-in policy is ${HOSTED_PWA_SHELL_VERSION_POLICY}.`);
|
|
189
|
+
}
|
|
190
|
+
const shellRevision = requireShellRevision(entry.shellRevision, 'Microsoft Store ledger shellRevision', true);
|
|
191
|
+
if (schemaVersion !== 3) {
|
|
192
|
+
throw new Error(`The ${HOSTED_PWA_SHELL_VERSION_POLICY} policy requires a schema-3 ledger; `
|
|
193
|
+
+ 'opt in explicitly instead of mixing it into a schema-2 history.');
|
|
194
|
+
}
|
|
195
|
+
return { versionPolicy: HOSTED_PWA_SHELL_VERSION_POLICY, shellRevision };
|
|
196
|
+
}
|
|
197
|
+
if (schemaVersion === 3) {
|
|
198
|
+
throw new Error(`Schema 3 Microsoft Store ledger versionPolicy must be ${HOSTED_PWA_SHELL_VERSION_POLICY}.`);
|
|
199
|
+
}
|
|
200
|
+
const packageVersion = requireFourPartVersion(entry.packageVersion, 'Microsoft Store package version');
|
|
201
|
+
const classicPackageVersion = requireFourPartVersion(entry.classicPackageVersion, 'Microsoft Store classic package version');
|
|
202
|
+
if (compareFourPartVersions(classicPackageVersion, packageVersion) >= 0) {
|
|
203
|
+
throw new Error(`Microsoft Store classicPackageVersion ${classicPackageVersion} must be lower than packageVersion ${packageVersion}.`);
|
|
204
|
+
}
|
|
205
|
+
return { classicPackageVersion, packageVersion };
|
|
206
|
+
}
|
|
207
|
+
function compareFourPartVersions(left, right) {
|
|
208
|
+
const leftParts = left.split('.').map((part) => Number.parseInt(part, 10));
|
|
209
|
+
const rightParts = right.split('.').map((part) => Number.parseInt(part, 10));
|
|
210
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
211
|
+
const leftPart = leftParts[index] ?? 0;
|
|
212
|
+
const rightPart = rightParts[index] ?? 0;
|
|
213
|
+
if (leftPart !== rightPart) {
|
|
214
|
+
return leftPart - rightPart;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
function clonePlan(value) {
|
|
220
|
+
const plan = requireObject(structuredClone(value), 'release plan');
|
|
221
|
+
if (plan.schemaVersion !== 2) {
|
|
222
|
+
throw new Error('Unsupported release plan schema.');
|
|
223
|
+
}
|
|
224
|
+
requireToken(plan.gameId, 'release plan gameId');
|
|
225
|
+
requireFinalVersion(plan.gameVersion, 'release plan gameVersion');
|
|
226
|
+
requirePositiveInteger(plan.releaseRevision, 'release plan releaseRevision');
|
|
227
|
+
validatePlanTargets(plan);
|
|
228
|
+
requireFullGitSha(plan.sourceGitSha, 'release plan sourceGitSha');
|
|
229
|
+
requireFullGitSha(plan.kitGitSha, 'release plan kitGitSha');
|
|
230
|
+
requireSha256Digest(plan.targetConfigDigest, 'release plan targetConfigDigest');
|
|
231
|
+
requireString(plan.buildId, 'release plan buildId');
|
|
232
|
+
requireString(plan.releaseLabel, 'release plan releaseLabel');
|
|
233
|
+
return plan;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Shape-check every target entry of an existing plan. Entries must match the
|
|
237
|
+
* exact form this module allocates so a hand-edited plan cannot smuggle
|
|
238
|
+
* unvalidated numbers or unknown targets into a candidate release.
|
|
239
|
+
*/
|
|
240
|
+
function validatePlanTargets(plan) {
|
|
241
|
+
const targets = requireObject(plan.targets, 'release plan targets');
|
|
242
|
+
for (const [target, value] of Object.entries(targets)) {
|
|
243
|
+
if (!PLATFORM_VERSION_RELEASE_TARGETS.includes(target)) {
|
|
244
|
+
throw new Error(`Release plan target ${target} is not supported by the allocation policies.`);
|
|
245
|
+
}
|
|
246
|
+
const entry = requireObject(value, `release plan target ${target}`);
|
|
247
|
+
if (LABEL_ONLY_RELEASE_TARGETS.includes(target)) {
|
|
248
|
+
rejectUnknownFields(entry, ['buildId', 'releaseLabel', 'version'], `release plan target ${target}`);
|
|
249
|
+
requireString(entry.buildId, `release plan target ${target} buildId`);
|
|
250
|
+
assertEqual(entry.buildId, plan.buildId, `release plan target ${target} buildId`);
|
|
251
|
+
requireString(entry.releaseLabel, `release plan target ${target} releaseLabel`);
|
|
252
|
+
assertEqual(entry.releaseLabel, plan.releaseLabel, `release plan target ${target} releaseLabel`);
|
|
253
|
+
requireFinalVersion(entry.version, `release plan target ${target} version`);
|
|
254
|
+
assertEqual(entry.version, plan.gameVersion, `release plan target ${target} version`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (target === 'android') {
|
|
258
|
+
rejectUnknownFields(entry, ['releaseLabel', 'versionCode', 'versionName'], 'release plan android target');
|
|
259
|
+
requireString(entry.releaseLabel, 'release plan android releaseLabel');
|
|
260
|
+
assertEqual(entry.releaseLabel, plan.releaseLabel, 'release plan android releaseLabel');
|
|
261
|
+
requirePositiveInteger(entry.versionCode, 'release plan android versionCode');
|
|
262
|
+
requireFinalVersion(entry.versionName, 'release plan android versionName');
|
|
263
|
+
assertEqual(entry.versionName, plan.gameVersion, 'release plan android versionName');
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (target === 'ios') {
|
|
267
|
+
rejectUnknownFields(entry, ['buildNumber', 'marketingVersion', 'releaseLabel'], 'release plan ios target');
|
|
268
|
+
requirePositiveInteger(entry.buildNumber, 'release plan ios buildNumber');
|
|
269
|
+
requireFinalVersion(entry.marketingVersion, 'release plan ios marketingVersion');
|
|
270
|
+
assertEqual(entry.marketingVersion, plan.gameVersion, 'release plan ios marketingVersion');
|
|
271
|
+
requireString(entry.releaseLabel, 'release plan ios releaseLabel');
|
|
272
|
+
assertEqual(entry.releaseLabel, plan.releaseLabel, 'release plan ios releaseLabel');
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (entry.intent !== undefined && entry.intent !== 'hosted-content-only') {
|
|
276
|
+
throw new Error(`Release plan microsoft-store intent must be hosted-content-only when present; `
|
|
277
|
+
+ `received ${String(entry.intent)}.`);
|
|
278
|
+
}
|
|
279
|
+
assertEqual(entry.releaseLabel, plan.releaseLabel, `release plan ${target} releaseLabel`);
|
|
280
|
+
if (entry.intent !== undefined) {
|
|
281
|
+
rejectUnknownFields(entry, ['intent', 'releaseLabel'], 'release plan microsoft-store target');
|
|
282
|
+
requireString(entry.releaseLabel, 'release plan microsoft-store releaseLabel');
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (entry.versionPolicy === HOSTED_PWA_SHELL_VERSION_POLICY) {
|
|
286
|
+
rejectUnknownFields(entry, [
|
|
287
|
+
'classicPackageVersion',
|
|
288
|
+
'packageVersion',
|
|
289
|
+
'releaseLabel',
|
|
290
|
+
'shellRevision',
|
|
291
|
+
'versionPolicy',
|
|
292
|
+
], 'release plan microsoft-store target');
|
|
293
|
+
requireShellRevision(entry.shellRevision, 'release plan microsoft-store shellRevision', false);
|
|
294
|
+
const derived = formatHostedPwaShellVersions(entry.shellRevision);
|
|
295
|
+
assertEqual(entry.packageVersion, derived.packageVersion, 'release plan microsoft-store package version');
|
|
296
|
+
assertEqual(entry.classicPackageVersion, derived.classicPackageVersion, 'release plan microsoft-store classic package version');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
rejectUnknownFields(entry, ['classicPackageVersion', 'packageVersion', 'releaseLabel'], 'release plan microsoft-store target');
|
|
300
|
+
requireFourPartVersion(entry.packageVersion, 'release plan microsoft-store package version');
|
|
301
|
+
requireFourPartVersion(entry.classicPackageVersion, 'release plan microsoft-store classic package version');
|
|
302
|
+
}
|
|
303
|
+
requireString(entry.releaseLabel, 'release plan microsoft-store releaseLabel');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function rejectUnknownFields(entry, allowed, label) {
|
|
307
|
+
for (const key of Object.keys(entry)) {
|
|
308
|
+
if (!allowed.includes(key)) {
|
|
309
|
+
throw new Error(`${label} has an unsupported field: ${key}.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function normalizeTargetRequests(values) {
|
|
314
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
315
|
+
throw new Error('At least one release target is required.');
|
|
316
|
+
}
|
|
317
|
+
const normalized = values.map((request) => {
|
|
318
|
+
const entry = requireObject(request, 'release target request');
|
|
319
|
+
const target = requireToken(entry.target, 'release target');
|
|
320
|
+
if (!PLATFORM_VERSION_RELEASE_TARGETS.includes(target)) {
|
|
321
|
+
throw new Error(`Unsupported release target: ${target}`);
|
|
322
|
+
}
|
|
323
|
+
const intent = (entry.intent ?? 'package-upload');
|
|
324
|
+
if (intent !== 'package-upload' && intent !== 'hosted-content-only') {
|
|
325
|
+
throw new Error(`Unknown release intent: ${String(entry.intent)}`);
|
|
326
|
+
}
|
|
327
|
+
if (intent === 'hosted-content-only' && target !== 'microsoft-store') {
|
|
328
|
+
throw new Error(`hosted-content-only is only meaningful for microsoft-store; ${target} has no hosted content split.`);
|
|
329
|
+
}
|
|
330
|
+
return { target: target, intent };
|
|
331
|
+
});
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
for (const { target } of normalized) {
|
|
334
|
+
if (seen.has(target)) {
|
|
335
|
+
throw new Error('Release targets must not contain duplicates.');
|
|
336
|
+
}
|
|
337
|
+
seen.add(target);
|
|
338
|
+
}
|
|
339
|
+
return normalized;
|
|
340
|
+
}
|
|
341
|
+
function allocateTargetVersion(ledger, target, gameVersion, buildId, releaseLabel, intent) {
|
|
342
|
+
if (LABEL_ONLY_RELEASE_TARGETS.includes(target)) {
|
|
343
|
+
return { buildId, releaseLabel, version: gameVersion };
|
|
344
|
+
}
|
|
345
|
+
switch (target) {
|
|
346
|
+
case 'microsoft-store': {
|
|
347
|
+
// Hosted web content that does not upload a Windows package must not
|
|
348
|
+
// consume a Store shell revision or four-part number; it needs no Store
|
|
349
|
+
// ledger entry at all.
|
|
350
|
+
if (intent === 'hosted-content-only') {
|
|
351
|
+
return { intent: 'hosted-content-only', releaseLabel };
|
|
352
|
+
}
|
|
353
|
+
const current = requireObject(ledger.platforms['microsoft-store'], 'Microsoft Store ledger entry');
|
|
354
|
+
if (current.versionPolicy === HOSTED_PWA_SHELL_VERSION_POLICY) {
|
|
355
|
+
// A fresh hosted-PWA ledger starts at shellRevision 0 (nothing
|
|
356
|
+
// allocated yet); nextShellRevision validates the non-negative range.
|
|
357
|
+
const nextRevision = nextShellRevision(current.shellRevision);
|
|
358
|
+
const { classicPackageVersion, packageVersion } = formatHostedPwaShellVersions(nextRevision);
|
|
359
|
+
ledger.platforms['microsoft-store'] = {
|
|
360
|
+
versionPolicy: HOSTED_PWA_SHELL_VERSION_POLICY,
|
|
361
|
+
shellRevision: nextRevision,
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
classicPackageVersion,
|
|
365
|
+
packageVersion,
|
|
366
|
+
releaseLabel,
|
|
367
|
+
shellRevision: nextRevision,
|
|
368
|
+
versionPolicy: HOSTED_PWA_SHELL_VERSION_POLICY,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const packageVersion = incrementFourPartVersion(current.packageVersion, 'Microsoft Store package version');
|
|
372
|
+
const classicPackageVersion = incrementFourPartVersion(current.classicPackageVersion, 'Microsoft Store classic package version');
|
|
373
|
+
ledger.platforms['microsoft-store'] = { packageVersion, classicPackageVersion };
|
|
374
|
+
return { classicPackageVersion, packageVersion, releaseLabel };
|
|
375
|
+
}
|
|
376
|
+
case 'android': {
|
|
377
|
+
const current = requireObject(ledger.platforms.android, 'Android ledger entry');
|
|
378
|
+
const versionCode = requireNonNegativeInteger(current.versionCode, 'Android ledger versionCode');
|
|
379
|
+
if (versionCode >= ANDROID_VERSION_CODE_MAX) {
|
|
380
|
+
throw new Error(`Android versionCode has reached its documented maximum of ${String(ANDROID_VERSION_CODE_MAX)}.`);
|
|
381
|
+
}
|
|
382
|
+
ledger.platforms.android = { versionCode: versionCode + 1 };
|
|
383
|
+
return { releaseLabel, versionCode: versionCode + 1, versionName: gameVersion };
|
|
384
|
+
}
|
|
385
|
+
case 'ios': {
|
|
386
|
+
const current = requireObject(ledger.platforms.ios, 'iOS ledger entry');
|
|
387
|
+
const buildNumber = requireNonNegativeInteger(current.buildNumber, 'iOS ledger buildNumber');
|
|
388
|
+
if (buildNumber >= Number.MAX_SAFE_INTEGER) {
|
|
389
|
+
throw new Error('iOS buildNumber has exhausted the safe integer range.');
|
|
390
|
+
}
|
|
391
|
+
ledger.platforms.ios = { buildNumber: buildNumber + 1 };
|
|
392
|
+
return { buildNumber: buildNumber + 1, marketingVersion: gameVersion, releaseLabel };
|
|
393
|
+
}
|
|
394
|
+
default:
|
|
395
|
+
throw new Error(`Unsupported release target: ${target}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function assertPlanIdentity(plan, expected) {
|
|
399
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
400
|
+
if (plan[key] !== value) {
|
|
401
|
+
throw new Error(`Existing release plan ${key} is immutable `
|
|
402
|
+
+ `(${String(plan[key])} !== ${String(value)}). `
|
|
403
|
+
+ 'Prepare a new immutable release plan; a target-only release may retain '
|
|
404
|
+
+ 'the game SemVer but receives the next global release revision.');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function assertPlanTargetIntent(entry, target, intent) {
|
|
409
|
+
const recordedIntent = entry.intent === undefined ? 'package-upload' : entry.intent;
|
|
410
|
+
if (recordedIntent !== intent) {
|
|
411
|
+
throw new Error(`Existing release plan target ${target} intent ${String(recordedIntent)} is immutable (${intent} requested).`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function assertPlanReleaseRevision(plan, ledger) {
|
|
415
|
+
const lastAllocated = requireNonNegativeInteger(ledger.releaseRevision.lastAllocated, 'platform version ledger lastAllocated');
|
|
416
|
+
if (plan.releaseRevision > lastAllocated) {
|
|
417
|
+
throw new Error(`Existing release plan releaseRevision ${String(plan.releaseRevision)} is ahead of ledger ${String(lastAllocated)}.`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function assertPlanPlatformVersions(plan, ledger) {
|
|
421
|
+
const microsoftStore = plan.targets['microsoft-store'];
|
|
422
|
+
if (microsoftStore !== undefined) {
|
|
423
|
+
const actual = requireObject(microsoftStore, 'existing Microsoft Store plan');
|
|
424
|
+
if (actual.intent !== undefined) {
|
|
425
|
+
if (actual.intent !== 'hosted-content-only') {
|
|
426
|
+
throw new Error(`Unknown Microsoft Store plan intent: ${String(actual.intent)}.`);
|
|
427
|
+
}
|
|
428
|
+
if (actual.packageVersion !== undefined || actual.classicPackageVersion !== undefined) {
|
|
429
|
+
throw new Error('A hosted-content-only Microsoft Store plan must not claim package versions.');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const expected = requireObject(ledger.platforms['microsoft-store'], 'Microsoft Store ledger entry');
|
|
434
|
+
if (expected.versionPolicy === HOSTED_PWA_SHELL_VERSION_POLICY) {
|
|
435
|
+
if (actual.versionPolicy !== HOSTED_PWA_SHELL_VERSION_POLICY) {
|
|
436
|
+
throw new Error(`Existing Microsoft Store plan versionPolicy must be ${HOSTED_PWA_SHELL_VERSION_POLICY}.`);
|
|
437
|
+
}
|
|
438
|
+
const shellRevision = requireShellRevision(actual.shellRevision, 'existing Microsoft Store plan shellRevision', false);
|
|
439
|
+
assertNumberNotAhead(shellRevision, requireShellRevision(expected.shellRevision, 'Microsoft Store ledger shellRevision', true), 'Microsoft Store shell revision');
|
|
440
|
+
const derived = formatHostedPwaShellVersions(shellRevision);
|
|
441
|
+
assertEqual(actual.packageVersion, derived.packageVersion, 'existing Microsoft Store package version');
|
|
442
|
+
assertEqual(actual.classicPackageVersion, derived.classicPackageVersion, 'existing Microsoft Store classic package version');
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
if (actual.versionPolicy !== undefined) {
|
|
446
|
+
throw new Error(`An existing ${String(actual.versionPolicy)} Microsoft Store plan cannot be reused against an independent four-part ledger.`);
|
|
447
|
+
}
|
|
448
|
+
assertVersionNotAhead(actual.packageVersion, expected.packageVersion, 'Microsoft Store package version');
|
|
449
|
+
assertVersionNotAhead(actual.classicPackageVersion, expected.classicPackageVersion, 'Microsoft Store classic package version');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const android = plan.targets.android;
|
|
454
|
+
if (android !== undefined) {
|
|
455
|
+
assertNumberNotAhead(requirePositiveInteger(requireObject(android, 'existing Android plan').versionCode, 'existing Android plan versionCode'), requireObject(ledger.platforms.android, 'Android ledger entry').versionCode, 'Android versionCode');
|
|
456
|
+
}
|
|
457
|
+
const ios = plan.targets.ios;
|
|
458
|
+
if (ios !== undefined) {
|
|
459
|
+
assertNumberNotAhead(requirePositiveInteger(requireObject(ios, 'existing iOS plan').buildNumber, 'existing iOS plan buildNumber'), requireObject(ledger.platforms.ios, 'iOS ledger entry').buildNumber, 'iOS buildNumber');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function incrementFourPartVersion(value, label) {
|
|
463
|
+
const version = requireFourPartVersion(value, label);
|
|
464
|
+
const parts = version.split('.').map((part) => Number.parseInt(part, 10));
|
|
465
|
+
if (parts[3] !== 0) {
|
|
466
|
+
throw new Error(`${label} must reserve the fourth component as 0 for Windows Store compatibility: ${version}`);
|
|
467
|
+
}
|
|
468
|
+
if ((parts[2] ?? 0) >= MICROSOFT_STORE_VERSION_COMPONENT_MAX) {
|
|
469
|
+
throw new Error(`${label} has exhausted its third numeric component: ${version}`);
|
|
470
|
+
}
|
|
471
|
+
parts[2] = (parts[2] ?? 0) + 1;
|
|
472
|
+
return parts.join('.');
|
|
473
|
+
}
|
|
474
|
+
function nextShellRevision(value) {
|
|
475
|
+
const revision = requireShellRevision(value, 'Microsoft Store ledger shellRevision', true);
|
|
476
|
+
if (revision >= MICROSOFT_STORE_VERSION_COMPONENT_MAX) {
|
|
477
|
+
throw new Error(`Microsoft Store shell revision range is exhausted at ${String(revision)}.`);
|
|
478
|
+
}
|
|
479
|
+
return revision + 1;
|
|
480
|
+
}
|
|
481
|
+
function requireShellRevision(value, label, allowZero) {
|
|
482
|
+
const revision = allowZero
|
|
483
|
+
? requireNonNegativeInteger(value, label)
|
|
484
|
+
: requirePositiveInteger(value, label);
|
|
485
|
+
if (revision > MICROSOFT_STORE_VERSION_COMPONENT_MAX) {
|
|
486
|
+
throw new Error(`${label} must fit the Microsoft Store third version component (0-${String(MICROSOFT_STORE_VERSION_COMPONENT_MAX)}).`);
|
|
487
|
+
}
|
|
488
|
+
return revision;
|
|
489
|
+
}
|
|
490
|
+
function nextReleaseRevision(ledger) {
|
|
491
|
+
const lastAllocated = requireNonNegativeInteger(ledger.releaseRevision.lastAllocated, 'platform version ledger lastAllocated');
|
|
492
|
+
if (lastAllocated >= Number.MAX_SAFE_INTEGER) {
|
|
493
|
+
throw new Error('Platform version ledger has exhausted its release revision range.');
|
|
494
|
+
}
|
|
495
|
+
return lastAllocated + 1;
|
|
496
|
+
}
|
|
497
|
+
function recordAllocatedReleaseRevision(ledger, releaseRevision) {
|
|
498
|
+
const lastAllocated = requireNonNegativeInteger(ledger.releaseRevision.lastAllocated, 'platform version ledger lastAllocated');
|
|
499
|
+
if (releaseRevision !== lastAllocated + 1) {
|
|
500
|
+
throw new Error(`Release revision ${String(releaseRevision)} does not follow ledger revision ${String(lastAllocated)}.`);
|
|
501
|
+
}
|
|
502
|
+
// Keep unmodeled releaseRevision metadata when recording the allocation.
|
|
503
|
+
ledger.releaseRevision = { ...ledger.releaseRevision, lastAllocated: releaseRevision };
|
|
504
|
+
}
|
|
505
|
+
function requireFourPartVersion(value, label) {
|
|
506
|
+
const version = requireString(value, label);
|
|
507
|
+
if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(version)) {
|
|
508
|
+
throw new Error(`${label} must be a four-part numeric version without leading zeroes: ${version}`);
|
|
509
|
+
}
|
|
510
|
+
const parts = version.split('.');
|
|
511
|
+
// The package-generation input contract rejects a zero major component and
|
|
512
|
+
// any nonzero fourth component, so ledgers carrying them must fail here
|
|
513
|
+
// instead of allocating unusable plans.
|
|
514
|
+
if (Number.parseInt(parts[0] ?? '0', 10) === 0) {
|
|
515
|
+
throw new Error(`${label} must have a non-zero first component: ${version}`);
|
|
516
|
+
}
|
|
517
|
+
if (Number.parseInt(parts[3] ?? '0', 10) !== 0) {
|
|
518
|
+
throw new Error(`${label} must reserve the fourth component as 0: ${version}`);
|
|
519
|
+
}
|
|
520
|
+
for (const part of parts) {
|
|
521
|
+
if (Number.parseInt(part, 10) > MICROSOFT_STORE_VERSION_COMPONENT_MAX) {
|
|
522
|
+
throw new Error(`${label} components must fit 0-${String(MICROSOFT_STORE_VERSION_COMPONENT_MAX)}: ${version}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return version;
|
|
526
|
+
}
|
|
527
|
+
function requireObject(value, label) {
|
|
528
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
529
|
+
throw new Error(`${label} must be an object.`);
|
|
530
|
+
}
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
function requireString(value, label) {
|
|
534
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
535
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
536
|
+
}
|
|
537
|
+
return value;
|
|
538
|
+
}
|
|
539
|
+
function requireToken(value, label) {
|
|
540
|
+
const token = requireString(value, label);
|
|
541
|
+
if (!/^[0-9A-Za-z._-]+$/u.test(token)) {
|
|
542
|
+
throw new Error(`${label} has unsupported characters: ${token}`);
|
|
543
|
+
}
|
|
544
|
+
return token;
|
|
545
|
+
}
|
|
546
|
+
/** Git revisions are full lowercase 40-character SHA-1 values. */
|
|
547
|
+
function requireFullGitSha(value, label) {
|
|
548
|
+
const sha = requireString(value, label);
|
|
549
|
+
if (!/^[0-9a-f]{40}$/u.test(sha)) {
|
|
550
|
+
throw new Error(`${label} must be a full 40-character Git SHA.`);
|
|
551
|
+
}
|
|
552
|
+
return sha;
|
|
553
|
+
}
|
|
554
|
+
/** Target configuration digests are 64-character SHA-256 values, distinct from Git revisions. */
|
|
555
|
+
function requireSha256Digest(value, label) {
|
|
556
|
+
const digest = requireString(value, label);
|
|
557
|
+
if (!/^[0-9a-f]{64}$/u.test(digest)) {
|
|
558
|
+
throw new Error(`${label} must be a 64-character SHA-256 digest.`);
|
|
559
|
+
}
|
|
560
|
+
return digest;
|
|
561
|
+
}
|
|
562
|
+
function requireFinalVersion(value, label) {
|
|
563
|
+
const version = requireString(value, label);
|
|
564
|
+
if (!isMpgdFinalSemVer(version)) {
|
|
565
|
+
throw new Error(`${label} must be a final SemVer without prerelease/build metadata: ${version}`);
|
|
566
|
+
}
|
|
567
|
+
return version;
|
|
568
|
+
}
|
|
569
|
+
function requirePositiveInteger(value, label) {
|
|
570
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
|
|
571
|
+
throw new Error(`${label} must be a positive safe integer.`);
|
|
572
|
+
}
|
|
573
|
+
return value;
|
|
574
|
+
}
|
|
575
|
+
function requireNonNegativeInteger(value, label) {
|
|
576
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
577
|
+
throw new Error(`${label} must be a non-negative safe integer.`);
|
|
578
|
+
}
|
|
579
|
+
return value;
|
|
580
|
+
}
|
|
581
|
+
function assertEqual(actual, expected, label) {
|
|
582
|
+
if (actual !== expected) {
|
|
583
|
+
throw new Error(`${label} ${String(actual)} must equal ${String(expected)}.`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function assertVersionNotAhead(actual, latest, label) {
|
|
587
|
+
const actualParts = requireFourPartVersion(actual, `existing ${label}`)
|
|
588
|
+
.split('.')
|
|
589
|
+
.map((part) => Number.parseInt(part, 10));
|
|
590
|
+
const latestParts = requireFourPartVersion(latest, `ledger ${label}`)
|
|
591
|
+
.split('.')
|
|
592
|
+
.map((part) => Number.parseInt(part, 10));
|
|
593
|
+
for (let index = 0; index < actualParts.length; index += 1) {
|
|
594
|
+
const actualPart = actualParts[index] ?? 0;
|
|
595
|
+
const latestPart = latestParts[index] ?? 0;
|
|
596
|
+
if (actualPart === latestPart) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (actualPart > latestPart) {
|
|
600
|
+
throw new Error(`Existing release plan ${label} ${String(actual)} is ahead of ledger ${String(latest)}; the ledger cannot decrease.`);
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function assertNumberNotAhead(actual, latest, label) {
|
|
606
|
+
const normalizedLatest = requireNonNegativeInteger(latest, `ledger ${label}`);
|
|
607
|
+
if (actual > normalizedLatest) {
|
|
608
|
+
throw new Error(`Existing release plan ${label} ${String(actual)} is ahead of ledger ${String(normalizedLatest)}; the ledger cannot decrease.`);
|
|
609
|
+
}
|
|
610
|
+
}
|
package/dist/runtime.js
CHANGED
|
@@ -157,11 +157,7 @@ export function getFeatureAvailability(feature, config, capabilities) {
|
|
|
157
157
|
enabled,
|
|
158
158
|
targetEnabled,
|
|
159
159
|
capabilitySupported,
|
|
160
|
-
reason: enabled
|
|
161
|
-
? 'available'
|
|
162
|
-
: targetEnabled
|
|
163
|
-
? 'capability-unsupported'
|
|
164
|
-
: 'target-disabled',
|
|
160
|
+
reason: enabled ? 'available' : targetEnabled ? 'capability-unsupported' : 'target-disabled',
|
|
165
161
|
};
|
|
166
162
|
}
|
|
167
163
|
export function getIntegrationAvailability(integration, config, gateway) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/target-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Target-specific platform feature availability and effective config helpers for mpgd.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"typia": "13.0.2",
|
|
40
|
-
"@mpgd/catalog": "0.7.
|
|
41
|
-
"@mpgd/i18n": "0.6.
|
|
42
|
-
"@mpgd/platform": "0.12.
|
|
40
|
+
"@mpgd/catalog": "0.7.3",
|
|
41
|
+
"@mpgd/i18n": "0.6.2",
|
|
42
|
+
"@mpgd/platform": "0.12.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"ttsc": "0.
|
|
45
|
+
"ttsc": "0.30.4",
|
|
46
46
|
"typescript": "7.0.2"
|
|
47
47
|
},
|
|
48
48
|
"main": "./dist/index.js",
|
|
@@ -55,6 +55,6 @@
|
|
|
55
55
|
"lint": "ttsc --noEmit",
|
|
56
56
|
"format": "ttsc format",
|
|
57
57
|
"fix": "ttsc fix",
|
|
58
|
-
"test": "cd ../.. && node tools/run-ttsx.mjs packages/target-config/test/target-config-smoke.ts"
|
|
58
|
+
"test": "cd ../.. && node tools/run-ttsx.mjs packages/target-config/test/target-config-smoke.ts && node tools/run-ttsx.mjs packages/target-config/test/platform-version-allocation-smoke.ts"
|
|
59
59
|
}
|
|
60
60
|
}
|