@wildshard/shard-format 0.4.5 → 0.4.6
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 +5 -1
- package/src/format/audio-rules.ts +36 -12
- package/src/format/issue-codes.ts +262 -0
- package/src/format/product-stream-admit.ts +31 -17
- package/src/format/product-stream-assets.ts +24 -6
- package/src/format/product-stream-miss.ts +3 -0
- package/src/format/product-stream-packs.ts +6 -1
- package/src/format/product-stream-rows.ts +63 -31
- package/src/format/product-stream-tar.ts +20 -8
- package/src/format/product-stream.ts +25 -12
- package/src/format/product.ts +29 -21
- package/src/format/productValidation.ts +74 -43
- package/src/format/tar-paths.ts +3 -2
- package/src/format/tar.ts +12 -11
- package/src/tools/runtime-hosts.json +497 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wildshard/shard-format",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "The raw shardfile format: the spec, the validator and a validate CLI. The public, versioned contract every other repo builds on.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"src/format/*.ts",
|
|
20
20
|
"!src/format/*.test.ts",
|
|
21
21
|
"src/format/memoryGates.json",
|
|
22
|
+
"src/tools/runtime-hosts.json",
|
|
22
23
|
"packages/wire/src/net-command.ts",
|
|
23
24
|
"src/cli/*.ts",
|
|
24
25
|
"src/cli/*.mjs",
|
|
@@ -115,6 +116,7 @@
|
|
|
115
116
|
"./eliteScripts": "./src/format/eliteScripts.ts",
|
|
116
117
|
"./encounterRows": "./src/format/encounterRows.ts",
|
|
117
118
|
"./encounters": "./src/format/encounters.ts",
|
|
119
|
+
"./engineMusic": "./src/format/engine-music.ts",
|
|
118
120
|
"./entryClearance": "./src/format/entryClearance.ts",
|
|
119
121
|
"./entryCoverage": "./src/format/entryCoverage.ts",
|
|
120
122
|
"./entryGeometry": "./src/format/entryGeometry.ts",
|
|
@@ -161,6 +163,7 @@
|
|
|
161
163
|
"./immutable": "./src/format/immutable.ts",
|
|
162
164
|
"./inputDeclarations": "./src/format/inputDeclarations.ts",
|
|
163
165
|
"./islandLibrary": "./src/format/islandLibrary.ts",
|
|
166
|
+
"./issue-codes": "./src/format/issue-codes.ts",
|
|
164
167
|
"./items": "./src/format/items.ts",
|
|
165
168
|
"./json": "./src/format/json.ts",
|
|
166
169
|
"./keyedSky": "./src/format/keyedSky.ts",
|
|
@@ -182,6 +185,7 @@
|
|
|
182
185
|
"./materials": "./src/format/materials.ts",
|
|
183
186
|
"./memoryAdmission": "./src/format/memoryAdmission.ts",
|
|
184
187
|
"./memoryGates.json": "./src/format/memoryGates.json",
|
|
188
|
+
"./runtime-hosts.json": "./src/tools/runtime-hosts.json",
|
|
185
189
|
"./meshCollision": "./src/format/meshCollision.ts",
|
|
186
190
|
"./meshCollisionAssets": "./src/format/meshCollisionAssets.ts",
|
|
187
191
|
"./meshCollisionBytes": "./src/format/meshCollisionBytes.ts",
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { issueError } from './issue-codes.ts'
|
|
2
|
+
|
|
1
3
|
import type { RecordingMeasure, RecordingRole } from './audio-measure.ts'
|
|
2
4
|
import type { AudioWaiver } from './audio-waiver.ts'
|
|
5
|
+
import type { IssueCodeOf } from './issue-codes.ts'
|
|
3
6
|
|
|
4
7
|
// AUDIO AU5 (meta docs/plans/AUDIO-PLAN.md, J299): shardfile owns the audio
|
|
5
8
|
// rules, and sharddev's `pack` and api-server's admission run this one check.
|
|
@@ -16,6 +19,8 @@ import type { AudioWaiver } from './audio-waiver.ts'
|
|
|
16
19
|
type AudioLevel = 'green' | 'amber' | 'red'
|
|
17
20
|
|
|
18
21
|
interface AudioFinding {
|
|
22
|
+
/** SHARD-AUTHORING SA41: the stable code (issue-codes.ts, docs/VALIDATION-CODES.md); `message` keeps its words. */
|
|
23
|
+
readonly code: IssueCodeOf<'audio'>
|
|
19
24
|
readonly level: 'amber' | 'red'
|
|
20
25
|
readonly rule: 'budget' | 'bitrate' | 'duration' | 'format' | 'waiver'
|
|
21
26
|
readonly message: string
|
|
@@ -86,11 +91,16 @@ function formatProblem(
|
|
|
86
91
|
const rate = String(row.sampleRate)
|
|
87
92
|
if (row.codec === 'wav') {
|
|
88
93
|
const message = `${row.url} is PCM WAV at ${rate} Hz; ship AAC-LC 48 kHz`
|
|
89
|
-
return { level: 'amber', rule: 'format', message }
|
|
94
|
+
return { code: 'audio-wav', level: 'amber', rule: 'format', message }
|
|
90
95
|
}
|
|
91
96
|
if (row.sampleRate !== AUDIO_RULES.sampleRate) {
|
|
92
97
|
const message = `${row.url} is ${rate} Hz AAC; ship 48 kHz`
|
|
93
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
code: 'audio-sample-rate',
|
|
100
|
+
level: 'amber',
|
|
101
|
+
rule: 'format',
|
|
102
|
+
message,
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
105
|
return undefined
|
|
96
106
|
}
|
|
@@ -99,11 +109,13 @@ function formatFinding(row: Readonly<RecordingMeasure>): AudioFinding[] {
|
|
|
99
109
|
if (row.codec === 'unreadable') {
|
|
100
110
|
const why = row.reason ?? 'unknown'
|
|
101
111
|
const message = `${row.url} is not AAC-LC 48 kHz mono or stereo (${why})`
|
|
102
|
-
return [{ level: 'red', rule: 'format', message }]
|
|
112
|
+
return [{ code: 'audio-unreadable', level: 'red', rule: 'format', message }]
|
|
103
113
|
}
|
|
104
114
|
if (row.codec === 'aac' && row.reason !== undefined) {
|
|
105
115
|
const message = `${row.url} is AAC outside the strict playback index (${row.reason})`
|
|
106
|
-
return [
|
|
116
|
+
return [
|
|
117
|
+
{ code: 'audio-strict-index', level: 'amber', rule: 'format', message },
|
|
118
|
+
]
|
|
107
119
|
}
|
|
108
120
|
const problem = formatProblem(row)
|
|
109
121
|
return problem === undefined ? [] : [problem]
|
|
@@ -114,7 +126,7 @@ function durationFinding(row: Readonly<RecordingMeasure>): AudioFinding[] {
|
|
|
114
126
|
return []
|
|
115
127
|
}
|
|
116
128
|
const message = `${row.url} runs ${row.seconds.toFixed(ONE_DECIMAL)} s, over ${String(AUDIO_RULES.maxSeconds)} s`
|
|
117
|
-
return [{ level: 'red', rule: 'duration', message }]
|
|
129
|
+
return [{ code: 'audio-duration', level: 'red', rule: 'duration', message }]
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
function bitrateFinding(row: Readonly<RecordingMeasure>): AudioFinding[] {
|
|
@@ -124,7 +136,7 @@ function bitrateFinding(row: Readonly<RecordingMeasure>): AudioFinding[] {
|
|
|
124
136
|
}
|
|
125
137
|
const layout = row.channels === MONO ? 'mono' : 'stereo'
|
|
126
138
|
const message = `${row.url} is ${row.kbps.toFixed(ONE_DECIMAL)} kbps ${layout} ${row.role}, over ${String(target)} kbps (+10%)`
|
|
127
|
-
return [{ level: 'amber', rule: 'bitrate', message }]
|
|
139
|
+
return [{ code: 'audio-bitrate', level: 'amber', rule: 'bitrate', message }]
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
function fileFindings(row: Readonly<RecordingMeasure>): AudioFinding[] {
|
|
@@ -156,17 +168,26 @@ function redBudget(input: Readonly<BudgetInput>): AudioFinding[] {
|
|
|
156
168
|
const over = `${input.slug} holds ${mb(input.bytes)} of its own audio, over the ${mb(AUDIO_RULES.budgetRedBytes)} red line`
|
|
157
169
|
const { waiver } = input
|
|
158
170
|
if (waiver === undefined) {
|
|
159
|
-
return [
|
|
171
|
+
return [
|
|
172
|
+
{ code: 'audio-budget-red', level: 'red', rule: 'budget', message: over },
|
|
173
|
+
]
|
|
160
174
|
}
|
|
161
175
|
const gap = waiverGap(waiver, input)
|
|
162
176
|
if (gap === undefined) {
|
|
163
177
|
const message = `${over}; waived until ${waiver.until} (${waiver.reason})`
|
|
164
|
-
return [
|
|
178
|
+
return [
|
|
179
|
+
{ code: 'audio-budget-waived', level: 'amber', rule: 'budget', message },
|
|
180
|
+
]
|
|
165
181
|
}
|
|
166
182
|
const named = `waiver for ${waiver.shard} until ${waiver.until} ${gap}`
|
|
167
183
|
return [
|
|
168
|
-
{
|
|
169
|
-
|
|
184
|
+
{
|
|
185
|
+
code: 'audio-waiver-invalid',
|
|
186
|
+
level: 'amber',
|
|
187
|
+
rule: 'waiver',
|
|
188
|
+
message: named,
|
|
189
|
+
},
|
|
190
|
+
{ code: 'audio-budget-red', level: 'red', rule: 'budget', message: over },
|
|
170
191
|
]
|
|
171
192
|
}
|
|
172
193
|
|
|
@@ -178,7 +199,9 @@ function budgetFindings(input: Readonly<BudgetInput>): AudioFinding[] {
|
|
|
178
199
|
return redBudget(input)
|
|
179
200
|
}
|
|
180
201
|
const message = `${input.slug} holds ${mb(input.bytes)} of its own audio, over the ${mb(AUDIO_RULES.budgetAmberBytes)} line`
|
|
181
|
-
return [
|
|
202
|
+
return [
|
|
203
|
+
{ code: 'audio-budget-amber', level: 'amber', rule: 'budget', message },
|
|
204
|
+
]
|
|
182
205
|
}
|
|
183
206
|
|
|
184
207
|
function levelOf(findings: readonly AudioFinding[]): AudioLevel {
|
|
@@ -255,7 +278,8 @@ function assertAudioAdmission(report: Readonly<AudioReport>): void {
|
|
|
255
278
|
.filter((row: Readonly<AudioFinding>): boolean => row.level === 'red')
|
|
256
279
|
.map((row: Readonly<AudioFinding>): string => row.message)
|
|
257
280
|
if (red.length > NONE) {
|
|
258
|
-
throw
|
|
281
|
+
throw issueError(
|
|
282
|
+
'audio-refused',
|
|
259
283
|
`Audio rules (AU5) refuse ${report.slug}: ${red.join('; ')}`,
|
|
260
284
|
)
|
|
261
285
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// SHARD-AUTHORING SA41 (SA21's follow-up, J317 Q19): every issue shard-format reports carries a stable `code`, so a
|
|
2
|
+
// consumer (sharddev's refusal table, api-server) matches the code instead of the message's words. This is the one
|
|
3
|
+
// table; docs/VALIDATION-CODES.md is generated from it (`node src/tools/validation-codes.ts write`) and a test fails when
|
|
4
|
+
// the page drifts. A code is stable: rename one only with a release note, since consumers match it. Messages are not
|
|
5
|
+
// part of the contract and keep their words.
|
|
6
|
+
//
|
|
7
|
+
// Where the code is:
|
|
8
|
+
// - a validator refusal (`validateShardfileProduct`, `validateShardfileStream`, `readTar`): `error.code` on the thrown
|
|
9
|
+
// Error, its message unchanged. The validators tag by the check that refused; a more specific code set nearer the
|
|
10
|
+
// throw wins. A code that is not the table's (Node's `ENOENT` from a host's file port,
|
|
11
|
+
// `ERR_ENCODING_INVALID_ENCODED_DATA` from bad UTF-8) is replaced: such codes differ by host, and the message keeps
|
|
12
|
+
// its words.
|
|
13
|
+
// - a performance report issue: `PerformanceIssue.code`.
|
|
14
|
+
// - an audio finding: `AudioFinding.code`; `assertAudioAdmission`'s refusal: `error.code`.
|
|
15
|
+
|
|
16
|
+
// Validate: both validators, the same code for the same refusal.
|
|
17
|
+
const VALIDATE_CODES = {
|
|
18
|
+
'archive-invalid':
|
|
19
|
+
'The tarball is not strict ustar (a bad header, path, padding or end, a truncated entry, too many entries), or the stored object does not decode.',
|
|
20
|
+
'index-invalid':
|
|
21
|
+
'`shard.json` is missing, too large, not canonical JSON, or fails the index schema.',
|
|
22
|
+
'file-mismatch':
|
|
23
|
+
'A listed file is missing, unreadable (a symlink, not a regular file), larger than declared, or its hash or size differs.',
|
|
24
|
+
'section-invalid':
|
|
25
|
+
'A section is not canonical JSON, fails its section schema, or is a `client/plan` kind with no declared side.',
|
|
26
|
+
'script-side':
|
|
27
|
+
'A script runs on a side its declaration does not allow (SERVER-PARITY SS7).',
|
|
28
|
+
'script-admission':
|
|
29
|
+
'The script admission rows the sections declare are invalid.',
|
|
30
|
+
'blob-invalid':
|
|
31
|
+
"A blob's bytes contradict its listing: kind or extension, registered named JSON and its schema, or costs understated.",
|
|
32
|
+
'server-file-invalid':
|
|
33
|
+
'A server file (J91) is unregistered named JSON or its bytes differ from its declared kind.',
|
|
34
|
+
'ktx2-costs':
|
|
35
|
+
'The declared KTX2 device costs differ from the verified bytes.',
|
|
36
|
+
'pack-invalid':
|
|
37
|
+
"A pack is missing a member, or a member or the pack's hash differs.",
|
|
38
|
+
'cook-invalid':
|
|
39
|
+
'The cook file, its preparation, or a mount (path or bytes) is invalid (C0, J137).',
|
|
40
|
+
'runtime-files':
|
|
41
|
+
'Copied runtime files are present without a runtime import checker, or fail it.',
|
|
42
|
+
'unlisted-file': 'A file in the product is not listed in `shard.json`.',
|
|
43
|
+
'shard-schema':
|
|
44
|
+
'The shardfile assembled from the sections fails the shardfile schema.',
|
|
45
|
+
'declared-costs':
|
|
46
|
+
'The declared costs fail preflight: a budget or memory admission over its limit.',
|
|
47
|
+
'performance-targets':
|
|
48
|
+
'A declared performance target over its limit refuses (the `REFUSAL:` lines).',
|
|
49
|
+
'asset-admission':
|
|
50
|
+
"An asset's bytes fail admission: parse, hash or wire size, costs, bounds or script contract.",
|
|
51
|
+
'world-targets':
|
|
52
|
+
'`world/targets` names an item or quest the shardfile does not declare.',
|
|
53
|
+
'source-hash': 'The sections differ from the declared `build.sourceHash`.',
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
// Stream: the streaming validator's own refusals, beside the validate codes it shares.
|
|
57
|
+
const STREAM_CODES = {
|
|
58
|
+
'stream-cap':
|
|
59
|
+
'A member, pack or the placed assets are over the streaming cap.',
|
|
60
|
+
'stream-miss': 'A check asked for bytes the stream did not hold (AC6a2).',
|
|
61
|
+
'stream-changed': 'The stored object changed between its reads.',
|
|
62
|
+
} as const
|
|
63
|
+
|
|
64
|
+
// Performance: `PerformanceIssue.code`.
|
|
65
|
+
const PERFORMANCE_CODES = {
|
|
66
|
+
'perf-memory-target':
|
|
67
|
+
'A resident memory target (tradeable) is over; always a warning.',
|
|
68
|
+
'perf-library-download-budget':
|
|
69
|
+
'The declared library download budget is over the allowance.',
|
|
70
|
+
'perf-critical-download-budget':
|
|
71
|
+
'The declared critical (sim) download budget is over its limit.',
|
|
72
|
+
'perf-library-download':
|
|
73
|
+
"The library closure's download bytes are over the allowance.",
|
|
74
|
+
'perf-critical-download':
|
|
75
|
+
"The critical closure's download bytes are over the limit.",
|
|
76
|
+
'perf-tile-download':
|
|
77
|
+
"A tile's download bytes are over its LOD cap (provisional: a warning).",
|
|
78
|
+
'perf-tile-draws':
|
|
79
|
+
"A tile's draws are over its LOD cap (provisional: a warning).",
|
|
80
|
+
'perf-tile-triangles':
|
|
81
|
+
"A tile's triangles are over its LOD cap (provisional: a warning).",
|
|
82
|
+
'perf-far-download': "The far view's download bytes are over the limit.",
|
|
83
|
+
'perf-far-draws': "The far view's draws are over the limit.",
|
|
84
|
+
'perf-far-triangles': "The far view's triangles are over the limit.",
|
|
85
|
+
'perf-native-view-unmeasured':
|
|
86
|
+
'Native view and download measurement is required and missing.',
|
|
87
|
+
'perf-native-resident-unmeasured':
|
|
88
|
+
'The native runtime declares no measured resident cost.',
|
|
89
|
+
'perf-playing-memory':
|
|
90
|
+
'The worst playing total bytes are over the limit (report-only under G433: a warning).',
|
|
91
|
+
'perf-loading-memory':
|
|
92
|
+
'The worst loading total bytes are over the limit (report-only under G433: a warning).',
|
|
93
|
+
'perf-script-sample-invalid':
|
|
94
|
+
'A measured script or fuel figure is not a finite, non-negative number.',
|
|
95
|
+
'perf-script-cpu': 'Script CPU p95 per tick is over the limit.',
|
|
96
|
+
'perf-script-fuel': 'Script fuel per tick is over the limit.',
|
|
97
|
+
'perf-script-samples-missing': 'Fewer than 60 measured script or fuel ticks.',
|
|
98
|
+
} as const
|
|
99
|
+
|
|
100
|
+
// Audio (AUDIO AU5): `AudioFinding.code`, and the admission refusal.
|
|
101
|
+
const AUDIO_CODES = {
|
|
102
|
+
'audio-unreadable':
|
|
103
|
+
'A recording is not readable as AAC-LC 48 kHz mono or stereo (red).',
|
|
104
|
+
'audio-strict-index':
|
|
105
|
+
'A recording is AAC outside the strict playback index (amber).',
|
|
106
|
+
'audio-wav': 'A recording is PCM WAV; ship AAC-LC 48 kHz (amber).',
|
|
107
|
+
'audio-sample-rate':
|
|
108
|
+
'A recording is AAC at a sample rate other than 48 kHz (amber).',
|
|
109
|
+
'audio-duration': 'A recording runs over the maximum seconds (red).',
|
|
110
|
+
'audio-bitrate':
|
|
111
|
+
"A recording's bitrate is over its role and channel cap plus slack (amber).",
|
|
112
|
+
'audio-budget-amber':
|
|
113
|
+
"The shard's own audio bytes are over the amber line (amber).",
|
|
114
|
+
'audio-budget-red':
|
|
115
|
+
"The shard's own audio bytes are over the red line, with no waiver that holds (red).",
|
|
116
|
+
'audio-budget-waived': 'Over the red line, but a dated waiver holds (amber).',
|
|
117
|
+
'audio-waiver-invalid':
|
|
118
|
+
'The waiver names another shard, has expired, or covers fewer bytes (amber; the red budget finding follows).',
|
|
119
|
+
'audio-refused':
|
|
120
|
+
'`assertAudioAdmission` refuses a report with a red finding.',
|
|
121
|
+
} as const
|
|
122
|
+
|
|
123
|
+
/** Which report a code appears in. */
|
|
124
|
+
type IssueFamily = 'validate' | 'stream' | 'performance' | 'audio'
|
|
125
|
+
|
|
126
|
+
/** Every family's codes and what each means, in table order. */
|
|
127
|
+
const ISSUE_FAMILIES: {
|
|
128
|
+
readonly validate: typeof VALIDATE_CODES
|
|
129
|
+
readonly stream: typeof STREAM_CODES
|
|
130
|
+
readonly performance: typeof PERFORMANCE_CODES
|
|
131
|
+
readonly audio: typeof AUDIO_CODES
|
|
132
|
+
} = {
|
|
133
|
+
validate: VALIDATE_CODES,
|
|
134
|
+
stream: STREAM_CODES,
|
|
135
|
+
performance: PERFORMANCE_CODES,
|
|
136
|
+
audio: AUDIO_CODES,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The codes of one family. */
|
|
140
|
+
type IssueCodeOf<Family extends IssueFamily> = Family extends IssueFamily
|
|
141
|
+
? keyof (typeof ISSUE_FAMILIES)[Family]
|
|
142
|
+
: never
|
|
143
|
+
/** A stable issue code. */
|
|
144
|
+
type IssueCode = IssueCodeOf<IssueFamily>
|
|
145
|
+
|
|
146
|
+
/** One row of the table. */
|
|
147
|
+
interface IssueCodeRow {
|
|
148
|
+
readonly code: IssueCode
|
|
149
|
+
readonly family: IssueFamily
|
|
150
|
+
readonly meaning: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A key of a family's own table, as the code it is. */
|
|
154
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- only ever given the keys of ISSUE_FAMILIES' tables
|
|
155
|
+
const asCode = (code: string): IssueCode => code as IssueCode
|
|
156
|
+
|
|
157
|
+
const FAMILY_ORDER: readonly IssueFamily[] = [
|
|
158
|
+
'validate',
|
|
159
|
+
'stream',
|
|
160
|
+
'performance',
|
|
161
|
+
'audio',
|
|
162
|
+
]
|
|
163
|
+
const ROWS: readonly IssueCodeRow[] = FAMILY_ORDER.flatMap(
|
|
164
|
+
(family: IssueFamily): IssueCodeRow[] => {
|
|
165
|
+
const table: Readonly<Record<string, string>> = ISSUE_FAMILIES[family]
|
|
166
|
+
return Object.entries(table).map(
|
|
167
|
+
([code, meaning]: readonly [string, string]): IssueCodeRow => ({
|
|
168
|
+
code: asCode(code),
|
|
169
|
+
family,
|
|
170
|
+
meaning,
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
const KNOWN: ReadonlySet<string> = new Set(
|
|
176
|
+
ROWS.map((row: IssueCodeRow): string => row.code),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
/** Every code, in table order. */
|
|
180
|
+
function issueCodeRows(): readonly IssueCodeRow[] {
|
|
181
|
+
return ROWS
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether a string is one of the table's codes. */
|
|
185
|
+
function isIssueCode(value: unknown): value is IssueCode {
|
|
186
|
+
return typeof value === 'string' && KNOWN.has(value)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The issue code an error carries, if it is one of the table's. */
|
|
190
|
+
function issueCodeOf(error: unknown): IssueCode | undefined {
|
|
191
|
+
const code: unknown =
|
|
192
|
+
typeof error === 'object' && error !== null && 'code' in error
|
|
193
|
+
? error.code
|
|
194
|
+
: undefined
|
|
195
|
+
return isIssueCode(code) ? code : undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Give `error` this code unless it already carries one of the table's (a nearer tag); returns it. An own property, so a
|
|
199
|
+
* read-only `code` getter (a DOMException's) is shadowed rather than thrown on; an error that refuses even that (frozen)
|
|
200
|
+
* is returned untouched, never replaced. */
|
|
201
|
+
function tagIssue(error: unknown, code: IssueCode): unknown {
|
|
202
|
+
if (error instanceof Error && issueCodeOf(error) === undefined) {
|
|
203
|
+
try {
|
|
204
|
+
Object.defineProperty(error, 'code', {
|
|
205
|
+
value: code,
|
|
206
|
+
writable: true,
|
|
207
|
+
enumerable: true,
|
|
208
|
+
configurable: true,
|
|
209
|
+
})
|
|
210
|
+
} catch {
|
|
211
|
+
// A frozen error keeps its own shape; the refusal still surfaces with its words.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return error
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A new Error with this message and code. */
|
|
218
|
+
function issueError(
|
|
219
|
+
code: IssueCode,
|
|
220
|
+
message: string,
|
|
221
|
+
): Error & { readonly code: IssueCode } {
|
|
222
|
+
return Object.assign(new Error(message), { code })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A tarball framing refusal (`tar: ...`): `archive-invalid`. */
|
|
226
|
+
function archiveError(message: string): Error & { readonly code: IssueCode } {
|
|
227
|
+
return issueError('archive-invalid', message)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Run one check; a refusal it throws carries `code` unless it already has one of the table's. */
|
|
231
|
+
function withIssueCode<Result>(code: IssueCode, run: () => Result): Result {
|
|
232
|
+
try {
|
|
233
|
+
return run()
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw tagIssue(error, code)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** `withIssueCode` for an async check. */
|
|
240
|
+
async function withIssueCodeAsync<Result>(
|
|
241
|
+
code: IssueCode,
|
|
242
|
+
run: () => Promise<Result>,
|
|
243
|
+
): Promise<Result> {
|
|
244
|
+
try {
|
|
245
|
+
return await run()
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw tagIssue(error, code)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export type { IssueCode, IssueCodeOf, IssueCodeRow, IssueFamily }
|
|
252
|
+
export {
|
|
253
|
+
ISSUE_FAMILIES,
|
|
254
|
+
archiveError,
|
|
255
|
+
isIssueCode,
|
|
256
|
+
issueCodeOf,
|
|
257
|
+
issueCodeRows,
|
|
258
|
+
issueError,
|
|
259
|
+
tagIssue,
|
|
260
|
+
withIssueCode,
|
|
261
|
+
withIssueCodeAsync,
|
|
262
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
assetAdmission,
|
|
9
9
|
preflightDeclaredCosts,
|
|
10
10
|
} from './assetAdmission.ts'
|
|
11
|
+
import { issueError, withIssueCode } from './issue-codes.ts'
|
|
11
12
|
import { assembleOutputShardfile } from './outputIndex.ts'
|
|
12
13
|
import { offlineMemoryAdmission } from './product.ts'
|
|
13
14
|
import { assertProductTargets, checkWorldTargets } from './productValidation.ts'
|
|
@@ -26,9 +27,15 @@ function admitHead(
|
|
|
26
27
|
index: OutputIndex,
|
|
27
28
|
payloads: ReadonlyMap<string, unknown>,
|
|
28
29
|
): Shardfile {
|
|
29
|
-
const shard =
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const shard = withIssueCode('shard-schema', (): Shardfile =>
|
|
31
|
+
parseShardfile(assembleOutputShardfile(index, payloads)),
|
|
32
|
+
)
|
|
33
|
+
withIssueCode('declared-costs', (): void => {
|
|
34
|
+
preflightDeclaredCosts(shard, offlineMemoryAdmission('warn'))
|
|
35
|
+
})
|
|
36
|
+
withIssueCode('performance-targets', (): void => {
|
|
37
|
+
assertProductTargets(shard, 'warn')
|
|
38
|
+
})
|
|
32
39
|
return shard
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -44,7 +51,9 @@ function admitBeforeAssets(
|
|
|
44
51
|
try {
|
|
45
52
|
const head = admitHead(index, payloads)
|
|
46
53
|
return {
|
|
47
|
-
admission:
|
|
54
|
+
admission: withIssueCode('asset-admission', (): AssetAdmission =>
|
|
55
|
+
assetAdmission(head, offlineMemoryAdmission('warn'), scripts),
|
|
56
|
+
),
|
|
48
57
|
refusal: undefined,
|
|
49
58
|
}
|
|
50
59
|
} catch (error) {
|
|
@@ -63,18 +72,20 @@ function admitAssets(
|
|
|
63
72
|
if (admission === undefined) {
|
|
64
73
|
throw before.refusal
|
|
65
74
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
return withIssueCode('asset-admission', (): Shardfile => {
|
|
76
|
+
admitCommonsBytes(admission, later.assets, hash)
|
|
77
|
+
const refused = later.fileErrors.find(
|
|
78
|
+
(error: string | undefined): boolean => error !== undefined,
|
|
79
|
+
)
|
|
80
|
+
if (refused !== undefined) {
|
|
81
|
+
throw new Error(refused)
|
|
82
|
+
}
|
|
83
|
+
return admitAfterFiles(
|
|
84
|
+
admission,
|
|
85
|
+
later.assets,
|
|
86
|
+
offlineMemoryAdmission('warn'),
|
|
87
|
+
)
|
|
88
|
+
})
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
/** `validateShardfileProduct`'s steps after the assets: the world targets and the source hash. */
|
|
@@ -93,7 +104,10 @@ function admitTail(
|
|
|
93
104
|
product.hash(validationSourceBytes(shard)) !==
|
|
94
105
|
product.index.build.sourceHash
|
|
95
106
|
) {
|
|
96
|
-
throw
|
|
107
|
+
throw issueError(
|
|
108
|
+
'source-hash',
|
|
109
|
+
'Output sections differ from the declared source hash',
|
|
110
|
+
)
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// (product-stream-packs.ts).
|
|
14
14
|
|
|
15
15
|
import { admitFileBytes } from './assetAdmission.ts'
|
|
16
|
+
import { issueError, withIssueCodeAsync } from './issue-codes.ts'
|
|
16
17
|
import { StreamMissError } from './product-stream-miss.ts'
|
|
17
18
|
import { packGroups, packPass } from './product-stream-packs.ts'
|
|
18
19
|
import { walkTar } from './product-stream-tar.ts'
|
|
@@ -327,7 +328,10 @@ function streamedMember(
|
|
|
327
328
|
): void {
|
|
328
329
|
state.seen += NEXT
|
|
329
330
|
if (state.plan.members.get(path)?.sha !== found.sha) {
|
|
330
|
-
throw
|
|
331
|
+
throw issueError(
|
|
332
|
+
'stream-changed',
|
|
333
|
+
'stream: the stored object changed between reads',
|
|
334
|
+
)
|
|
331
335
|
}
|
|
332
336
|
const { bytes } = found
|
|
333
337
|
if (bytes !== undefined) {
|
|
@@ -366,7 +370,10 @@ async function oneRead(
|
|
|
366
370
|
},
|
|
367
371
|
})
|
|
368
372
|
if (state.seen !== plan.members.size) {
|
|
369
|
-
throw
|
|
373
|
+
throw issueError(
|
|
374
|
+
'stream-changed',
|
|
375
|
+
'stream: the stored object changed between reads',
|
|
376
|
+
)
|
|
370
377
|
}
|
|
371
378
|
return { peak: state.peak, verdicts: packs.finish() }
|
|
372
379
|
}
|
|
@@ -411,7 +418,10 @@ function placedPaths(
|
|
|
411
418
|
}
|
|
412
419
|
}
|
|
413
420
|
if (bytes > PLACED_HOLD_CAP) {
|
|
414
|
-
throw
|
|
421
|
+
throw issueError(
|
|
422
|
+
'stream-cap',
|
|
423
|
+
'stream: the placed assets are over the streaming cap',
|
|
424
|
+
)
|
|
415
425
|
}
|
|
416
426
|
return keep
|
|
417
427
|
}
|
|
@@ -442,9 +452,8 @@ function assetPlan(base: AssetPassInput): AssetPlan {
|
|
|
442
452
|
}
|
|
443
453
|
}
|
|
444
454
|
|
|
445
|
-
/**
|
|
446
|
-
|
|
447
|
-
async function assetPasses(base: AssetPassInput): Promise<AssetPasses> {
|
|
455
|
+
/** `assetPasses`, before its refusals take `pack-invalid`. */
|
|
456
|
+
async function passes(base: AssetPassInput): Promise<AssetPasses> {
|
|
448
457
|
const plan = assetPlan(base)
|
|
449
458
|
const order = [...plan.members.keys()]
|
|
450
459
|
const sizes = new Map(
|
|
@@ -489,5 +498,14 @@ async function assetPasses(base: AssetPassInput): Promise<AssetPasses> {
|
|
|
489
498
|
}
|
|
490
499
|
}
|
|
491
500
|
|
|
501
|
+
/** The later reads: the asset checks and carried packs with the first group of left-out packs, then one read per
|
|
502
|
+
* further group. Without an admission (the shard failed before its bytes), only the packs. */
|
|
503
|
+
async function assetPasses(base: AssetPassInput): Promise<AssetPasses> {
|
|
504
|
+
return await withIssueCodeAsync(
|
|
505
|
+
'pack-invalid',
|
|
506
|
+
async (): Promise<AssetPasses> => await passes(base),
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
492
510
|
export type { AssetPassInput, AssetPasses, StoredSource }
|
|
493
511
|
export { assetPasses }
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// check, and the corpus and pin tests prove no check does.
|
|
4
4
|
|
|
5
5
|
class StreamMissError extends Error {
|
|
6
|
+
/** SHARD-AUTHORING SA41: the issue code (issue-codes.ts). */
|
|
7
|
+
public readonly code = 'stream-miss'
|
|
8
|
+
|
|
6
9
|
public constructor(message: string) {
|
|
7
10
|
super(message)
|
|
8
11
|
this.name = 'StreamMissError'
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// The packs are split into groups whose held bytes stay under a cap, and
|
|
9
9
|
// each group takes its own read of the stored object.
|
|
10
10
|
|
|
11
|
+
import { issueError } from './issue-codes.ts'
|
|
12
|
+
|
|
11
13
|
import type { OutputIndex } from './outputIndex.ts'
|
|
12
14
|
import type { StreamHasher } from './product-stream-tar.ts'
|
|
13
15
|
|
|
@@ -132,7 +134,10 @@ function packGroups(
|
|
|
132
134
|
group.length === NEXT &&
|
|
133
135
|
heldPeak([entryPaths(pack, paths)], order, sizes) > PACK_HOLD_CAP
|
|
134
136
|
) {
|
|
135
|
-
throw
|
|
137
|
+
throw issueError(
|
|
138
|
+
'stream-cap',
|
|
139
|
+
`stream: a pack over the streaming cap: ${pack.path}`,
|
|
140
|
+
)
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
}
|