@dereekb/firebase 13.12.4 → 13.12.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.
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
- import { existsSync, readFileSync, globSync } from 'node:fs';
3
- import { join, dirname, isAbsolute, resolve } from 'node:path';
2
+ import { existsSync, readFileSync, globSync, readdirSync } from 'node:fs';
3
+ import { join, dirname, isAbsolute, resolve, sep, basename } from 'node:path';
4
4
  import { parse as parse$1 } from '@typescript-eslint/typescript-estree';
5
5
  import { parse as parse$2 } from '@typescript-eslint/parser';
6
6
 
@@ -17227,6 +17227,415 @@ function reportCompositeKeyTag(ctx, tag, allowedEncodings) {
17227
17227
  }
17228
17228
  };
17229
17229
 
17230
+ var SPEC_SUFFIX = '.spec.ts';
17231
+ /**
17232
+ * Classifies a spec filename against the conventions for the given parent
17233
+ * folder. Pure function — never touches the filesystem.
17234
+ *
17235
+ * @param config - Inputs.
17236
+ * @param config.filename - Bare filename including the `.spec.ts` suffix
17237
+ * (e.g. `job.scenario.requirement.spec.ts`).
17238
+ * @param config.parentFolderName - Name of the directory containing the file
17239
+ * (e.g. `job`). Used to detect cross-group misplacement.
17240
+ * @returns The classification.
17241
+ */ function classifySpecFile(config) {
17242
+ var filename = config.filename, parentFolderName = config.parentFolderName;
17243
+ var result;
17244
+ if (!filename.endsWith(SPEC_SUFFIX)) {
17245
+ result = {
17246
+ filename: filename,
17247
+ group: '',
17248
+ kind: 'non-spec',
17249
+ subgroups: [],
17250
+ isCanonical: false
17251
+ };
17252
+ } else {
17253
+ var _parts_;
17254
+ var stem = filename.slice(0, -SPEC_SUFFIX.length);
17255
+ var parts = stem.split('.');
17256
+ var group = (_parts_ = parts[0]) !== null && _parts_ !== void 0 ? _parts_ : '';
17257
+ if (group !== parentFolderName) {
17258
+ result = {
17259
+ filename: filename,
17260
+ group: group,
17261
+ kind: 'non-group',
17262
+ subgroups: [],
17263
+ isCanonical: false
17264
+ };
17265
+ } else {
17266
+ var rest = parts.slice(1);
17267
+ result = classifyRemainingSegments({
17268
+ filename: filename,
17269
+ group: group,
17270
+ rest: rest
17271
+ });
17272
+ }
17273
+ }
17274
+ return result;
17275
+ }
17276
+ function classifyRemainingSegments(config) {
17277
+ var filename = config.filename, group = config.group, rest = config.rest;
17278
+ var result;
17279
+ var crudIdx = rest.indexOf('crud');
17280
+ var scenarioIdx = rest.indexOf('scenario');
17281
+ if (crudIdx === 0) {
17282
+ if (rest.length === 1) {
17283
+ result = {
17284
+ filename: filename,
17285
+ group: group,
17286
+ kind: 'crud',
17287
+ subgroups: [],
17288
+ isCanonical: true
17289
+ };
17290
+ } else {
17291
+ result = {
17292
+ filename: filename,
17293
+ group: group,
17294
+ kind: 'crud-subgroup',
17295
+ subgroups: rest.slice(1),
17296
+ isCanonical: true
17297
+ };
17298
+ }
17299
+ } else if (scenarioIdx === 0) {
17300
+ if (rest.length === 1) {
17301
+ result = {
17302
+ filename: filename,
17303
+ group: group,
17304
+ kind: 'scenario',
17305
+ subgroups: [],
17306
+ isCanonical: true
17307
+ };
17308
+ } else {
17309
+ result = {
17310
+ filename: filename,
17311
+ group: group,
17312
+ kind: 'scenario-subgroup',
17313
+ subgroups: rest.slice(1),
17314
+ isCanonical: true
17315
+ };
17316
+ }
17317
+ } else if (crudIdx > 0) {
17318
+ var subgroups = rest.filter(function(_, i) {
17319
+ return i !== crudIdx;
17320
+ });
17321
+ var recommendedRename = buildCanonicalFilename({
17322
+ group: group,
17323
+ bucket: 'crud',
17324
+ subgroups: subgroups
17325
+ });
17326
+ result = {
17327
+ filename: filename,
17328
+ group: group,
17329
+ kind: 'crud-misplaced',
17330
+ subgroups: subgroups,
17331
+ isCanonical: false,
17332
+ recommendedRename: recommendedRename,
17333
+ driftReason: '`crud` segment is not directly after the group name.'
17334
+ };
17335
+ } else if (scenarioIdx > 0) {
17336
+ var subgroups1 = rest.filter(function(_, i) {
17337
+ return i !== scenarioIdx;
17338
+ });
17339
+ var recommendedRename1 = buildCanonicalFilename({
17340
+ group: group,
17341
+ bucket: 'scenario',
17342
+ subgroups: subgroups1
17343
+ });
17344
+ result = {
17345
+ filename: filename,
17346
+ group: group,
17347
+ kind: 'scenario-misplaced',
17348
+ subgroups: subgroups1,
17349
+ isCanonical: false,
17350
+ recommendedRename: recommendedRename1,
17351
+ driftReason: '`scenario` segment is not directly after the group name.'
17352
+ };
17353
+ } else if (rest.length === 0) {
17354
+ var recommendedRename2 = buildCanonicalFilename({
17355
+ group: group,
17356
+ bucket: 'scenario',
17357
+ subgroups: []
17358
+ });
17359
+ result = {
17360
+ filename: filename,
17361
+ group: group,
17362
+ kind: 'no-bucket',
17363
+ subgroups: [],
17364
+ isCanonical: false,
17365
+ recommendedRename: recommendedRename2,
17366
+ driftReason: 'Missing `crud` or `scenario` segment.'
17367
+ };
17368
+ } else {
17369
+ var recommendedRename3 = buildCanonicalFilename({
17370
+ group: group,
17371
+ bucket: 'scenario',
17372
+ subgroups: rest
17373
+ });
17374
+ result = {
17375
+ filename: filename,
17376
+ group: group,
17377
+ kind: 'no-bucket',
17378
+ subgroups: rest,
17379
+ isCanonical: false,
17380
+ recommendedRename: recommendedRename3,
17381
+ driftReason: 'Missing `crud` or `scenario` segment — defaulting suggestion to `scenario`.'
17382
+ };
17383
+ }
17384
+ return result;
17385
+ }
17386
+ /**
17387
+ * Renders a canonical spec filename for the given group + bucket + subgroup
17388
+ * chain. Pure data — used both by drift remediation and by the
17389
+ * `recommendSpecPath()` helper below.
17390
+ *
17391
+ * @param config - Inputs.
17392
+ * @param config.group - The model-group name (e.g. `job`).
17393
+ * @param config.bucket - `crud` or `scenario`.
17394
+ * @param config.subgroups - Optional ordered subgroup segments
17395
+ * (e.g. `['requirement','worker']`).
17396
+ * @returns The canonical filename (e.g. `job.scenario.requirement.worker.spec.ts`).
17397
+ */ function buildCanonicalFilename(config) {
17398
+ var group = config.group, bucket = config.bucket, subgroups = config.subgroups;
17399
+ var tail = subgroups.length === 0 ? '' : ".".concat(subgroups.join('.'));
17400
+ return "".concat(group, ".").concat(bucket).concat(tail, ".spec.ts");
17401
+ }
17402
+
17403
+ /**
17404
+ * Default subpath segment (relative to an app source root) below which the
17405
+ * rule expects model-group function folders. Matches the convention used by
17406
+ * `<apiDir>/src/app/function/<group>/`.
17407
+ */ var DEFAULT_FUNCTION_DIR_SEGMENT = 'src/app/function';
17408
+ function matchFunctionSpecPath(filename, functionDirSegment) {
17409
+ var result;
17410
+ if (filename.endsWith('.spec.ts')) {
17411
+ var normalized = filename.split(sep).join('/');
17412
+ var marker = "/".concat(functionDirSegment, "/");
17413
+ var markerIdx = normalized.indexOf(marker);
17414
+ if (markerIdx >= 0) {
17415
+ var afterMarker = normalized.slice(markerIdx + marker.length);
17416
+ var parts = afterMarker.split('/');
17417
+ if (parts.length === 2) {
17418
+ var _parts_, _parts_1;
17419
+ result = {
17420
+ filename: (_parts_ = parts[1]) !== null && _parts_ !== void 0 ? _parts_ : '',
17421
+ parentFolderName: (_parts_1 = parts[0]) !== null && _parts_1 !== void 0 ? _parts_1 : ''
17422
+ };
17423
+ } else if (parts.length > 2) {
17424
+ result = {
17425
+ filename: basename(filename),
17426
+ parentFolderName: basename(dirname(filename))
17427
+ };
17428
+ }
17429
+ }
17430
+ }
17431
+ return result;
17432
+ }
17433
+ /**
17434
+ * ESLint rule that enforces the canonical naming convention for Firebase
17435
+ * Functions API spec files: every `.spec.ts` under
17436
+ * `<apiDir>/src/app/function/<group>/` must be `<group>.crud[.<sub>...].spec.ts`
17437
+ * or `<group>.scenario[.<sub>...].spec.ts`. Drift forms surface a rename
17438
+ * suggestion derived from the shared `classifySpecFile` classifier in
17439
+ * `@dereekb/util`, so this rule and the `dbx_model_test_validate_app` MCP
17440
+ * tool never diverge.
17441
+ *
17442
+ * Not auto-fixable: renaming files (and updating any imports/snapshots they
17443
+ * carry) is outside the safe scope of an ESLint autofix.
17444
+ */ var FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE = {
17445
+ meta: {
17446
+ type: 'suggestion',
17447
+ fixable: undefined,
17448
+ docs: {
17449
+ description: 'Require API spec filenames under `src/app/function/<group>/` to follow the `<group>.crud[.<sub>...].spec.ts` / `<group>.scenario[.<sub>...].spec.ts` convention.',
17450
+ recommended: true
17451
+ },
17452
+ messages: {
17453
+ testFileDriftRename: '`{{filename}}`: {{reason}} Rename to `{{recommendedRename}}`.',
17454
+ testFileMissingBucket: '`{{filename}}`: missing `crud` / `scenario` segment. Rename to `{{recommendedRename}}` (default) or to a `crud` variant if the tests are CRUD-flavored.',
17455
+ testFileNonGroupPlacement: '`{{filename}}`: first segment `{{group}}` does not match the parent folder `{{parentFolderName}}`. Move into `{{group}}/` or rename the prefix to match the current folder.'
17456
+ },
17457
+ schema: [
17458
+ {
17459
+ type: 'object',
17460
+ additionalProperties: false,
17461
+ properties: {
17462
+ functionDirSegment: {
17463
+ type: 'string'
17464
+ }
17465
+ }
17466
+ }
17467
+ ]
17468
+ },
17469
+ create: function create(context) {
17470
+ var _context_options_, _options_functionDirSegment;
17471
+ var options = (_context_options_ = context.options[0]) !== null && _context_options_ !== void 0 ? _context_options_ : {};
17472
+ var functionDirSegment = (_options_functionDirSegment = options.functionDirSegment) !== null && _options_functionDirSegment !== void 0 ? _options_functionDirSegment : DEFAULT_FUNCTION_DIR_SEGMENT;
17473
+ var matched = matchFunctionSpecPath(context.filename, functionDirSegment);
17474
+ function check(programNode) {
17475
+ if (!matched) return;
17476
+ var classification = classifySpecFile({
17477
+ filename: matched.filename,
17478
+ parentFolderName: matched.parentFolderName
17479
+ });
17480
+ if (classification.isCanonical) return;
17481
+ if (classification.kind === 'non-spec') return;
17482
+ if (classification.kind === 'crud-misplaced' || classification.kind === 'scenario-misplaced') {
17483
+ var _classification_driftReason, _classification_recommendedRename;
17484
+ context.report({
17485
+ node: programNode,
17486
+ messageId: 'testFileDriftRename',
17487
+ data: {
17488
+ filename: classification.filename,
17489
+ reason: (_classification_driftReason = classification.driftReason) !== null && _classification_driftReason !== void 0 ? _classification_driftReason : 'segment order does not match the convention.',
17490
+ recommendedRename: (_classification_recommendedRename = classification.recommendedRename) !== null && _classification_recommendedRename !== void 0 ? _classification_recommendedRename : ''
17491
+ }
17492
+ });
17493
+ } else if (classification.kind === 'no-bucket') {
17494
+ var _classification_recommendedRename1;
17495
+ context.report({
17496
+ node: programNode,
17497
+ messageId: 'testFileMissingBucket',
17498
+ data: {
17499
+ filename: classification.filename,
17500
+ recommendedRename: (_classification_recommendedRename1 = classification.recommendedRename) !== null && _classification_recommendedRename1 !== void 0 ? _classification_recommendedRename1 : ''
17501
+ }
17502
+ });
17503
+ } else if (classification.kind === 'non-group') {
17504
+ context.report({
17505
+ node: programNode,
17506
+ messageId: 'testFileNonGroupPlacement',
17507
+ data: {
17508
+ filename: classification.filename,
17509
+ group: classification.group,
17510
+ parentFolderName: matched.parentFolderName
17511
+ }
17512
+ });
17513
+ }
17514
+ }
17515
+ return {
17516
+ Program: function Program(node) {
17517
+ return check(node);
17518
+ }
17519
+ };
17520
+ }
17521
+ };
17522
+
17523
+ function isGroupIndex(filename, functionDirSegment) {
17524
+ var normalized = filename.split(sep).join('/');
17525
+ var marker = "/".concat(functionDirSegment, "/");
17526
+ var markerIdx = normalized.indexOf(marker);
17527
+ var result = false;
17528
+ if (markerIdx >= 0) {
17529
+ var afterMarker = normalized.slice(markerIdx + marker.length);
17530
+ var parts = afterMarker.split('/');
17531
+ result = parts.length === 2 && parts[1] === 'index.ts';
17532
+ }
17533
+ return result;
17534
+ }
17535
+ function hasCrudSpec(groupDir, group) {
17536
+ var found = false;
17537
+ try {
17538
+ var entries = readdirSync(groupDir);
17539
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
17540
+ try {
17541
+ for(var _iterator = entries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
17542
+ var entry = _step.value;
17543
+ var classification = classifySpecFile({
17544
+ filename: entry,
17545
+ parentFolderName: group
17546
+ });
17547
+ if (classification.kind === 'crud' || classification.kind === 'crud-subgroup') {
17548
+ found = true;
17549
+ break;
17550
+ }
17551
+ }
17552
+ } catch (err) {
17553
+ _didIteratorError = true;
17554
+ _iteratorError = err;
17555
+ } finally{
17556
+ try {
17557
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
17558
+ _iterator.return();
17559
+ }
17560
+ } finally{
17561
+ if (_didIteratorError) {
17562
+ throw _iteratorError;
17563
+ }
17564
+ }
17565
+ }
17566
+ } catch (unused) {
17567
+ // Directory unreadable — treat as no crud spec; rule will emit the warning
17568
+ // and the user can investigate. We do NOT swallow the error silently
17569
+ // in any other way.
17570
+ }
17571
+ return found;
17572
+ }
17573
+ /**
17574
+ * ESLint rule that fires on every `<apiDir>/src/app/function/<group>/index.ts`
17575
+ * (the canonical anchor file for a Firebase Functions group) and verifies
17576
+ * that the same folder contains a `<group>.crud.spec.ts` (or any
17577
+ * `<group>.crud.<sub>.spec.ts` variant) sibling. Mirrors the coverage check
17578
+ * in `dbx_model_test_validate_app` so editor + CI lint flag missing CRUD
17579
+ * coverage without needing the MCP audit.
17580
+ *
17581
+ * Not auto-fixable: creating a spec file shell with sensible test cases is
17582
+ * outside the safe scope of an ESLint autofix.
17583
+ */ var FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE = {
17584
+ meta: {
17585
+ type: 'suggestion',
17586
+ fixable: undefined,
17587
+ docs: {
17588
+ description: 'Require every API model-group function folder to have a `<group>.crud.spec.ts` covering its CRUD function map.',
17589
+ recommended: true
17590
+ },
17591
+ messages: {
17592
+ modelGroupMissingCrudSpec: 'Model group `{{group}}` has no `{{expectedFilename}}`. Add it covering the CRUD function map (create/read/update/delete + permission/error paths).'
17593
+ },
17594
+ schema: [
17595
+ {
17596
+ type: 'object',
17597
+ additionalProperties: false,
17598
+ properties: {
17599
+ functionDirSegment: {
17600
+ type: 'string'
17601
+ }
17602
+ }
17603
+ }
17604
+ ]
17605
+ },
17606
+ create: function create(context) {
17607
+ var _context_options_, _options_functionDirSegment;
17608
+ var options = (_context_options_ = context.options[0]) !== null && _context_options_ !== void 0 ? _context_options_ : {};
17609
+ var functionDirSegment = (_options_functionDirSegment = options.functionDirSegment) !== null && _options_functionDirSegment !== void 0 ? _options_functionDirSegment : DEFAULT_FUNCTION_DIR_SEGMENT;
17610
+ var filename = context.filename;
17611
+ var isAnchor = isGroupIndex(filename, functionDirSegment);
17612
+ function check(programNode) {
17613
+ if (!isAnchor) return;
17614
+ var groupDir = dirname(filename);
17615
+ var group = basename(groupDir);
17616
+ if (hasCrudSpec(groupDir, group)) return;
17617
+ var expectedFilename = buildCanonicalFilename({
17618
+ group: group,
17619
+ bucket: 'crud',
17620
+ subgroups: []
17621
+ });
17622
+ context.report({
17623
+ node: programNode,
17624
+ messageId: 'modelGroupMissingCrudSpec',
17625
+ data: {
17626
+ group: group,
17627
+ expectedFilename: expectedFilename
17628
+ }
17629
+ });
17630
+ }
17631
+ return {
17632
+ Program: function Program(node) {
17633
+ return check(node);
17634
+ }
17635
+ };
17636
+ }
17637
+ };
17638
+
17230
17639
  /**
17231
17640
  * ESLint plugin for `@dereekb/firebase` rules.
17232
17641
  *
@@ -17246,7 +17655,9 @@ function reportCompositeKeyTag(ctx, tag, allowedEncodings) {
17246
17655
  'require-firestore-rule-for-service-model': FIREBASE_REQUIRE_FIRESTORE_RULE_FOR_SERVICE_MODEL_RULE,
17247
17656
  'require-dbx-model-service-factory-tag': FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE,
17248
17657
  'require-service-factory-for-dbx-model': FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE,
17249
- 'require-dbx-model-companion-tags': FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE
17658
+ 'require-dbx-model-companion-tags': FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE,
17659
+ 'require-canonical-api-spec-filename': FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE,
17660
+ 'require-api-crud-spec-for-group': FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE
17250
17661
  }
17251
17662
  };
17252
17663
  /**
@@ -17255,4 +17666,4 @@ function reportCompositeKeyTag(ctx, tag, allowedEncodings) {
17255
17666
  * @dbxAllowConstantName
17256
17667
  */ var firebaseESLintPlugin = FIREBASE_ESLINT_PLUGIN;
