@cplieger/deadset-ts 3.3.1 → 3.5.0

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.
@@ -1,5 +1,5 @@
1
1
  import { ConfigError, defaultConfig, } from "./config.js";
2
- import { FIXED_SEVERITY_CODES } from "./kinds.js";
2
+ import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
3
3
  import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
4
4
  /** Names the command-line flags in a refusal, where a document is named by its path. */
5
5
  const FLAGS_LABEL = "the command-line flags";
@@ -8,6 +8,12 @@ const FAMILY_KEY_LENGTH = 4;
8
8
  const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
9
9
  const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
10
10
  const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
11
+ const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
12
+ const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
13
+ const PROVIDER_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/u;
14
+ const PROVIDER_DIGEST = /^sha256:[0-9a-f]{64}$/u;
15
+ /** The members only an acquirable provider entry carries. */
16
+ const ACQUIRABLE_MEMBERS = ["source", "version", "digest"];
11
17
  /** The members only a platform build configuration carries. */
12
18
  const PLATFORM_MEMBERS = ["os", "arch", "tags"];
13
19
  /** The member only a project build configuration carries. */
@@ -223,8 +229,8 @@ function readDelimiters(analysis, label) {
223
229
  };
224
230
  }
225
231
  /**
226
- * A severity key that names no setting, because the Contract fixes the severity of
227
- * the kind or kinds it names.
232
+ * A severity key that names no setting: it names no live kind, or the Contract
233
+ * fixes the severity of the kind or kinds it names.
228
234
  */
