@withtruing/companion 0.13.2 → 0.14.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.
Files changed (57) hide show
  1. package/README.md +25 -26
  2. package/dist/anthropic-output-schema.js +41 -0
  3. package/dist/api-key-caller.js +9 -2
  4. package/dist/claude-cli-caller.js +107 -8
  5. package/dist/cli.js +837 -207
  6. package/dist/codex-binary.js +96 -0
  7. package/dist/codex-cli-caller.js +90 -16
  8. package/dist/config.js +177 -2
  9. package/dist/git-range.js +88 -46
  10. package/dist/http-client.js +95 -10
  11. package/dist/investigation-reader.js +12 -6
  12. package/dist/managed-repo.js +132 -0
  13. package/dist/observed-sources.js +132 -6
  14. package/dist/reader-result-contract.js +296 -2
  15. package/dist/repository-provider.js +608 -0
  16. package/dist/schema-entities.js +18 -18
  17. package/dist/service.js +7 -2
  18. package/dist/surface-mapper.js +53 -11
  19. package/dist/text-model.js +91 -8
  20. package/dist/whole-read/archive.js +196 -0
  21. package/dist/whole-read/checks.js +297 -0
  22. package/dist/whole-read/claims.js +196 -0
  23. package/dist/whole-read/coverage-ledger.js +51 -0
  24. package/dist/whole-read/decode.js +234 -0
  25. package/dist/whole-read/drafting.js +502 -0
  26. package/dist/whole-read/generated-rules.js +68 -0
  27. package/dist/whole-read/go-proof.js +255 -0
  28. package/dist/whole-read/grouping-prompt.js +19 -0
  29. package/dist/whole-read/grouping.js +247 -0
  30. package/dist/whole-read/identity.js +103 -0
  31. package/dist/whole-read/import-graph.js +293 -0
  32. package/dist/whole-read/inventory-collector.js +55 -0
  33. package/dist/whole-read/inventory-rules.js +141 -0
  34. package/dist/whole-read/lockfiles.js +947 -0
  35. package/dist/whole-read/notes.js +216 -0
  36. package/dist/whole-read/npm-proof.js +272 -0
  37. package/dist/whole-read/owner-decision-detectors.js +270 -0
  38. package/dist/whole-read/packing.js +612 -0
  39. package/dist/whole-read/pages.js +429 -0
  40. package/dist/whole-read/phase-policy.js +54 -0
  41. package/dist/whole-read/prd-build.js +279 -0
  42. package/dist/whole-read/prd-fidelity.js +14 -0
  43. package/dist/whole-read/python-proof.js +489 -0
  44. package/dist/whole-read/read-prompt.js +67 -0
  45. package/dist/whole-read/redaction.js +411 -0
  46. package/dist/whole-read/relationship-mapping.js +98 -0
  47. package/dist/whole-read/sanitize.js +266 -0
  48. package/dist/whole-read/secret-rules.js +26 -0
  49. package/dist/whole-read/structure.js +300 -0
  50. package/dist/whole-read/types.js +2 -0
  51. package/dist/whole-read-claims.js +12 -0
  52. package/dist/whole-read-git.js +340 -0
  53. package/dist/whole-read-pool.js +80 -0
  54. package/dist/whole-read-runner.js +945 -0
  55. package/dist/whole-read-shared-source.js +194 -0
  56. package/dist/whole-read-temp.js +479 -0
  57. package/package.json +8 -2