17257
17668
 
17258
- export { API_DETAILS_IMPORT_MODULE, DBX_MODEL_FIREBASE_INDEX_MARKER, DBX_MODEL_SERVICE_FACTORY_TAG, DEFAULT_API_DETAILS_FACTORY_NAME, DEFAULT_CONSTRAINT_FACTORY_NAMES, DEFAULT_CRUD_FUNCTION_TYPE_VERBS, DEFAULT_CRUD_VERB_NAMES, DEFAULT_DISCOVERY_EXCLUDED_DIRS, DEFAULT_FACTORY_SEARCH_ROOTS, DEFAULT_FACTORY_TAG, DEFAULT_FIRESTORE_RULES_FILENAME, DEFAULT_IDENTITY_FACTORY_NAME, DEFAULT_INDEX_AFFECTING_CONSTRAINT_NAMES, DEFAULT_MODEL_MARKER_TAG, DEFAULT_MODEL_SEARCH_ROOTS, DEFAULT_PAGINATION_CONSTRAINT_NAMES, DEFAULT_REGISTRY_FACTORY_CALL_NAME, DEFAULT_STORAGE_FILE_UPLOAD_POLICY_TYPE_NAME, DEFAULT_STORAGE_RULES_FILENAME, FIREBASE_ESLINT_PLUGIN, FIREBASE_MODEL_SERVICE_FACTORY_MODULE, FIREBASE_MODEL_SERVICE_FACTORY_NAME, FIREBASE_MODULE, FIREBASE_REQUIRE_API_DETAILS_FOR_CRUD_FUNCTION_RULE, FIREBASE_REQUIRE_COMPLETE_CRUD_FUNCTION_CONFIG_MAP_RULE, FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE, FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_COMPANION_TAGS_RULE, FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_QUERY_SUFFIX_RULE, FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_VALID_DISPATCHER_RULE, FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE, FIREBASE_REQUIRE_FIRESTORE_CONSTRAINT_TYPE_PARAMETER_RULE, FIREBASE_REQUIRE_FIRESTORE_RULE_FOR_SERVICE_MODEL_RULE, FIREBASE_REQUIRE_INPUT_TYPE_FOR_API_DETAILS_RULE, FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE, FIREBASE_REQUIRE_STORAGEFILE_POLICY_MATCHES_RULES_RULE, FIREBASE_REQUIRE_TAGGED_FIRESTORE_CONSTRAINTS_RULE, INPUT_TYPE_PROPERTY_NAME, MIRRORS_POLICY_KEY_MARKER_REGEX, MODEL_FIREBASE_CRUD_FUNCTION_CONFIG_MAP_TYPE_NAME, QUERY_SUFFIX, discoveryGlobExcludeFilter, firebaseESLintPlugin, parseFirestoreRules, parseStorageRules };
17669
+ export { API_DETAILS_IMPORT_MODULE, DBX_MODEL_FIREBASE_INDEX_MARKER, DBX_MODEL_SERVICE_FACTORY_TAG, DEFAULT_API_DETAILS_FACTORY_NAME, DEFAULT_CONSTRAINT_FACTORY_NAMES, DEFAULT_CRUD_FUNCTION_TYPE_VERBS, DEFAULT_CRUD_VERB_NAMES, DEFAULT_DISCOVERY_EXCLUDED_DIRS, DEFAULT_FACTORY_SEARCH_ROOTS, DEFAULT_FACTORY_TAG, DEFAULT_FIRESTORE_RULES_FILENAME, DEFAULT_FUNCTION_DIR_SEGMENT, DEFAULT_IDENTITY_FACTORY_NAME, DEFAULT_INDEX_AFFECTING_CONSTRAINT_NAMES, DEFAULT_MODEL_MARKER_TAG, DEFAULT_MODEL_SEARCH_ROOTS, DEFAULT_PAGINATION_CONSTRAINT_NAMES, DEFAULT_REGISTRY_FACTORY_CALL_NAME, DEFAULT_STORAGE_FILE_UPLOAD_POLICY_TYPE_NAME, DEFAULT_STORAGE_RULES_FILENAME, FIREBASE_ESLINT_PLUGIN, FIREBASE_MODEL_SERVICE_FACTORY_MODULE, FIREBASE_MODEL_SERVICE_FACTORY_NAME, FIREBASE_MODULE, FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE, FIREBASE_REQUIRE_API_DETAILS_FOR_CRUD_FUNCTION_RULE, FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE, FIREBASE_REQUIRE_COMPLETE_CRUD_FUNCTION_CONFIG_MAP_RULE, FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE, FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_COMPANION_TAGS_RULE, FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_QUERY_SUFFIX_RULE, FIREBASE_REQUIRE_DBX_MODEL_FIREBASE_INDEX_VALID_DISPATCHER_RULE, FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE, FIREBASE_REQUIRE_FIRESTORE_CONSTRAINT_TYPE_PARAMETER_RULE, FIREBASE_REQUIRE_FIRESTORE_RULE_FOR_SERVICE_MODEL_RULE, FIREBASE_REQUIRE_INPUT_TYPE_FOR_API_DETAILS_RULE, FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE, FIREBASE_REQUIRE_STORAGEFILE_POLICY_MATCHES_RULES_RULE, FIREBASE_REQUIRE_TAGGED_FIRESTORE_CONSTRAINTS_RULE, INPUT_TYPE_PROPERTY_NAME, MIRRORS_POLICY_KEY_MARKER_REGEX, MODEL_FIREBASE_CRUD_FUNCTION_CONFIG_MAP_TYPE_NAME, QUERY_SUFFIX, discoveryGlobExcludeFilter, firebaseESLintPlugin, parseFirestoreRules, parseStorageRules };
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/firebase/eslint",
3
- "version": "13.12.4",
3
+ "version": "13.12.6",
4
4
  "peerDependencies": {
5
- "@dereekb/util": "13.12.4",
5
+ "@dereekb/util": "13.12.6",
6
6
  "@marcbachmann/cel-js": "^7.6.1",
7
7
  "@typescript-eslint/parser": "8.59.3",
8
8
  "@typescript-eslint/utils": "8.59.3"
9
9
  },