229
235
  function unimplementedSeverityKey(label, path, reason) {
230
236
  return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented: ${reason}`);
@@ -245,16 +251,19 @@ function spellCodes(codes) {
245
251
  }
246
252
  return `${codes.slice(0, -1).join(", ")} and ${String(codes[codes.length - 1])}`;
247
253
  }
254
+ /** Whether one severity key names a live kind: the code itself, or a family holding one. */
255
+ function namesLiveKind(code) {
256
+ return LIVE_CODES.some((candidate) => code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)));
257
+ }
248
258
  /**
249
259
  * The severity object, one code at a time.
250
260
  *
251
- * A key is one issue-kind code or one two-digit family prefix, and a key naming a
252
- * kind whose severity the Contract fixes, or a family prefix whose range holds
253
- * one, is an unimplemented key rather than a setting.
261
+ * A key is one issue-kind code or one two-digit family prefix. A key naming no
262
+ * live kind, and a key naming a kind whose severity the Contract fixes or a family
263
+ * prefix whose range holds one, is an unimplemented key rather than a setting.
254
264
  *
255
- * Whether a key names a kind this analyzer reports is not decided here. This
256
- * analyzer reports no issue kind, so the question has one answer for every key
257
- * and asking it would refuse the whole object.
265
+ * Whether a key names a kind this analyzer reports is not decided here: a live
266
+ * kind of another language is a setting the analyzer of that language reads.
258
267
  */
259
268
  function readSeverity(document, label) {
260
269
  const object = section(document, SEVERITY_SECTION, SEVERITY_SECTION, label);
@@ -267,6 +276,11 @@ function readSeverity(document, label) {
267
276
  if (!SEVERITY_KEY.test(code)) {
268
277
  throw unimplementedSeverityKey(label, path, "a severity key is one issue-kind code or one two-digit family prefix");
269
278
  }
279
+ if (!namesLiveKind(code)) {
280
+ throw unimplementedSeverityKey(label, path, code.length === FAMILY_KEY_LENGTH
281
+ ? "the family this prefix names holds no live issue kind"
282
+ : "the code names no live issue kind");
283
+ }
270
284
  const fixed = fixedByContract(code);
271
285
  if (fixed.length > 0) {
272
286
  throw unimplementedSeverityKey(label, path, `the Contract fixes the severity of ${spellCodes(fixed)}, which this key names`);
@@ -292,6 +306,160 @@ function readExemptions(document, label) {
292
306
  }
293
307
  return disabled;
294
308
  }
309
+ /** The members that name a declaration, each belonging to exactly one shape. */
310
+ const SHAPE_MEMBERS = ["symbol", "module", "name", "global"];
311
+ /** The prefix of a stable symbol reference in the TypeScript form. */
312
+ const SYMBOL_PREFIX = "ts://";
313
+ /**
314
+ * A bare specifier: neither relative nor absolute, and not one the package's own
315
+ * imports field maps, each of which names a file of the analyzed program.
316
+ */
317
+ const BARE_SPECIFIER = /^[^./#]/u;
318
+ /**
319
+ * One declaration entry, read as the one shape its members name. An entry carrying
320
+ * members of two shapes, or of none, is refused at the entry, as is a module named
321
+ * without the declaration's path or a path without its module.
322
+ */
323
+ function readDeclaration(entry, at, label) {
324
+ if (!isRecord(entry)) {
325
+ throw malformed(label, at, "is not an object");
326
+ }
327
+ const named = SHAPE_MEMBERS.filter((name) => Object.hasOwn(entry, name));
328
+ if (named.length === 1 && named[0] === "symbol") {
329
+ const symbol = requireMember(readString(entry, "symbol", `${at}.symbol`, label), `${at}.symbol`, label);
330
+ if (!symbol.startsWith(SYMBOL_PREFIX)) {
331
+ throw malformed(label, `${at}.symbol`, `${JSON.stringify(symbol)} is not a ts:// reference`);
332
+ }
333
+ return { shape: "symbol", symbol };
334
+ }
335
+ if (named.length === 1 && named[0] === "global") {
336
+ const path = `${at}.global`;
337
+ return {
338
+ shape: "global",
339
+ global: requireMember(readString(entry, "global", path, label), path, label),
340
+ };
341
+ }
342
+ if (named.length === 2 && named.includes("module") && named.includes("name")) {
343
+ const module = requireMember(readString(entry, "module", `${at}.module`, label), `${at}.module`, label);
344
+ if (!BARE_SPECIFIER.test(module)) {
345
+ throw malformed(label, `${at}.module`, `${JSON.stringify(module)} is not a bare specifier: a file of the analyzed program is named by its symbol`);
346
+ }
347
+ const path = `${at}.name`;
348
+ return {
349
+ shape: "module",
350
+ module,
351
+ name: requireMember(readString(entry, "name", path, label), path, label),
352
+ };
353
+ }
354
+ throw malformed(label, at, named.length === 0
355
+ ? 'names no declaration: an entry carries "symbol", "module" with "name", or "global"'
356
+ : `carries ${spell(named)}, which is not one of "symbol", "module" with "name", or "global"`);
357
+ }
358
+ /** One array of declaration entries, absent where the document does not write it. */
359
+ function readDeclarations(parent, name, path, label) {
360
+ const value = member(parent, name);
361
+ if (value === undefined) {
362
+ return undefined;
363
+ }
364
+ if (!Array.isArray(value)) {
365
+ throw malformed(label, path, "is not an array");
366
+ }
367
+ return value.map((entry, index) => readDeclaration(entry, `${path}[${String(index)}]`, label));
368
+ }
369
+ /**
370
+ * One lifecycle contract. The member names are required, and the entry names at least
371
+ * one component declaration or one base class, because a contract naming neither
372
+ * makes no class a component and so retains nothing.
373
+ */
374
+ function readLifecycleContract(entry, at, label) {
375
+ if (!isRecord(entry)) {
376
+ throw malformed(label, at, "is not an object");
377
+ }
378
+ const components = readDeclarations(entry, "components", `${at}.components`, label) ?? [];
379
+ const bases = readDeclarations(entry, "bases", `${at}.bases`, label) ?? [];
380
+ const members = readStrings(entry, "members", `${at}.members`, label, 1);
381
+ if (members === undefined) {
382
+ throw malformed(label, `${at}.members`, "is required and names nothing");
383
+ }
384
+ if (components.length === 0 && bases.length === 0) {
385
+ throw malformed(label, at, 'names no declaration in "components" and no class in "bases", so it makes no class a component');
386
+ }
387
+ return { components, bases, members };
388
+ }
389
+ /** The lifecycle contracts, one per framework the project declares. */
390
+ function readLifecycleContracts(ts, label) {
391
+ const path = "ts.lifecycle_contracts";
392
+ const value = member(ts, "lifecycle_contracts");
393
+ if (value === undefined) {
394
+ return undefined;
395
+ }
396
+ if (!Array.isArray(value)) {
397
+ throw malformed(label, path, "is not an array");
398
+ }
399
+ return value.map((entry, index) => readLifecycleContract(entry, `${path}[${String(index)}]`, label));
400
+ }
401
+ /** One string member a provider entry requires, refused where it fails `pattern`. */
402
+ function readMatching(entry, name, at, label, pattern) {
403
+ const path = `${at}.${name}`;
404
+ const value = requireMember(readString(entry, name, path, label), path, label);
405
+ if (!pattern.test(value)) {
406
+ throw malformed(label, path, `${JSON.stringify(value)} does not match ${pattern.source}`);
407
+ }
408
+ return value;
409
+ }
410
+ /**
411
+ * One provider entry, read as the one shape its members name: a member only an
412
+ * acquirable analyzer carries makes it acquirable, and then it carries all three.
413
+ */
414
+ function readProvider(entry, at, label) {
415
+ if (!isRecord(entry)) {
416
+ throw malformed(label, at, "is not an object");
417
+ }
418
+ const name = readMatching(entry, "name", at, label, ANALYZER_NAME);
419
+ const languages = readStrings(entry, "languages", `${at}.languages`, label, 1, LANGUAGES);
420
+ if (languages === undefined) {
421
+ throw malformed(label, `${at}.languages`, "is required and names nothing");
422
+ }
423
+ const commandPath = `${at}.command`;
424
+ const command = requireMember(readString(entry, "command", commandPath, label), commandPath, label);
425
+ if (!ACQUIRABLE_MEMBERS.some((member) => Object.hasOwn(entry, member))) {
426
+ return { shape: "installed", name, languages, command };
427
+ }
428
+ return {
429
+ shape: "acquirable",
430
+ name,
431
+ languages,
432
+ command,
433
+ source: readMatching(entry, "source", at, label, PROVIDER_SOURCE),
434
+ version: readMatching(entry, "version", at, label, PROVIDER_VERSION),
435
+ digest: readMatching(entry, "digest", at, label, PROVIDER_DIGEST),
436
+ };
437
+ }
438
+ /**
439
+ * The provider list. Every file a run writes for an analyzer is keyed by the
440
+ * entry's name, so a list naming one name twice is refused at the later entry.
441
+ */
442
+ function readProviders(providers, label) {
443
+ const path = "providers.analyzers";
444
+ const value = member(providers, "analyzers");
445
+ if (value === undefined) {
446
+ return undefined;
447
+ }
448
+ if (!Array.isArray(value)) {
449
+ throw malformed(label, path, "is not an array");
450
+ }
451
+ const read = value.map((entry, index) => readProvider(entry, `${path}[${String(index)}]`, label));
452
+ const first = new Map();
453
+ read.forEach((entry, index) => {
454
+ const earlier = first.get(entry.name);
455
+ if (earlier !== undefined) {
456
+ throw malformed(label, `${path}[${String(index)}].name`, `${JSON.stringify(entry.name)} is already the name of ${path}[${String(earlier)}], ` +
457
+ "and every entry's name keys the files a run writes for it");
458
+ }
459
+ first.set(entry.name, index);
460
+ });
461
+ return read;
462
+ }
295
463
  /**
296
464
  * Reads one already-parsed configuration document as the settings it supplies,
297
465
  * refusing every constraint the closed key list declares that a parse cannot
@@ -308,6 +476,7 @@ function readDoc(value, label) {
308
476
  const consumers = section(value, "consumers", "consumers", label);
309
477
  const roots = section(value, "roots", "roots", label);
310
478
  const reporters = section(value, "reporters", "reporters", label);
479
+ const providers = section(value, "providers", "providers", label);
311
480
  const ts = section(value, "ts", "ts", label);
312
481
  section(value, "go", "go", label);
313
482
  section(value, "provenance", "provenance", label);
@@ -335,8 +504,12 @@ function readDoc(value, label) {
335
504
  cascade: readEnum(reporters, "cascade", "reporters.cascade", label, CASCADES),
336
505
  maxFindings: readCount(reporters, "max_findings", "reporters.max_findings", label),
337
506
  failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
507
+ providers: readProviders(providers, label),
338
508
  testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
339
509
  entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
510
+ injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
511
+ lifecycleContracts: readLifecycleContracts(ts, label),
512
+ serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
340
513
  };
341
514
  }
342
515
  /** Parses one document's text and walks it against the closed key list. */