@@ -0,0 +1,947 @@
1
+ import { createRequire } from 'node:module';
2
+ import { parse as parseJsonc, visit } from 'jsonc-parser';
3
+ import { parse as parseToml } from 'smol-toml';
4
+ import { parseDocument } from 'yaml';
5
+ const require = createRequire(import.meta.url);
6
+ const yarnLockfile = require('@yarnpkg/lockfile');
7
+ export const B14_LOCKFILE_NAMES = Object.freeze([
8
+ 'package-lock.json',
9
+ 'npm-shrinkwrap.json',
10
+ 'pnpm-lock.yaml',
11
+ 'yarn.lock',
12
+ 'bun.lock',
13
+ ]);
14
+ export const B15_LOCKFILE_NAMES = Object.freeze([
15
+ 'Cargo.lock',
16
+ 'poetry.lock',
17
+ 'uv.lock',
18
+ 'Pipfile.lock',
19
+ 'go.sum',
20
+ 'Gemfile.lock',
21
+ 'composer.lock',
22
+ ]);
23
+ export const LOCKFILE_OWNERS = Object.freeze({
24
+ 'package-lock.json': 'B14',
25
+ 'npm-shrinkwrap.json': 'B14',
26
+ 'pnpm-lock.yaml': 'B14',
27
+ 'yarn.lock': 'B14',
28
+ 'bun.lock': 'B14',
29
+ 'Cargo.lock': 'B15',
30
+ 'poetry.lock': 'B15',
31
+ 'uv.lock': 'B15',
32
+ 'Pipfile.lock': 'B15',
33
+ 'go.sum': 'B15',
34
+ 'Gemfile.lock': 'B15',
35
+ 'composer.lock': 'B15',
36
+ });
37
+ const RULES_VERSION = 'inventory-rules@1';
38
+ const NPM_PARSER_VERSION = 'npm-lockfile@1';
39
+ const PNPM_PARSER_VERSION = 'pnpm-lockfile@1';
40
+ const YARN_CLASSIC_PARSER_VERSION = '@yarnpkg/lockfile@1.1.0';
41
+ const YARN_BERRY_PARSER_VERSION = 'yaml@2.8.2';
42
+ const BUN_PARSER_VERSION = 'jsonc-parser@3.3.1';
43
+ const TOML_PARSER_VERSION = 'smol-toml@1.9.0';
44
+ const GO_SUM_PARSER_VERSION = 'go-sum@1';
45
+ const GEMFILE_LOCK_PARSER_VERSION = 'gemfile-lock@1';
46
+ const PNPM_FORMAT_VERSIONS = new Set(['5.0', '5.1', '5.2', '5.3', '5.4', '6.0', '9.0']);
47
+ const YARN_BERRY_FORMAT_VERSIONS = new Set(['4', '5', '6', '7', '8']);
48
+ const BUN_FORMAT_VERSIONS = new Set(['0', '1']);
49
+ const NPM_PACKAGE_NAME = /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/;
50
+ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
51
+ const BOOLEAN_FIELDS = ['dev', 'optional', 'bundled', 'inBundle', 'hasInstallScript', 'link'];
52
+ const hasOwn = (record, key) => Object.hasOwn(record, key);
53
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
54
+ const nonBlankString = (value) => typeof value === 'string' && value.trim().length > 0;
55
+ function finalPathComponent(path) {
56
+ if (path.length === 0 || path.includes('\0'))
57
+ return null;
58
+ const component = path.slice(path.lastIndexOf('/') + 1);
59
+ return component.length === 0 ? null : component;
60
+ }
61
+ function ownedName(path) {
62
+ const filename = finalPathComponent(path);
63
+ return filename !== null && Object.hasOwn(LOCKFILE_OWNERS, filename)
64
+ ? filename
65
+ : null;
66
+ }
67
+ function hasDuplicateJsonKeys(text) {
68
+ let offset = 0;
69
+ const whitespace = () => {
70
+ while (offset < text.length && /[\t\n\r ]/.test(text[offset]))
71
+ offset += 1;
72
+ };
73
+ const string = () => {
74
+ if (text[offset] !== '"')
75
+ return null;
76
+ const start = offset;
77
+ offset += 1;
78
+ while (offset < text.length) {
79
+ const character = text[offset];
80
+ if (character === '\\') {
81
+ offset += 2;
82
+ }
83
+ else if (character === '"') {
84
+ offset += 1;
85
+ try {
86
+ return JSON.parse(text.slice(start, offset));
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ else {
93
+ offset += 1;
94
+ }
95
+ }
96
+ return null;
97
+ };
98
+ const primitive = () => {
99
+ const start = offset;
100
+ while (offset < text.length && !/[\t\n\r ,\]}]/.test(text[offset]))
101
+ offset += 1;
102
+ return offset > start;
103
+ };
104
+ const value = () => {
105
+ whitespace();
106
+ if (text[offset] === '{')
107
+ return object();
108
+ if (text[offset] === '[')
109
+ return array();
110
+ if (text[offset] === '"')
111
+ return string() !== null;
112
+ return primitive();
113
+ };
114
+ const object = () => {
115
+ if (text[offset] !== '{')
116
+ return false;
117
+ offset += 1;
118
+ whitespace();
119
+ const keys = new Set();
120
+ if (text[offset] === '}') {
121
+ offset += 1;
122
+ return true;
123
+ }
124
+ while (offset < text.length) {
125
+ whitespace();
126
+ const key = string();
127
+ if (key === null || keys.has(key))
128
+ return false;
129
+ keys.add(key);
130
+ whitespace();
131
+ if (text[offset] !== ':')
132
+ return false;
133
+ offset += 1;
134
+ if (!value())
135
+ return false;
136
+ whitespace();
137
+ if (text[offset] === '}') {
138
+ offset += 1;
139
+ return true;
140
+ }
141
+ if (text[offset] !== ',')
142
+ return false;
143
+ offset += 1;
144
+ }
145
+ return false;
146
+ };
147
+ const array = () => {
148
+ if (text[offset] !== '[')
149
+ return false;
150
+ offset += 1;
151
+ whitespace();
152
+ if (text[offset] === ']') {
153
+ offset += 1;
154
+ return true;
155
+ }
156
+ while (offset < text.length) {
157
+ if (!value())
158
+ return false;
159
+ whitespace();
160
+ if (text[offset] === ']') {
161
+ offset += 1;
162
+ return true;
163
+ }
164
+ if (text[offset] !== ',')
165
+ return false;
166
+ offset += 1;
167
+ }
168
+ return false;
169
+ };
170
+ return !value() || (whitespace(), offset !== text.length);
171
+ }
172
+ function stringMap(value) {
173
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'string');
174
+ }
175
+ function stringArray(value) {
176
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
177
+ }
178
+ function safePackagePath(path) {
179
+ return path === '' || (!path.startsWith('/') && !path.endsWith('/') && !path.includes('\\')
180
+ && path.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..'));
181
+ }
182
+ function packageNameFromPath(path) {
183
+ const parts = path.split('/');
184
+ const nodeModules = parts.lastIndexOf('node_modules');
185
+ if (nodeModules < 0 || nodeModules === parts.length - 1)
186
+ return null;
187
+ const tail = parts.slice(nodeModules + 1);
188
+ const name = tail[0].startsWith('@') && tail.length === 2
189
+ ? `${tail[0]}/${tail[1]}`
190
+ : tail.length === 1 ? tail[0] : '';
191
+ return NPM_PACKAGE_NAME.test(name) ? name : null;
192
+ }
193
+ function validPackageRecord(value, requireVersion) {
194
+ if (!isRecord(value))
195
+ return false;
196
+ if (hasOwn(value, 'version') && !nonBlankString(value.version))
197
+ return false;
198
+ if (requireVersion && !nonBlankString(value.version)
199
+ && !(value.link === true && nonBlankString(value.resolved)))
200
+ return false;
201
+ if (hasOwn(value, 'resolved') && !nonBlankString(value.resolved))
202
+ return false;
203
+ if (hasOwn(value, 'link') && typeof value.link !== 'boolean')
204
+ return false;
205
+ if (hasOwn(value, 'bin') && !(typeof value.bin === 'string' || stringMap(value.bin)))
206
+ return false;
207
+ if (hasOwn(value, 'engines') && !stringMap(value.engines) && !stringArray(value.engines))
208
+ return false;
209
+ for (const field of DEPENDENCY_FIELDS) {
210
+ if (hasOwn(value, field) && !stringMap(value[field]))
211
+ return false;
212
+ }
213
+ for (const field of BOOLEAN_FIELDS) {
214
+ if (hasOwn(value, field) && typeof value[field] !== 'boolean')
215
+ return false;
216
+ }
217
+ return true;
218
+ }
219
+ function validDependencyRecords(value, dependencies) {
220
+ if (!isRecord(value))
221
+ return false;
222
+ for (const [name, record] of Object.entries(value)) {
223
+ if (!NPM_PACKAGE_NAME.test(name) || !isRecord(record) || !nonBlankString(record.version))
224
+ return false;
225
+ if (hasOwn(record, 'requires') && !stringMap(record.requires))
226
+ return false;
227
+ if (hasOwn(record, 'dependencies') && !validDependencyRecords(record.dependencies, dependencies))
228
+ return false;
229
+ for (const field of ['devDependencies', 'optionalDependencies', 'peerDependencies']) {
230
+ if (hasOwn(record, field) && !stringMap(record[field]))
231
+ return false;
232
+ }
233
+ for (const field of BOOLEAN_FIELDS) {
234
+ if (hasOwn(record, field) && typeof record[field] !== 'boolean')
235
+ return false;
236
+ }
237
+ dependencies.push({ ecosystem: 'npm', name, version: record.version });
238
+ }
239
+ return true;
240
+ }
241
+ function normalizedDependencies(dependencies) {
242
+ const unique = new Map();
243
+ for (const dependency of dependencies)
244
+ unique.set(`${dependency.name}\0${dependency.version}`, dependency);
245
+ return [...unique.values()].sort((left, right) => left.name === right.name ? left.version.localeCompare(right.version) : left.name.localeCompare(right.name));
246
+ }
247
+ function parseNpmLockfile(filename, text) {
248
+ let parsed;
249
+ try {
250
+ parsed = JSON.parse(text);
251
+ }
252
+ catch {
253
+ return { kind: 'read', reason: 'npm_lockfile_invalid_json' };
254
+ }
255
+ try {
256
+ return validateNpmLockfile(filename, text, parsed);
257
+ }
258
+ catch {
259
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
260
+ }
261
+ }
262
+ function validateNpmLockfile(filename, text, parsed) {
263
+ if (hasDuplicateJsonKeys(text))
264
+ return { kind: 'read', reason: 'npm_lockfile_duplicate_key' };
265
+ if (!isRecord(parsed) || !Number.isInteger(parsed.lockfileVersion)) {
266
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
267
+ }
268
+ const formatVersion = String(parsed.lockfileVersion);
269
+ if (formatVersion !== '1' && formatVersion !== '2' && formatVersion !== '3') {
270
+ return { kind: 'read', reason: 'npm_lockfile_unsupported_version' };
271
+ }
272
+ if (hasOwn(parsed, 'name') && !nonBlankString(parsed.name))
273
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
274
+ if (hasOwn(parsed, 'version') && !nonBlankString(parsed.version))
275
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
276
+ if (hasOwn(parsed, 'requires') && typeof parsed.requires !== 'boolean')
277
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
278
+ const dependencies = [];
279
+ if (formatVersion === '1') {
280
+ if (!validDependencyRecords(parsed.dependencies, dependencies)) {
281
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
282
+ }
283
+ }
284
+ else {
285
+ if (!isRecord(parsed.packages) || !hasOwn(parsed.packages, '') || !validPackageRecord(parsed.packages[''], false)) {
286
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
287
+ }
288
+ for (const [path, record] of Object.entries(parsed.packages)) {
289
+ if (!safePackagePath(path) || (path.split('/').includes('node_modules') && packageNameFromPath(path) === null)
290
+ || !validPackageRecord(record, path !== '')) {
291
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
292
+ }
293
+ const name = packageNameFromPath(path);
294
+ if (name !== null && nonBlankString(record.version)) {
295
+ dependencies.push({ ecosystem: 'npm', name, version: record.version });
296
+ }
297
+ }
298
+ if (hasOwn(parsed, 'dependencies') && !validDependencyRecords(parsed.dependencies, dependencies)) {
299
+ return { kind: 'read', reason: 'npm_lockfile_invalid_shape' };
300
+ }
301
+ }
302
+ return {
303
+ kind: 'lockfile',
304
+ proof: {
305
+ dependencies: normalizedDependencies(dependencies),
306
+ filename,
307
+ format: 'npm',
308
+ formatVersion,
309
+ parserVersion: NPM_PARSER_VERSION,
310
+ rulesVersion: RULES_VERSION,
311
+ },
312
+ };
313
+ }
314
+ function yamlRecord(text) {
315
+ if (/^%|(?:^|[\s:[{,])(?:!|&|\*)[A-Za-z!]/m.test(text))
316
+ return null;
317
+ try {
318
+ const document = parseDocument(text, {
319
+ merge: false,
320
+ prettyErrors: false,
321
+ schema: 'core',
322
+ strict: true,
323
+ uniqueKeys: true,
324
+ });
325
+ if (document.errors.length !== 0 || document.warnings.length !== 0)
326
+ return null;
327
+ const parsed = document.toJS({ maxAliasCount: 0 });
328
+ return isRecord(parsed) ? parsed : null;
329
+ }
330
+ catch {
331
+ return null;
332
+ }
333
+ }
334
+ function hasOnly(record, fields) {
335
+ return Object.keys(record).every((key) => fields.includes(key));
336
+ }
337
+ function stringRecord(value) {
338
+ return isRecord(value) && Object.values(value).every(nonBlankString);
339
+ }
340
+ function recordOfRecords(value) {
341
+ return isRecord(value) && Object.values(value).every(isRecord);
342
+ }
343
+ function stringList(value) {
344
+ return Array.isArray(value) && value.every(nonBlankString);
345
+ }
346
+ function pnpmVersion(value) {
347
+ if (typeof value === 'string' && PNPM_FORMAT_VERSIONS.has(value))
348
+ return value;
349
+ if (typeof value === 'number') {
350
+ if (value === 5)
351
+ return '5.0';
352
+ if (value === 6)
353
+ return '6.0';
354
+ if (value === 9)
355
+ return '9.0';
356
+ const normalized = String(value);
357
+ return PNPM_FORMAT_VERSIONS.has(normalized) ? normalized : null;
358
+ }
359
+ return null;
360
+ }
361
+ function validPnpmResolution(value) {
362
+ return isRecord(value) && Object.values(value).some(nonBlankString)
363
+ && Object.values(value).every((entry) => nonBlankString(entry) || typeof entry === 'boolean' || Number.isFinite(entry));
364
+ }
365
+ function validPnpmPackageRecord(value) {
366
+ if (!isRecord(value) || !hasOwn(value, 'resolution') || !validPnpmResolution(value.resolution))
367
+ return false;
368
+ if (!hasOnly(value, [
369
+ 'resolution', 'dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta',
370
+ 'engines', 'cpu', 'os', 'libc', 'dev', 'optional', 'hasBin', 'requiresBuild', 'deprecated', 'id',
371
+ 'bundledDependencies', 'prepare', 'requires',
372
+ ]))
373
+ return false;
374
+ for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies', 'engines']) {
375
+ if (hasOwn(value, field) && !stringRecord(value[field]))
376
+ return false;
377
+ }
378
+ for (const field of ['peerDependenciesMeta']) {
379
+ if (hasOwn(value, field) && !recordOfRecords(value[field]))
380
+ return false;
381
+ }
382
+ for (const field of ['cpu', 'os', 'libc', 'bundledDependencies']) {
383
+ if (hasOwn(value, field) && !stringList(value[field]))
384
+ return false;
385
+ }
386
+ for (const field of ['dev', 'optional', 'hasBin', 'requiresBuild']) {
387
+ if (hasOwn(value, field) && typeof value[field] !== 'boolean')
388
+ return false;
389
+ }
390
+ for (const field of ['deprecated', 'id', 'prepare', 'requires']) {
391
+ if (hasOwn(value, field) && !nonBlankString(value[field]))
392
+ return false;
393
+ }
394
+ return true;
395
+ }
396
+ function validPnpmSnapshot(value) {
397
+ if (!isRecord(value) || !hasOnly(value, [
398
+ 'dependencies', 'optionalDependencies', 'peerDependencies', 'transitivePeerDependencies', 'optional',
399
+ ]))
400
+ return false;
401
+ for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
402
+ if (hasOwn(value, field) && !stringRecord(value[field]))
403
+ return false;
404
+ }
405
+ return (!hasOwn(value, 'transitivePeerDependencies') || stringList(value.transitivePeerDependencies))
406
+ && (!hasOwn(value, 'optional') || typeof value.optional === 'boolean');
407
+ }
408
+ function validPnpmMetadata(parsed) {
409
+ if (hasOwn(parsed, 'settings') && (!isRecord(parsed.settings)
410
+ || !Object.values(parsed.settings).every((value) => typeof value === 'boolean' || Number.isFinite(value))))
411
+ return false;
412
+ if (hasOwn(parsed, 'neverBuiltDependencies') && !stringList(parsed.neverBuiltDependencies))
413
+ return false;
414
+ if (hasOwn(parsed, 'onlyBuiltDependencies') && !stringList(parsed.onlyBuiltDependencies))
415
+ return false;
416
+ if (hasOwn(parsed, 'overrides') && !stringRecord(parsed.overrides))
417
+ return false;
418
+ if (hasOwn(parsed, 'packageExtensionsChecksum') && !nonBlankString(parsed.packageExtensionsChecksum))
419
+ return false;
420
+ if (hasOwn(parsed, 'patchedDependencies') && !stringRecord(parsed.patchedDependencies))
421
+ return false;
422
+ return !hasOwn(parsed, 'time') || (isRecord(parsed.time) && Object.values(parsed.time).every(Number.isFinite));
423
+ }
424
+ function pnpmPackageIdentity(key, formatVersion) {
425
+ let name = '';
426
+ let version = '';
427
+ if (formatVersion.startsWith('5.')) {
428
+ const match = /^\/((?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*)\/([^/]+)$/.exec(key);
429
+ if (match === null)
430
+ return null;
431
+ [, name, version] = match;
432
+ }
433
+ else {
434
+ const source = formatVersion === '6.0' ? key.slice(1) : key;
435
+ const separator = source.startsWith('@') ? source.indexOf('@', source.indexOf('/') + 1) : source.indexOf('@');
436
+ if (separator < 1)
437
+ return null;
438
+ name = source.slice(0, separator);
439
+ version = source.slice(separator + 1);
440
+ }
441
+ return NPM_PACKAGE_NAME.test(name) && nonBlankString(version) && !/^(?:file:|link:|workspace:|https?:)/.test(version)
442
+ ? { ecosystem: 'npm', name, version: version.replace(/\([^)]*\)$/, '') }
443
+ : null;
444
+ }
445
+ function parsePnpmLockfile(text) {
446
+ const parsed = yamlRecord(text);
447
+ if (parsed === null)
448
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_yaml' };
449
+ const formatVersion = pnpmVersion(parsed.lockfileVersion);
450
+ if (formatVersion === null)
451
+ return { kind: 'read', reason: 'pnpm_lockfile_unsupported_version' };
452
+ if (!hasOnly(parsed, [
453
+ 'lockfileVersion', 'settings', 'neverBuiltDependencies', 'onlyBuiltDependencies', 'overrides',
454
+ 'packageExtensionsChecksum', 'patchedDependencies', 'importers', 'packages', 'snapshots', 'time',
455
+ ]) || !validPnpmMetadata(parsed) || !isRecord(parsed.importers) || !isRecord(parsed.packages) || Object.keys(parsed.importers).length === 0) {
456
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
457
+ }
458
+ if ((formatVersion === '9.0' && !isRecord(parsed.snapshots)) || (formatVersion !== '9.0' && hasOwn(parsed, 'snapshots'))) {
459
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
460
+ }
461
+ const v5 = formatVersion.startsWith('5.');
462
+ for (const [path, importer] of Object.entries(parsed.importers)) {
463
+ if (!nonBlankString(path) || !isRecord(importer))
464
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
465
+ const dependencyFields = ['dependencies', 'devDependencies', 'optionalDependencies'];
466
+ if (v5) {
467
+ if (!hasOnly(importer, ['specifiers', ...dependencyFields, 'dependenciesMeta']) || !stringRecord(importer.specifiers)) {
468
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
469
+ }
470
+ if (hasOwn(importer, 'dependenciesMeta') && !recordOfRecords(importer.dependenciesMeta))
471
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
472
+ if (dependencyFields.some((field) => hasOwn(importer, field) && !stringRecord(importer[field])))
473
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
474
+ }
475
+ else {
476
+ if (!hasOnly(importer, [...dependencyFields, 'dependenciesMeta', 'publishDirectory'])
477
+ || (hasOwn(importer, 'publishDirectory') && !nonBlankString(importer.publishDirectory))
478
+ || (hasOwn(importer, 'dependenciesMeta') && !recordOfRecords(importer.dependenciesMeta)))
479
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
480
+ for (const field of dependencyFields) {
481
+ if (!hasOwn(importer, field))
482
+ continue;
483
+ if (!isRecord(importer[field]) || !Object.values(importer[field]).every((entry) => isRecord(entry)
484
+ && hasOnly(entry, ['specifier', 'version']) && nonBlankString(entry.specifier) && nonBlankString(entry.version))) {
485
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
486
+ }
487
+ }
488
+ }
489
+ }
490
+ const dependencies = [];
491
+ for (const [key, record] of Object.entries(parsed.packages)) {
492
+ if (!validPnpmPackageRecord(record))
493
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
494
+ const identity = pnpmPackageIdentity(key, formatVersion);
495
+ if (identity === null)
496
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
497
+ dependencies.push(identity);
498
+ }
499
+ if (formatVersion === '9.0' && !Object.values(parsed.snapshots).every(validPnpmSnapshot)) {
500
+ return { kind: 'read', reason: 'pnpm_lockfile_invalid_shape' };
501
+ }
502
+ return { kind: 'lockfile', proof: {
503
+ dependencies: normalizedDependencies(dependencies), filename: 'pnpm-lock.yaml', format: 'pnpm', formatVersion,
504
+ parserVersion: PNPM_PARSER_VERSION, rulesVersion: RULES_VERSION,
505
+ } };
506
+ }
507
+ function yarnDescriptorIdentity(descriptor, version) {
508
+ const selector = descriptor.trim();
509
+ const separator = selector.startsWith('@') ? selector.indexOf('@', selector.indexOf('/') + 1) : selector.indexOf('@');
510
+ if (separator < 1 || /^(?:file:|link:|portal:|workspace:|https?:|git:|github:|patch:)/.test(selector.slice(separator + 1)))
511
+ return null;
512
+ const name = selector.slice(0, separator);
513
+ return NPM_PACKAGE_NAME.test(name) && nonBlankString(version) ? { ecosystem: 'npm', name, version } : null;
514
+ }
515
+ function validYarnClassicEntry(value) {
516
+ if (!isRecord(value) || !nonBlankString(value.version) || !hasOnly(value, [
517
+ 'version', 'resolved', 'integrity', 'uid', 'registry', 'dependencies', 'optionalDependencies', 'peerDependencies',
518
+ ]))
519
+ return false;
520
+ for (const field of ['resolved', 'integrity', 'uid', 'registry'])
521
+ if (hasOwn(value, field) && !nonBlankString(value[field]))
522
+ return false;
523
+ for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies'])
524
+ if (hasOwn(value, field) && !stringRecord(value[field]))
525
+ return false;
526
+ return true;
527
+ }
528
+ function parseYarnClassic(text) {
529
+ try {
530
+ const parsed = yarnLockfile.parse(text);
531
+ if (parsed.type !== 'success' || !isRecord(parsed.object) || Object.keys(parsed.object).length === 0) {
532
+ return { kind: 'read', reason: 'yarn_classic_invalid_grammar' };
533
+ }
534
+ const dependencies = [];
535
+ for (const [selectors, entry] of Object.entries(parsed.object)) {
536
+ if (!validYarnClassicEntry(entry))
537
+ return { kind: 'read', reason: 'yarn_classic_invalid_shape' };
538
+ for (const descriptor of selectors.split(/,\s*/)) {
539
+ const identity = yarnDescriptorIdentity(descriptor, entry.version);
540
+ if (identity !== null)
541
+ dependencies.push(identity);
542
+ }
543
+ }
544
+ return { kind: 'lockfile', proof: {
545
+ dependencies: normalizedDependencies(dependencies), filename: 'yarn.lock', format: 'yarn-classic', formatVersion: '1',
546
+ parserVersion: YARN_CLASSIC_PARSER_VERSION, rulesVersion: RULES_VERSION,
547
+ } };
548
+ }
549
+ catch {
550
+ return { kind: 'read', reason: 'yarn_classic_invalid_grammar' };
551
+ }
552
+ }
553
+ function validYarnBerryEntry(value) {
554
+ if (!isRecord(value) || !nonBlankString(value.version) || !nonBlankString(value.resolution) || !hasOnly(value, [
555
+ 'version', 'resolution', 'dependencies', 'optionalDependencies', 'peerDependencies', 'dependenciesMeta',
556
+ 'peerDependenciesMeta', 'bin', 'languageName', 'linkType', 'checksum', 'conditions',
557
+ ]))
558
+ return false;
559
+ for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies', 'bin'])
560
+ if (hasOwn(value, field) && !stringRecord(value[field]))
561
+ return false;
562
+ for (const field of ['dependenciesMeta', 'peerDependenciesMeta'])
563
+ if (hasOwn(value, field) && !recordOfRecords(value[field]))
564
+ return false;
565
+ for (const field of ['languageName', 'checksum', 'conditions'])
566
+ if (hasOwn(value, field) && !nonBlankString(value[field]))
567
+ return false;
568
+ return !hasOwn(value, 'linkType') || value.linkType === 'hard' || value.linkType === 'soft';
569
+ }
570
+ function parseYarnBerry(parsed) {
571
+ const metadata = parsed.__metadata;
572
+ if (!isRecord(metadata) || !hasOnly(metadata, ['version', 'cacheKey']) || !Number.isInteger(metadata.version)
573
+ || !YARN_BERRY_FORMAT_VERSIONS.has(String(metadata.version)) || (hasOwn(metadata, 'cacheKey') && !nonBlankString(metadata.cacheKey))) {
574
+ return { kind: 'read', reason: 'yarn_berry_unsupported_version' };
575
+ }
576
+ const dependencies = [];
577
+ for (const [selectors, entry] of Object.entries(parsed)) {
578
+ if (selectors === '__metadata')
579
+ continue;
580
+ if (!validYarnBerryEntry(entry))
581
+ return { kind: 'read', reason: 'yarn_berry_invalid_shape' };
582
+ for (const descriptor of selectors.split(/,\s*/)) {
583
+ const identity = yarnDescriptorIdentity(descriptor, entry.version);
584
+ if (identity !== null)
585
+ dependencies.push(identity);
586
+ }
587
+ }
588
+ return { kind: 'lockfile', proof: {
589
+ dependencies: normalizedDependencies(dependencies), filename: 'yarn.lock', format: 'yarn-berry', formatVersion: String(metadata.version),
590
+ parserVersion: YARN_BERRY_PARSER_VERSION, rulesVersion: RULES_VERSION,
591
+ } };
592
+ }
593
+ function parseYarnLockfile(text) {
594
+ const yaml = yamlRecord(text);
595
+ return yaml !== null && hasOwn(yaml, '__metadata') ? parseYarnBerry(yaml) : parseYarnClassic(text);
596
+ }
597
+ function validBunStringRecord(value) {
598
+ return isRecord(value) && Object.values(value).every(nonBlankString);
599
+ }
600
+ function validBunWorkspace(value) {
601
+ if (!isRecord(value) || !hasOnly(value, [
602
+ 'name', 'version', 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies',
603
+ ]))
604
+ return false;
605
+ if (hasOwn(value, 'name') && !nonBlankString(value.name))
606
+ return false;
607
+ if (hasOwn(value, 'version') && !nonBlankString(value.version))
608
+ return false;
609
+ return ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
610
+ .every((field) => !hasOwn(value, field) || validBunStringRecord(value[field]));
611
+ }
612
+ function bunPackageIdentity(value) {
613
+ const separator = value.startsWith('@') ? value.indexOf('@', value.indexOf('/') + 1) : value.indexOf('@');
614
+ if (separator < 1)
615
+ return null;
616
+ const name = value.slice(0, separator);
617
+ const version = value.slice(separator + 1);
618
+ return NPM_PACKAGE_NAME.test(name) && nonBlankString(version) && !/[\s/]/.test(version)
619
+ && !/^(?:file:|git:|github:|https?:|npm:|workspace:)/.test(version)
620
+ ? { ecosystem: 'npm', name, version }
621
+ : null;
622
+ }
623
+ function parseBunLockfile(text) {
624
+ const errors = [];
625
+ let duplicateKey = false;
626
+ let syntaxError = false;
627
+ const objectKeys = [];
628
+ const parsed = parseJsonc(text, errors, { allowTrailingComma: true, disallowComments: false });
629
+ visit(text, {
630
+ onObjectBegin: () => { objectKeys.push(new Set()); },
631
+ onObjectProperty: (property) => {
632
+ const keys = objectKeys.at(-1);
633
+ if (keys === undefined || keys.has(property))
634
+ duplicateKey = true;
635
+ else
636
+ keys.add(property);
637
+ },
638
+ onObjectEnd: () => { objectKeys.pop(); },
639
+ onError: () => { syntaxError = true; },
640
+ }, { allowTrailingComma: true, disallowComments: false });
641
+ if (errors.length !== 0 || syntaxError)
642
+ return { kind: 'read', reason: 'bun_lockfile_invalid_jsonc' };
643
+ if (duplicateKey)
644
+ return { kind: 'read', reason: 'bun_lockfile_duplicate_key' };
645
+ if (!isRecord(parsed) || !Number.isInteger(parsed.lockfileVersion)) {
646
+ return { kind: 'read', reason: 'bun_lockfile_invalid_shape' };
647
+ }
648
+ const formatVersion = String(parsed.lockfileVersion);
649
+ if (!BUN_FORMAT_VERSIONS.has(formatVersion))
650
+ return { kind: 'read', reason: 'bun_lockfile_unsupported_version' };
651
+ if (!hasOnly(parsed, ['lockfileVersion', 'workspaces', 'packages'])
652
+ || !isRecord(parsed.workspaces) || !isRecord(parsed.packages) || !hasOwn(parsed.workspaces, '')) {
653
+ return { kind: 'read', reason: 'bun_lockfile_invalid_shape' };
654
+ }
655
+ for (const [path, workspace] of Object.entries(parsed.workspaces)) {
656
+ if ((path !== '' && (!path || path.startsWith('/') || path.includes('\\') || path.includes('\0'))) || !validBunWorkspace(workspace)) {
657
+ return { kind: 'read', reason: 'bun_lockfile_invalid_shape' };
658
+ }
659
+ }
660
+ const dependencies = [];
661
+ const identities = new Set();
662
+ for (const [name, entry] of Object.entries(parsed.packages)) {
663
+ if (!NPM_PACKAGE_NAME.test(name) || !Array.isArray(entry) || entry.length !== 4
664
+ || !nonBlankString(entry[0]) || typeof entry[1] !== 'string' || !validBunStringRecord(entry[2]) || typeof entry[3] !== 'string') {
665
+ return { kind: 'read', reason: 'bun_lockfile_invalid_shape' };
666
+ }
667
+ const identity = bunPackageIdentity(entry[0]);
668
+ if (identity === null)
669
+ return { kind: 'read', reason: 'bun_lockfile_invalid_shape' };
670
+ const key = `${identity.name}\0${identity.version}`;
671
+ if (identities.has(key))
672
+ return { kind: 'read', reason: 'bun_lockfile_conflicting_identity' };
673
+ identities.add(key);
674
+ dependencies.push(identity);
675
+ }
676
+ return { kind: 'lockfile', proof: {
677
+ dependencies: normalizedDependencies(dependencies), filename: 'bun.lock', format: 'bun', formatVersion,
678
+ parserVersion: BUN_PARSER_VERSION, rulesVersion: RULES_VERSION,
679
+ } };
680
+ }
681
+ function tomlRecord(text) {
682
+ try {
683
+ const parsed = parseToml(text, { unsafeKeyBehaviour: 'throw' });
684
+ return isRecord(parsed) ? parsed : null;
685
+ }
686
+ catch {
687
+ return null;
688
+ }
689
+ }
690
+ function nonBlankStringArray(value) {
691
+ return Array.isArray(value) && value.every(nonBlankString);
692
+ }
693
+ function packageArray(value, validator) {
694
+ return Array.isArray(value) && value.length > 0 && value.every((entry) => isRecord(entry) && validator(entry));
695
+ }
696
+ function proof(filename, format, formatVersion, parserVersion, dependencies) {
697
+ return {
698
+ kind: 'lockfile',
699
+ proof: { filename, format, formatVersion, parserVersion, rulesVersion: RULES_VERSION, dependencies: normalizedDependencies(dependencies) },
700
+ };
701
+ }
702
+ function parseCargoLockfile(text) {
703
+ const parsed = tomlRecord(text);
704
+ if (parsed === null)
705
+ return { kind: 'read', reason: 'cargo_lockfile_invalid_toml' };
706
+ if (!Number.isInteger(parsed.version))
707
+ return { kind: 'read', reason: 'cargo_lockfile_invalid_shape' };
708
+ const formatVersion = String(parsed.version);
709
+ if (!new Set(['1', '2', '3', '4']).has(formatVersion))
710
+ return { kind: 'read', reason: 'cargo_lockfile_unsupported_version' };
711
+ if (!packageArray(parsed.package, (entry) => nonBlankString(entry.name) && nonBlankString(entry.version)
712
+ && (!hasOwn(entry, 'source') || nonBlankString(entry.source))
713
+ && (!hasOwn(entry, 'checksum') || nonBlankString(entry.checksum))
714
+ && (!hasOwn(entry, 'dependencies') || nonBlankStringArray(entry.dependencies)))) {
715
+ return { kind: 'read', reason: 'cargo_lockfile_invalid_shape' };
716
+ }
717
+ return proof('Cargo.lock', 'cargo', formatVersion, TOML_PARSER_VERSION, parsed.package.map((entry) => ({ ecosystem: 'cargo', name: entry.name, version: entry.version })));
718
+ }
719
+ const POETRY_FORMAT_VERSIONS = new Set(['1.1', '1.2', '2.0', '2.1', '2.2']);
720
+ function parsePoetryLockfile(text) {
721
+ const parsed = tomlRecord(text);
722
+ if (parsed === null)
723
+ return { kind: 'read', reason: 'poetry_lockfile_invalid_toml' };
724
+ if (!isRecord(parsed.metadata) || !nonBlankString(parsed.metadata['lock-version']) || !nonBlankString(parsed.metadata['content-hash'])) {
725
+ return { kind: 'read', reason: 'poetry_lockfile_invalid_shape' };
726
+ }
727
+ const formatVersion = parsed.metadata['lock-version'];
728
+ if (!POETRY_FORMAT_VERSIONS.has(formatVersion))
729
+ return { kind: 'read', reason: 'poetry_lockfile_unsupported_version' };
730
+ if (!packageArray(parsed.package, (entry) => nonBlankString(entry.name) && nonBlankString(entry.version)
731
+ && nonBlankString(entry.description) && typeof entry.optional === 'boolean' && nonBlankString(entry['python-versions'])
732
+ && (!hasOwn(entry, 'groups') || nonBlankStringArray(entry.groups))
733
+ && (!hasOwn(entry, 'category') || nonBlankString(entry.category)))) {
734
+ return { kind: 'read', reason: 'poetry_lockfile_invalid_shape' };
735
+ }
736
+ return proof('poetry.lock', 'poetry', formatVersion, TOML_PARSER_VERSION, parsed.package.map((entry) => ({ ecosystem: 'poetry', name: entry.name, version: entry.version })));
737
+ }
738
+ function parseUvLockfile(text) {
739
+ const parsed = tomlRecord(text);
740
+ if (parsed === null)
741
+ return { kind: 'read', reason: 'uv_lockfile_invalid_toml' };
742
+ if (parsed.version !== 1)
743
+ return Number.isInteger(parsed.version)
744
+ ? { kind: 'read', reason: 'uv_lockfile_unsupported_version' }
745
+ : { kind: 'read', reason: 'uv_lockfile_invalid_shape' };
746
+ if (parsed.revision !== 1 || !nonBlankString(parsed['requires-python'])
747
+ || !packageArray(parsed.package, (entry) => nonBlankString(entry.name) && nonBlankString(entry.version)
748
+ && isRecord(entry.source) && Object.keys(entry.source).length > 0 && Object.values(entry.source).every(nonBlankString))) {
749
+ return { kind: 'read', reason: 'uv_lockfile_invalid_shape' };
750
+ }
751
+ return proof('uv.lock', 'uv', '1', TOML_PARSER_VERSION, parsed.package.map((entry) => ({ ecosystem: 'uv', name: entry.name, version: entry.version })));
752
+ }
753
+ function validPipfilePackages(value) {
754
+ return isRecord(value) && Object.values(value).every((entry) => nonBlankString(entry)
755
+ || (isRecord(entry) && nonBlankString(entry.version)));
756
+ }
757
+ function parsePipfileLockfile(text) {
758
+ let parsed;
759
+ try {
760
+ parsed = JSON.parse(text);
761
+ }
762
+ catch {
763
+ return { kind: 'read', reason: 'pipfile_lockfile_invalid_json' };
764
+ }
765
+ if (hasDuplicateJsonKeys(text) || !isRecord(parsed) || !isRecord(parsed._meta) || !isRecord(parsed._meta.hash)
766
+ || !nonBlankString(parsed._meta.hash.sha256) || parsed._meta['pipfile-spec'] !== 6
767
+ || !isRecord(parsed._meta.requires) || !Array.isArray(parsed._meta.sources)
768
+ || !parsed._meta.sources.every((source) => isRecord(source) && nonBlankString(source.url) && nonBlankString(source.name)
769
+ && typeof source.verify_ssl === 'boolean') || !validPipfilePackages(parsed.default)
770
+ || (hasOwn(parsed, 'develop') && !validPipfilePackages(parsed.develop))) {
771
+ return { kind: 'read', reason: 'pipfile_lockfile_invalid_shape' };
772
+ }
773
+ const defaultPackages = parsed.default;
774
+ const dependencies = Object.entries(defaultPackages).flatMap(([name, entry]) => {
775
+ const version = typeof entry === 'string' ? entry : isRecord(entry) ? entry.version : '';
776
+ return typeof version === 'string' && /^==[^\s]+$/.test(version)
777
+ ? [{ ecosystem: 'python', name, version: version.slice(2) }]
778
+ : [];
779
+ });
780
+ return proof('Pipfile.lock', 'pipfile', '6', 'json@builtin', dependencies);
781
+ }
782
+ function validComposerPackages(value) {
783
+ return Array.isArray(value) && value.every((entry) => isRecord(entry) && nonBlankString(entry.name) && nonBlankString(entry.version));
784
+ }
785
+ function parseComposerLockfile(text) {
786
+ let parsed;
787
+ try {
788
+ parsed = JSON.parse(text);
789
+ }
790
+ catch {
791
+ return { kind: 'read', reason: 'composer_lockfile_invalid_json' };
792
+ }
793
+ if (hasDuplicateJsonKeys(text) || !isRecord(parsed) || !/^[a-f0-9]{64}$/i.test(String(parsed['content-hash']))
794
+ || !validComposerPackages(parsed.packages) || (hasOwn(parsed, 'packages-dev') && !validComposerPackages(parsed['packages-dev']))) {
795
+ return { kind: 'read', reason: 'composer_lockfile_invalid_shape' };
796
+ }
797
+ const packages = [...parsed.packages, ...(parsed['packages-dev'] ?? [])];
798
+ return proof('composer.lock', 'composer', 'content-hash@1', 'json@builtin', packages.map((entry) => ({ ecosystem: 'composer', name: entry.name, version: entry.version })));
799
+ }
800
+ const GO_MODULE_PATH = /^[A-Za-z0-9][A-Za-z0-9.!~+_-]*(?:\/[A-Za-z0-9][A-Za-z0-9.!~+_-]*)*$/;
801
+ const GO_VERSION = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.+-]+)?(?:\+incompatible)?(?:\/go\.mod)?$/;
802
+ const GO_CHECKSUM = /^h1:[A-Za-z0-9+/]{43}=$/;
803
+ function parseGoSum(text) {
804
+ if (text.length === 0)
805
+ return { kind: 'read', reason: 'go_sum_invalid_grammar' };
806
+ const lines = text.split('\n').map((line) => line.endsWith('\r') ? line.slice(0, -1) : line);
807
+ if (lines.at(-1) === '')
808
+ lines.pop();
809
+ if (lines.length === 0 || lines.some((line) => line.length === 0 || line.includes('\r')))
810
+ return { kind: 'read', reason: 'go_sum_invalid_grammar' };
811
+ const dependencies = [];
812
+ for (const line of lines) {
813
+ const fields = line.split(' ');
814
+ if (fields.length !== 3 || fields.some((field) => field.length === 0))
815
+ return { kind: 'read', reason: 'go_sum_invalid_grammar' };
816
+ const [name, version, checksum] = fields;
817
+ if (!GO_MODULE_PATH.test(name) || !GO_VERSION.test(version) || !GO_CHECKSUM.test(checksum)) {
818
+ return { kind: 'read', reason: 'go_sum_invalid_grammar' };
819
+ }
820
+ dependencies.push({ ecosystem: 'go', name: name, version: version.replace(/\/go\.mod$/, '') });
821
+ }
822
+ return proof('go.sum', 'go-sum', '1', GO_SUM_PARSER_VERSION, dependencies);
823
+ }
824
+ const GEM_SOURCE_SECTIONS = new Set(['GEM', 'GIT', 'PATH', 'PLUGIN']);
825
+ const GEM_SIMPLE_SECTIONS = new Set(['CHECKSUMS', 'PLATFORMS', 'DEPENDENCIES', 'RUBY VERSION', 'RUBYGEMS']);
826
+ const GEM_SPEC = /^ ([A-Za-z0-9_.-]+) \(([^()\s][^()]*)\)$/;
827
+ const GEM_DEPENDENCY = /^ [A-Za-z0-9_.-]+(?: \([^()]+\))?$/;
828
+ const GEM_CHECKSUM = /^ [A-Za-z0-9_.-]+ \([^()]+\) (?:sha256|sha512)=[A-Za-z0-9+/=]+(?:, (?:sha256|sha512)=[A-Za-z0-9+/=]+)*$/;
829
+ function parseGemfileLock(text) {
830
+ if (text.length === 0 || text.includes('\r') && !text.includes('\r\n'))
831
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
832
+ const lines = text.split('\n').map((line) => line.endsWith('\r') ? line.slice(0, -1) : line);
833
+ if (lines.at(-1) === '')
834
+ lines.pop();
835
+ let index = 0;
836
+ let sourceCount = 0;
837
+ let platforms = false;
838
+ let dependenciesSection = false;
839
+ const dependencies = [];
840
+ while (index < lines.length) {
841
+ if (lines[index] === '') {
842
+ index += 1;
843
+ continue;
844
+ }
845
+ const section = lines[index];
846
+ if (!/^[A-Z][A-Z ]+$/.test(section) || (!GEM_SOURCE_SECTIONS.has(section) && !GEM_SIMPLE_SECTIONS.has(section) && section !== 'BUNDLED WITH')) {
847
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
848
+ }
849
+ index += 1;
850
+ if (GEM_SOURCE_SECTIONS.has(section)) {
851
+ sourceCount += 1;
852
+ let sawSpecs = false;
853
+ while (index < lines.length && lines[index] !== '' && !/^[A-Z][A-Z ]+$/.test(lines[index])) {
854
+ const line = lines[index];
855
+ if (/^ specs:$/.test(line)) {
856
+ if (sawSpecs)
857
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
858
+ sawSpecs = true;
859
+ index += 1;
860
+ let specifications = 0;
861
+ while (index < lines.length && GEM_SPEC.test(lines[index])) {
862
+ const match = GEM_SPEC.exec(lines[index]);
863
+ dependencies.push({ ecosystem: 'ruby', name: match[1], version: match[2] });
864
+ specifications += 1;
865
+ index += 1;
866
+ while (index < lines.length && lines[index].startsWith(' ')) {
867
+ if (!GEM_DEPENDENCY.test(lines[index]))
868
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
869
+ index += 1;
870
+ }
871
+ }
872
+ if (specifications === 0)
873
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
874
+ }
875
+ else if (!/^ (?:remote|revision|branch|ref|tag|glob):\s+\S.*$/.test(line)) {
876
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
877
+ }
878
+ else
879
+ index += 1;
880
+ }
881
+ if (!sawSpecs)
882
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
883
+ }
884
+ else if (section === 'BUNDLED WITH') {
885
+ if (index >= lines.length || !/^ \d+(?:\.\d+)+(?:[A-Za-z0-9.-]+)?$/.test(lines[index])) {
886
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
887
+ }
888
+ index += 1;
889
+ }
890
+ else {
891
+ let entries = 0;
892
+ while (index < lines.length && lines[index] !== '' && !/^[A-Z][A-Z ]+$/.test(lines[index])) {
893
+ const line = lines[index];
894
+ if (section === 'CHECKSUMS' && !GEM_CHECKSUM.test(line))
895
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
896
+ if (section === 'PLATFORMS' && !/^ [A-Za-z0-9_.-]+$/.test(line))
897
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
898
+ if (section === 'DEPENDENCIES' && !/^ [A-Za-z0-9_.-]+(?: \([^()]+\))?!?$/.test(line))
899
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
900
+ if ((section === 'RUBY VERSION' || section === 'RUBYGEMS') && !/^ \S.*$/.test(line))
901
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
902
+ entries += 1;
903
+ index += 1;
904
+ }
905
+ if (entries === 0)
906
+ return { kind: 'read', reason: 'gemfile_lock_invalid_grammar' };
907
+ if (section === 'PLATFORMS')
908
+ platforms = true;
909
+ if (section === 'DEPENDENCIES')
910
+ dependenciesSection = true;
911
+ }
912
+ }
913
+ return sourceCount > 0 && platforms && dependenciesSection
914
+ ? proof('Gemfile.lock', 'gemfile-lock', '1', GEMFILE_LOCK_PARSER_VERSION, dependencies)
915
+ : { kind: 'read', reason: 'gemfile_lock_invalid_shape' };
916
+ }
917
+ export function parseOwnedLockfile(input) {
918
+ const filename = typeof input.path === 'string' ? ownedName(input.path) : null;
919
+ if (filename === null)
920
+ return { kind: 'read', reason: 'lockfile_not_owned' };
921
+ if (typeof input.text !== 'string')
922
+ return { kind: 'read', reason: 'lockfile_input_invalid' };
923
+ if (filename === 'package-lock.json' || filename === 'npm-shrinkwrap.json') {
924
+ return parseNpmLockfile(filename, input.text);
925
+ }
926
+ if (filename === 'pnpm-lock.yaml')
927
+ return parsePnpmLockfile(input.text);
928
+ if (filename === 'yarn.lock')
929
+ return parseYarnLockfile(input.text);
930
+ if (filename === 'bun.lock')
931
+ return parseBunLockfile(input.text);
932
+ if (filename === 'Cargo.lock')
933
+ return parseCargoLockfile(input.text);
934
+ if (filename === 'poetry.lock')
935
+ return parsePoetryLockfile(input.text);
936
+ if (filename === 'uv.lock')
937
+ return parseUvLockfile(input.text);
938
+ if (filename === 'Pipfile.lock')
939
+ return parsePipfileLockfile(input.text);
940
+ if (filename === 'composer.lock')
941
+ return parseComposerLockfile(input.text);
942
+ if (filename === 'go.sum')
943
+ return parseGoSum(input.text);
944
+ if (filename === 'Gemfile.lock')
945
+ return parseGemfileLock(input.text);
946
+ return { kind: 'read', reason: 'lockfile_parser_not_implemented' };
947
+ }