10
10
  "devDependencies": {
11
- "@dereekb/firebase": "13.12.4",
11
+ "@dereekb/firebase": "13.12.6",
12
12
  "eslint": "10.4.0",
13
13
  "firebase": "^12.12.1"
14
14
  },
@@ -11,6 +11,8 @@ export { FIREBASE_REQUIRE_FIRESTORE_RULE_FOR_SERVICE_MODEL_RULE, DEFAULT_FIRESTO
11
11
  export { FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE, FIREBASE_MODEL_SERVICE_FACTORY_NAME, FIREBASE_MODEL_SERVICE_FACTORY_MODULE, DBX_MODEL_SERVICE_FACTORY_TAG, type FirebaseRequireDbxModelServiceFactoryTagRuleOptions, type FirebaseRequireDbxModelServiceFactoryTagRuleDefinition } from './require-dbx-model-service-factory-tag.rule';
12
12
  export { FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE, DEFAULT_FACTORY_SEARCH_ROOTS, DEFAULT_MODEL_MARKER_TAG, DEFAULT_FACTORY_TAG, type FirebaseRequireServiceFactoryForDbxModelRuleOptions, type FirebaseRequireServiceFactoryForDbxModelRuleDefinition } from './require-service-factory-for-dbx-model.rule';
13
13
  export { FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE, type FirebaseRequireDbxModelCompanionTagsRuleOptions, type FirebaseRequireDbxModelCompanionTagsRuleDefinition } from './require-dbx-model-companion-tags.rule';
14
+ export { FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE, DEFAULT_FUNCTION_DIR_SEGMENT, type FirebaseRequireCanonicalApiSpecFilenameRuleOptions, type FirebaseRequireCanonicalApiSpecFilenameRuleDefinition } from './require-canonical-api-spec-filename.rule';
15
+ export { FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE, type FirebaseRequireApiCrudSpecForGroupRuleOptions, type FirebaseRequireApiCrudSpecForGroupRuleDefinition } from './require-api-crud-spec-for-group.rule';
14
16
  export { parseStorageRules, MIRRORS_POLICY_KEY_MARKER_REGEX, type ParsedRuleBranch, type ParsedStorageRulesBlock } from './storage-rules-parser';
15
17
  export { parseFirestoreRules, type ParsedFirestoreMatchBlock } from './firestore-rules-parser';
16
18
  export { FIREBASE_ESLINT_PLUGIN, firebaseESLintPlugin, type FirebaseEslintPlugin } from './plugin';
@@ -11,6 +11,8 @@ import { FIREBASE_REQUIRE_FIRESTORE_RULE_FOR_SERVICE_MODEL_RULE } from './requir
11
11
  import { FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE } from './require-dbx-model-service-factory-tag.rule';
12
12
  import { FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE } from './require-service-factory-for-dbx-model.rule';
13
13
  import { FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE } from './require-dbx-model-companion-tags.rule';
14
+ import { FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE } from './require-canonical-api-spec-filename.rule';
15
+ import { FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE } from './require-api-crud-spec-for-group.rule';
14
16
  /**
15
17
  * ESLint plugin interface for `@dereekb/firebase` rules.
16
18
  */
@@ -29,6 +31,8 @@ export interface FirebaseEslintPlugin {
29
31
  readonly 'require-dbx-model-service-factory-tag': typeof FIREBASE_REQUIRE_DBX_MODEL_SERVICE_FACTORY_TAG_RULE;
30
32
  readonly 'require-service-factory-for-dbx-model': typeof FIREBASE_REQUIRE_SERVICE_FACTORY_FOR_DBX_MODEL_RULE;
31
33
  readonly 'require-dbx-model-companion-tags': typeof FIREBASE_REQUIRE_DBX_MODEL_COMPANION_TAGS_RULE;
34
+ readonly 'require-canonical-api-spec-filename': typeof FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE;
35
+ readonly 'require-api-crud-spec-for-group': typeof FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE;
32
36
  };
33
37
  }