@@ -535,9 +708,15 @@ function settleAll(inputs) {
535
708
  maxFindings: at("reporters.max_findings", defaults.reporters.maxFindings, (doc) => doc.maxFindings),
536
709
  failOn: at("reporters.fail_on", defaults.reporters.failOn, (doc) => doc.failOn),
537
710
  },
711
+ providers: {
712
+ analyzers: at("providers.analyzers", defaults.providers.analyzers, (doc) => doc.providers),
713
+ },
538
714
  ts: {
539
715
  testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
540
716
  entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
717
+ injectionRegistrations: at("ts.injection_registrations", defaults.ts.injectionRegistrations, (doc) => doc.injectionRegistrations),
718
+ lifecycleContracts: at("ts.lifecycle_contracts", defaults.ts.lifecycleContracts, (doc) => doc.lifecycleContracts),
719
+ serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
541
720
  },
542
721
  };
543
722
  return { config, provenance };
package/dist/src/run.js CHANGED
@@ -47,8 +47,14 @@ export const SETTING_OPTIONS = new Map([
47
47
  ]);
48
48
  /** The options that take a value and are not settings. */
49
49
  const PLAIN_OPTIONS = new Set(["target", "config", "central", "scope"]);
50
- /** The setting options whose value is a number rather than a string. */
51
- const NUMBER_OPTIONS = new Set(["max-findings"]);
50
+ /**
51
+ * The setting options whose value is an integer rather than a string. A value
52
+ * written other than as digits is passed on as the string it is, so resolution
53
+ * refuses it by its type rather than reading `1e2` as 100.
54
+ */
55
+ const INTEGER_OPTIONS = new Set(["max-findings"]);
56
+ /** An integer as an option writes it: an optional minus sign and digits alone. */
57
+ const INTEGER_TEXT = /^-?[0-9]+$/u;
52
58
  /**
53
59
  * The first option whose name carries a source-edit token, because every verb
54
60
  * reports and never edits. The match is on the name alone, so an option whose
@@ -116,7 +122,7 @@ function inputsOf(host, options) {
116
122
  if (value === undefined) {
117
123
  continue;
118
124
  }
119
- flat[path] = NUMBER_OPTIONS.has(name) ? Number(value) : value;
125
+ flat[path] = INTEGER_OPTIONS.has(name) && INTEGER_TEXT.test(value) ? Number(value) : value;
120
126
  flagLabels.set(path, `--${name}`);
121
127
  }
122
128
  const flags = Object.keys(flat).length === 0 ? undefined : JSON.stringify(flat);
@@ -1,5 +1,11 @@
1
1
  import { ConfigError } from "./config.js";
2
2
  const leaf = { kind: "leaf" };
3
+ const integer = { kind: "leaf", integer: true };
4
+ /** A list of declarations, each entry in one of the three shapes the `ts` section states. */
5
+ const declarations = {
6
+ kind: "list",
7
+ members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
8
+ };
3
9
  /**
4
10
  * The closed key list the Contract's configuration schema declares, derived from
5
11
  * that schema key by key. A test pins the two equal, so a Contract change that
@@ -39,12 +45,40 @@ export const SCHEMA_ROOT = {
39
45
  formats: leaf,
40
46
  sort: leaf,
41
47
  cascade: leaf,
42
- max_findings: leaf,
48
+ max_findings: integer,
43
49
  fail_on: leaf,
44
50
  },
45
51
  },
52
+ providers: {
53
+ kind: "section",
54
+ members: {
55
+ analyzers: {
56
+ kind: "list",
57
+ members: {
58
+ name: leaf,
59
+ languages: leaf,
60
+ command: leaf,
61
+ source: leaf,
62
+ version: leaf,
63
+ digest: leaf,
64
+ },
65
+ },
66
+ },
67
+ },
46
68
  go: { kind: "section", members: {} },
47
- ts: { kind: "section", members: { test_files: leaf, entry_files: leaf } },
69
+ ts: {
70
+ kind: "section",
71
+ members: {
72
+ test_files: leaf,
73
+ entry_files: leaf,
74
+ injection_registrations: declarations,
75
+ lifecycle_contracts: {
76
+ kind: "list",
77
+ members: { components: declarations, bases: declarations, members: leaf },
78
+ },
79
+ serializers: declarations,
80
+ },
81
+ },
48
82
  provenance: { kind: "map" },
49
83
  },
50
84
  };
@@ -227,6 +261,21 @@ function skipScalar(cursor) {
227
261
  cursor.at += 1;
228
262
  }
229
263
  }
264
+ /** A number the document writes as an optional minus sign and digits alone. */
265
+ const INTEGER_TEXT = /^-?[0-9]+$/u;
266
+ /** The start of a JSON number: a minus sign or a digit. */
267
+ const NUMBER_START = /^[-0-9]/u;
268
+ /**
269
+ * Refuses a number written with a fraction or an exponent where the key list
270
+ * declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
271
+ * the text can tell them apart. A scalar that is no number is the decoder's to
272
+ * refuse by its type.
273
+ */
274
+ function checkInteger(text, at, label) {
275
+ if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
276
+ throw malformed(label, at, `${text} is written with a fraction or an exponent, and an integer is digits alone`);
277
+ }
278
+ }
230
279
  function walkValue(cursor, at, node, label) {
231
280
  skipSpace(cursor);
232
281
  const ch = cursor.text.charCodeAt(cursor.at);
@@ -244,7 +293,11 @@ function walkValue(cursor, at, node, label) {
244
293
  readName(cursor, at, label);
245
294
  return;
246
295
  }
296
+ const start = cursor.at;
247
297
  skipScalar(cursor);
298
+ if (node.integer === true) {
299
+ checkInteger(cursor.text.slice(start, cursor.at), at, label);
300
+ }
248
301
  }