34
38
  /**
@@ -0,0 +1,47 @@
1
+ import { type AstNode } from './util';
2
+ /**
3
+ * Options for the require-api-crud-spec-for-group rule.
4
+ */
5
+ export interface FirebaseRequireApiCrudSpecForGroupRuleOptions {
6
+ /**
7
+ * Path-suffix marker that anchors the rule to API function folders.
8
+ * Defaults to `'src/app/function'`. Files outside this segment are ignored.
9
+ */
10
+ readonly functionDirSegment?: string;
11
+ }
12
+ /**
13
+ * ESLint rule definition for require-api-crud-spec-for-group.
14
+ */
15
+ export interface FirebaseRequireApiCrudSpecForGroupRuleDefinition {
16
+ readonly meta: {
17
+ readonly type: 'suggestion';
18
+ readonly fixable: undefined;
19
+ readonly docs: {
20
+ readonly description: string;
21
+ readonly recommended: boolean;
22
+ };
23
+ readonly messages: Readonly<Record<string, string>>;
24
+ readonly schema: readonly object[];
25
+ };
26
+ create(context: {
27
+ options: FirebaseRequireApiCrudSpecForGroupRuleOptions[];
28
+ filename: string;
29
+ report: (descriptor: {
30
+ node: AstNode;
31
+ messageId: string;
32
+ data?: Record<string, string>;
33
+ }) => void;
34
+ }): Record<string, (node: AstNode) => void>;
35
+ }
36
+ /**
37
+ * ESLint rule that fires on every `<apiDir>/src/app/function/<group>/index.ts`
38
+ * (the canonical anchor file for a Firebase Functions group) and verifies
39
+ * that the same folder contains a `<group>.crud.spec.ts` (or any
40
+ * `<group>.crud.<sub>.spec.ts` variant) sibling. Mirrors the coverage check
41
+ * in `dbx_model_test_validate_app` so editor + CI lint flag missing CRUD
42
+ * coverage without needing the MCP audit.
43
+ *
44
+ * Not auto-fixable: creating a spec file shell with sensible test cases is
45
+ * outside the safe scope of an ESLint autofix.
46
+ */
47
+ export declare const FIREBASE_REQUIRE_API_CRUD_SPEC_FOR_GROUP_RULE: FirebaseRequireApiCrudSpecForGroupRuleDefinition;
@@ -0,0 +1,55 @@
1
+ import { type AstNode } from './util';
2
+ /**
3
+ * Default subpath segment (relative to an app source root) below which the
4
+ * rule expects model-group function folders. Matches the convention used by
5
+ * `<apiDir>/src/app/function/<group>/`.
6
+ */
7
+ export declare const DEFAULT_FUNCTION_DIR_SEGMENT = "src/app/function";
8
+ /**
9
+ * Options for the require-canonical-api-spec-filename rule.
10
+ */
11
+ export interface FirebaseRequireCanonicalApiSpecFilenameRuleOptions {
12
+ /**
13
+ * Path-suffix marker that anchors the rule to API function folders.
14
+ * Defaults to `'src/app/function'`. Any `.spec.ts` whose path contains
15
+ * `<marker>/<group>/<filename>` is classified; everything else is ignored.
16
+ */
17
+ readonly functionDirSegment?: string;
18
+ }
19
+ /**
20
+ * ESLint rule definition for require-canonical-api-spec-filename.
21
+ */
22
+ export interface FirebaseRequireCanonicalApiSpecFilenameRuleDefinition {
23
+ readonly meta: {
24
+ readonly type: 'suggestion';
25
+ readonly fixable: undefined;
26
+ readonly docs: {
27
+ readonly description: string;
28
+ readonly recommended: boolean;
29
+ };
30
+ readonly messages: Readonly<Record<string, string>>;
31
+ readonly schema: readonly object[];
32
+ };
33
+ create(context: {
34
+ options: FirebaseRequireCanonicalApiSpecFilenameRuleOptions[];
35
+ filename: string;
36
+ report: (descriptor: {
37
+ node: AstNode;
38
+ messageId: string;
39
+ data?: Record<string, string>;
40
+ }) => void;
41
+ }): Record<string, (node: AstNode) => void>;
42
+ }
43
+ /**
44
+ * ESLint rule that enforces the canonical naming convention for Firebase
45
+ * Functions API spec files: every `.spec.ts` under
46
+ * `<apiDir>/src/app/function/<group>/` must be `<group>.crud[.<sub>...].spec.ts`
47
+ * or `<group>.scenario[.<sub>...].spec.ts`. Drift forms surface a rename
48
+ * suggestion derived from the shared `classifySpecFile` classifier in
49
+ * `@dereekb/util`, so this rule and the `dbx_model_test_validate_app` MCP
50
+ * tool never diverge.
51
+ *
52
+ * Not auto-fixable: renaming files (and updating any imports/snapshots they
53
+ * carry) is outside the safe scope of an ESLint autofix.
54
+ */
55
+ export declare const FIREBASE_REQUIRE_CANONICAL_API_SPEC_FILENAME_RULE: FirebaseRequireCanonicalApiSpecFilenameRuleDefinition;