249
302
  function walkObject(cursor, at, node, label) {
250
303
  const seen = new Set();
package/dist/src/scope.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { ConfigError } from "./config.js";
1
2
  import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
3
+ import { checkDocument } from "./schema.js";
2
4
  /** The roles a scope document declares for a module. */
3
5
  export const ROLE_TARGET = "target";
4
6
  export const ROLE_CONSUMER = "consumer";
@@ -17,18 +19,37 @@ export class ScopeError extends Error {
17
19
  this.name = "ScopeError";
18
20
  }
19
21
  }
20
- const DECLARED_KEYS = new Set(["target", "workspace", "consumers"]);
21
- const MODULE_KEYS = new Set(["id", "role", "path"]);
22
+ const MODULE = {
23
+ kind: "section",
24
+ members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
25
+ };
26
+ /** The closed key list of the scope document, compared as bytes. */
27
+ const SCOPE_ROOT = {
28
+ kind: "section",
29
+ members: {
30
+ target: MODULE,
31
+ workspace: { kind: "leaf" },
32
+ consumers: { kind: "list", members: MODULE.members ?? {} },
33
+ },
34
+ };
22
35
  function isRecord(value) {
23
36
  return typeof value === "object" && value !== null && !Array.isArray(value);
24
37
  }
38
+ /**
39
+ * One string member, or undefined where the document omits it. A member the
40
+ * document writes is a string holding at least one character, so `null` and `""`
41
+ * are refused rather than read as absent.
42
+ */
25
43
  function stringMember(parent, name, at) {
26
44
  if (!Object.hasOwn(parent, name)) {
27
- return "";
45
+ return undefined;
28
46
  }
29
47
  const value = parent[name];
30
48
  if (typeof value !== "string") {
31
- throw new ScopeError(`scope: ${at}.${name} is not a string`);
49
+ throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
50
+ }
51
+ if (value === "") {
52
+ throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
32
53
  }
33
54
  return value;
34
55
  }
@@ -36,30 +57,46 @@ function readModule(value, at, role, documentDir) {
36
57
  if (!isRecord(value)) {
37
58
  throw new ScopeError(`scope: ${at} is not an object`);
38
59
  }
39
- for (const name of Object.keys(value)) {
40
- if (!MODULE_KEYS.has(name)) {
41
- throw new ScopeError(`scope: ${at}.${name} is not a declared key`);
42
- }
43
- }
44
60
  const declared = stringMember(value, "role", at);
45
- if (declared !== "" && declared !== role) {
61
+ if (declared !== undefined && declared !== role) {
46
62
  throw new ScopeError(`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`);
47
63
  }
48
64
  const path = stringMember(value, "path", at);
49
- if (path === "") {
65
+ if (path === undefined) {
50
66
  throw new ScopeError(`scope: ${at}.path names nothing`);
51
67
  }
52
68
  return {
53
- id: stringMember(value, "id", at),
69
+ id: stringMember(value, "id", at) ?? "",
54
70
  path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
55
71
  };
56
72
  }
57
73
  /**
58
- * Decodes the scope document at `file`. Decoding is strict: an undeclared key is an
59
- * error, and a relative path inside the document resolves against the document's
60
- * own directory.
74
+ * Walks the document's text against the closed key list, refusing a key the list
75
+ * does not declare, compared as bytes, and a key one object writes twice, which a
76
+ * parse cannot report because it keeps the last value.
77
+ */
78
+ function checkKeys(text, file) {
79
+ try {
80
+ checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
81
+ }
82
+ catch (error) {
83
+ if (error instanceof ConfigError) {
84
+ // The configuration's refusal names the nearest configuration key, which a
85
+ // scope document does not have.
86
+ throw new ScopeError(error.kind === "unimplemented-key"
87
+ ? `scope: ${file}: ${error.key} is not a declared key`
88
+ : error.message);
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+ /**
94
+ * Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
95
+ * key written twice in one object, and a value of a type the document does not
96
+ * admit, `null` included, are errors, and a relative path inside the document
97
+ * resolves against the document's own directory.
61
98
  *
62
- * The `workspace` key the format declares is read and carries nothing here: it
99
+ * The `workspace` key the format declares is checked and carries nothing here: it
63
100
  * names the file a Go consumer resolves the target through, and a TypeScript
64
101
  * project resolves one through its own compiler configuration.
65
102
  */
@@ -85,11 +122,8 @@ export function readScope(host, file) {
85
122
  if (!isRecord(value)) {
86
123
  throw new ScopeError(`scope: ${file} is not one JSON object`);
87
124
  }
88
- for (const name of Object.keys(value)) {
89
- if (!DECLARED_KEYS.has(name)) {
90
- throw new ScopeError(`scope: ${file}: ${name} is not a declared key`);
91
- }
92
- }
125
+ checkKeys(text, file);
126
+ stringMember(value, "workspace", "");
93
127
  const documentDir = dirnamePath(absolute);
94
128
  if (!Object.hasOwn(value, "target")) {
95
129
  throw new ScopeError(`scope: ${file} names no target`);