@dereekb/dbx-components-mcp 14.0.0 → 14.1.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.
package/README.md CHANGED
@@ -78,4 +78,4 @@ The Firebase model registry (`src/registry/firebase-models.generated.ts`) is pro
78
78
  npx nx run dbx-components-mcp:generate-firebase-models
79
79
  ```
80
80
 
81
- The script formats output through Prettier so regenerating produces a byte-equal file when source models haven't changed.
81
+ The script formats output through oxfmt so regenerating produces a byte-equal file when source models haven't changed.
@@ -10,7 +10,7 @@ var __export = (target, all) => {
10
10
  // packages/dbx-components-mcp/package.json
11
11
  var package_default = {
12
12
  name: "@dereekb/dbx-components-mcp",
13
- version: "14.0.0",
13
+ version: "14.1.0",
14
14
  type: "module",
15
15
  bin: {
16
16
  "dbx-components-mcp": "dbx-components-mcp.js"
@@ -21,8 +21,8 @@ var package_default = {
21
21
  "ts-morph": "^21.0.0"
22
22
  },
23
23
  peerDependencies: {
24
- "@dereekb/dbx-cli": "14.0.0",
25
- "@dereekb/util": "14.0.0"
24
+ "@dereekb/dbx-cli": "14.1.0",
25
+ "@dereekb/util": "14.1.0"
26
26
  }
27
27
  };
28
28
 
@@ -706,7 +706,7 @@ function tallyProp(input) {
706
706
  }
707
707
  function scoreAgainstEntry(input, entry) {
708
708
  const tally = { matched: [], extraEntry: [], missingInput: [], weightedIntersect: 0, weightedUnion: 0 };
709
- const allProps = /* @__PURE__ */ new Set([...input.keys(), ...entry.keys()]);
709
+ const allProps = /* @__PURE__ */ new Set([...Array.from(input.keys()), ...Array.from(entry.keys())]);
710
710
  for (const prop of allProps) {
711
711
  tallyProp({ tally, prop, inputValue: input.get(prop), entryValue: entry.get(prop) });
712
712
  }
@@ -3003,6 +3003,14 @@ var FIREBASE_MODELS = [
3003
3003
  optional: false,
3004
3004
  description: "Per-provider connection state, keyed by provider type."
3005
3005
  },
3006
+ {
3007
+ name: "li",
3008
+ longName: "logins",
3009
+ converter: "firestoreObjectMap<UserExternalConnectionLogin, FirestoreModelData<UserExternalConnectionLogin>, UserExternalConnectionProviderType>({ objectField: { fields: userExternalConnectionLoginFields } })",
3010
+ tsType: "UserExternalConnectionLoginMap",
3011
+ optional: false,
3012
+ description: "Per-provider LOGIN LINKS, keyed by provider type."
3013
+ },
3006
3014
  {
3007
3015
  name: "c",
3008
3016
  longName: "connectedProviderTypes",
@@ -3011,6 +3019,14 @@ var FIREBASE_MODELS = [
3011
3019
  optional: false,
3012
3020
  description: "DERIVED from `e`: every provider type whose entry status is `connected`."
3013
3021
  },
3022
+ {
3023
+ name: "ec",
3024
+ longName: "externalAccountKeys",
3025
+ converter: "optionalFirestoreArray<UserExternalConnectionExternalAccountKey>()",
3026
+ tsType: "Maybe<UserExternalConnectionExternalAccountKey[]>",
3027
+ optional: true,
3028
+ description: "DERIVED from `e` UNION `li`: the `<providerType>:<externalAccountId>` key of every entry that names an external account, plus every login link's."
3029
+ },
3014
3030
  {
3015
3031
  name: "uat",
3016
3032
  longName: "updatedAt",
@@ -3021,7 +3037,7 @@ var FIREBASE_MODELS = [
3021
3037
  }
3022
3038
  ],
3023
3039
  enums: [],
3024
- detectionHints: ["e", "c", "uat"],
3040
+ detectionHints: ["e", "li", "c", "ec", "uat"],
3025
3041
  description: "The client-readable half of a user's third-party OAuth connection state.",
3026
3042
  modelGroup: "UserExternalConnection",
3027
3043
  collectionKind: "root",
@@ -10134,7 +10150,7 @@ function collectFromTypeReference(ref, context) {
10134
10150
  result = collectFromTypeNode(args[0], { ...context, forceOptional: false }).map((p) => ({ ...p, required: true }));
10135
10151
  } else if (refName === "Omit" && args.length === 2) {
10136
10152
  const keys = parseKeyUnion(args[1]);
10137
- const merged = /* @__PURE__ */ new Set([...context.omit, ...keys]);
10153
+ const merged = /* @__PURE__ */ new Set([...Array.from(context.omit), ...Array.from(keys)]);
10138
10154
  result = collectFromTypeNode(args[0], { ...context, omit: merged });
10139
10155
  } else if (refName === "Pick" && args.length === 2) {
10140
10156
  const keys = parseKeyUnion(args[1]);
@@ -11476,7 +11492,7 @@ function tryTransitiveResolution(input) {
11476
11492
  const calleeLine = resolved.decl.getStartLineNumber();
11477
11493
  warnings.push({ kind: "unannotated-query-helper", severity: "warning", name: factoryName, callee: calleeName, calleeFilePath, calleeLine, filePath, line: call.getStartLineNumber() });
11478
11494
  }
11479
- const nextVisited = /* @__PURE__ */ new Set([...visited, calleeKey]);
11495
+ const nextVisited = new Set(visited).add(calleeKey);
11480
11496
  walkBodyInto({
11481
11497
  decl: resolved.decl,
11482
11498
  factoryName,
@@ -15456,13 +15472,21 @@ function formatBuildFailure(outcome) {
15456
15472
  }
15457
15473
  function formatWriteSummary(input) {
15458
15474
  const { outputPath, diff, generatedComposites, generatedFieldOverrides } = input;
15459
- const lines = [`generate-firestore-indexes: wrote ${outputPath}`, ` composites: ${generatedComposites} (added ${diff.added.length}, removed ${diff.removed.length}, unchanged ${diff.unchanged.length})`, ` fieldOverrides: ${generatedFieldOverrides} (added ${diff.fieldOverridesAdded.length}, removed ${diff.fieldOverridesRemoved.length}, unchanged ${diff.fieldOverridesUnchanged.length})`];
15475
+ const lines = [
15476
+ `generate-firestore-indexes: wrote ${outputPath}`,
15477
+ ` composites: ${generatedComposites} (added ${diff.added.length}, removed ${diff.removed.length}, unchanged ${diff.unchanged.length})`,
15478
+ ` fieldOverrides: ${generatedFieldOverrides} (added ${diff.fieldOverridesAdded.length}, removed ${diff.fieldOverridesRemoved.length}, unchanged ${diff.fieldOverridesUnchanged.length})`
15479
+ ];
15460
15480
  return appendDiffSection(lines, diff).join("\n");
15461
15481
  }
15462
15482
  function formatCheckSummary(input) {
15463
15483
  const { drift, outputPath, diff, generatedComposites, generatedFieldOverrides } = input;
15464
15484
  const headline = drift ? `generate-firestore-indexes: drift detected against ${outputPath}` : `generate-firestore-indexes: in sync (${outputPath})`;
15465
- const lines = [headline, ` composites: ${generatedComposites} (added ${diff.added.length}, removed ${diff.removed.length}, unchanged ${diff.unchanged.length})`, ` fieldOverrides: ${generatedFieldOverrides} (added ${diff.fieldOverridesAdded.length}, removed ${diff.fieldOverridesRemoved.length}, unchanged ${diff.fieldOverridesUnchanged.length})`];
15485
+ const lines = [
15486
+ headline,
15487
+ ` composites: ${generatedComposites} (added ${diff.added.length}, removed ${diff.removed.length}, unchanged ${diff.unchanged.length})`,
15488
+ ` fieldOverrides: ${generatedFieldOverrides} (added ${diff.fieldOverridesAdded.length}, removed ${diff.fieldOverridesRemoved.length}, unchanged ${diff.fieldOverridesUnchanged.length})`
15489
+ ];
15466
15490
  return appendDiffSection(lines, diff).join("\n");
15467
15491
  }
15468
15492
  function appendDiffSection(lines, diff) {
@@ -19816,7 +19840,7 @@ function registerResources(server, options = {}) {
19816
19840
  if (options.authRegistry !== void 0) {
19817
19841
  registerAuthResource(server, { registry: options.authRegistry });
19818
19842
  }
19819
- registerFirestoreRulesResource(server, { ...options.cwd === void 0 ? {} : { cwd: options.cwd } });
19843
+ registerFirestoreRulesResource(server, options.cwd === void 0 ? {} : { cwd: options.cwd });
19820
19844
  }
19821
19845
 
19822
19846
  // packages/dbx-components-mcp/src/tools/lookup-form.tool.ts
@@ -19909,7 +19933,19 @@ function formatBrief(field) {
19909
19933
  const tierBits = formatTierBits(field);
19910
19934
  const arrayOptional = field.arrayOutput === "optional" ? " (single or array)" : "";
19911
19935
  const array = field.arrayOutput === "yes" ? " (array)" : arrayOptional;
19912
- return [`## ${field.factoryName}`, "", `**slug:** \`${field.slug}\` \xB7 **tier:** \`${field.tier}\` \xB7 **produces:** \`${field.produces}\`${array}${tierBits}`, "", field.description, "", "```ts", field.minimalExample, "```", "", `\u2192 Call \`dbx_form_lookup topic="${field.slug}" depth="full"\` for the config table and full example.`].join("\n");
19936
+ return [
19937
+ `## ${field.factoryName}`,
19938
+ "",
19939
+ `**slug:** \`${field.slug}\` \xB7 **tier:** \`${field.tier}\` \xB7 **produces:** \`${field.produces}\`${array}${tierBits}`,
19940
+ "",
19941
+ field.description,
19942
+ "",
19943
+ "```ts",
19944
+ field.minimalExample,
19945
+ "```",
19946
+ "",
19947
+ `\u2192 Call \`dbx_form_lookup topic="${field.slug}" depth="full"\` for the config table and full example.`
19948
+ ].join("\n");
19913
19949
  }
19914
19950
  function formatFull(field) {
19915
19951
  const header = formatHeader(field);
@@ -20411,7 +20447,19 @@ function formatSearchResults(input) {
20411
20447
  const field = hit.entry;
20412
20448
  const optionalArray = field.arrayOutput === "optional" ? " *(single or array)*" : "";
20413
20449
  const array = field.arrayOutput === "yes" ? " *(array)*" : optionalArray;
20414
- lines.push(`## \`${field.slug}\` \xB7 form \xB7 score ${hit.score}`, "", `- **factory:** \`${field.factoryName}\``, `- **tier:** \`${field.tier}\``, `- **produces:** \`${field.produces}\`${array}`, `- **matched:** \`${hit.matchedTokens.join(", ")}\``, "", field.description, "", `\u2192 \`dbx_form_lookup topic="${field.slug}"\` for full docs.`, "");
20450
+ lines.push(
20451
+ `## \`${field.slug}\` \xB7 form \xB7 score ${hit.score}`,
20452
+ "",
20453
+ `- **factory:** \`${field.factoryName}\``,
20454
+ `- **tier:** \`${field.tier}\``,
20455
+ `- **produces:** \`${field.produces}\`${array}`,
20456
+ `- **matched:** \`${hit.matchedTokens.join(", ")}\``,
20457
+ "",
20458
+ field.description,
20459
+ "",
20460
+ `\u2192 \`dbx_form_lookup topic="${field.slug}"\` for full docs.`,
20461
+ ""
20462
+ );
20415
20463
  }
20416
20464
  return lines.join("\n").trimEnd();
20417
20465
  }
@@ -21463,7 +21511,19 @@ function formatSearchResults2(options) {
21463
21511
  } else {
21464
21512
  const lines = [`# Search: \`${query}\``, "", `Tokens: \`${tokenDisplay}\`${scopeLabel} \xB7 ${hits.length} result${hits.length === 1 ? "" : "s"}`, ""];
21465
21513
  for (const hit of hits) {
21466
- lines.push(`## \`${hit.entry.slug}\` \xB7 ${hit.entry.category} \xB7 score ${hit.score}`, "", `- **class:** \`${hit.entry.className}\``, `- **kind:** \`${hit.entry.kind}\``, `- **selector:** \`${hit.entry.selector}\``, `- **matched:** \`${hit.matchedTokens.join(", ")}\``, "", hit.entry.description, "", `\u2192 \`dbx_ui_lookup topic="${hit.entry.slug}"\` for full docs.`, "");
21514
+ lines.push(
21515
+ `## \`${hit.entry.slug}\` \xB7 ${hit.entry.category} \xB7 score ${hit.score}`,
21516
+ "",
21517
+ `- **class:** \`${hit.entry.className}\``,
21518
+ `- **kind:** \`${hit.entry.kind}\``,
21519
+ `- **selector:** \`${hit.entry.selector}\``,
21520
+ `- **matched:** \`${hit.matchedTokens.join(", ")}\``,
21521
+ "",
21522
+ hit.entry.description,
21523
+ "",
21524
+ `\u2192 \`dbx_ui_lookup topic="${hit.entry.slug}"\` for full docs.`,
21525
+ ""
21526
+ );
21467
21527
  }
21468
21528
  appendRelatedExamples(lines, hits, examplesRegistry);
21469
21529
  result = lines.join("\n").trimEnd();
@@ -25553,7 +25613,31 @@ function attachRemediation(code17) {
25553
25613
 
25554
25614
  // packages/dbx-cli/validate/src/lib/_core/rule-catalog/format.ts
25555
25615
  function formatRuleAsMarkdown(entry) {
25556
- const lines = [`# \`${entry.code}\``, "", `**${entry.title}**`, "", `- **Severity:** \`${entry.severity}\``, `- **Source tool:** \`${entry.source}\``, "", "## What it flags", "", entry.whatItFlags, "", "## When it applies", "", entry.whenItApplies, "", "## When it does NOT apply", "", entry.whenItDoesNotApply, "", "## Canonical fix", "", entry.canonicalFix, ""];
25616
+ const lines = [
25617
+ `# \`${entry.code}\``,
25618
+ "",
25619
+ `**${entry.title}**`,
25620
+ "",
25621
+ `- **Severity:** \`${entry.severity}\``,
25622
+ `- **Source tool:** \`${entry.source}\``,
25623
+ "",
25624
+ "## What it flags",
25625
+ "",
25626
+ entry.whatItFlags,
25627
+ "",
25628
+ "## When it applies",
25629
+ "",
25630
+ entry.whenItApplies,
25631
+ "",
25632
+ "## When it does NOT apply",
25633
+ "",
25634
+ entry.whenItDoesNotApply,
25635
+ "",
25636
+ "## Canonical fix",
25637
+ "",
25638
+ entry.canonicalFix,
25639
+ ""
25640
+ ];
25557
25641
  if (entry.canonicalFixTemplate) {
25558
25642
  lines.push("## Fix template", "", entry.canonicalFixTemplate, "");
25559
25643
  }
@@ -35894,7 +35978,15 @@ function appendGroupSection(lines, apiRel, group) {
35894
35978
  function appendFileTable(lines, group) {
35895
35979
  lines.push("", "| File | Kind | Subgroups | Canonical | Rename suggestion |", "|---|---|---|---|---|");
35896
35980
  for (const file of group.files) {
35897
- lines.push("| " + [code3(file.filename), KIND_LABEL[file.classification.kind], file.classification.subgroups.length === 0 ? "\u2014" : code3(file.classification.subgroups.join(".")), file.classification.isCanonical ? "\u2713" : "\u2717", file.classification.recommendedRename === void 0 ? "\u2014" : code3(file.classification.recommendedRename)].join(" | ") + " |");
35981
+ lines.push(
35982
+ "| " + [
35983
+ code3(file.filename),
35984
+ KIND_LABEL[file.classification.kind],
35985
+ file.classification.subgroups.length === 0 ? "\u2014" : code3(file.classification.subgroups.join(".")),
35986
+ file.classification.isCanonical ? "\u2713" : "\u2717",
35987
+ file.classification.recommendedRename === void 0 ? "\u2014" : code3(file.classification.recommendedRename)
35988
+ ].join(" | ") + " |"
35989
+ );
35898
35990
  }
35899
35991
  }
35900
35992
  function appendWhereToAddSection(lines, apiRel, group) {
@@ -35910,7 +36002,17 @@ function appendRecommendation(lines, rec, group) {
35910
36002
  lines.push("", `- **${rec.label}** \u2192 \`${rec.canonicalPath}\` ${status}`, ` ${rec.summary}`);
35911
36003
  }
35912
36004
  function appendConventionLegend(lines) {
35913
- lines.push("", "## Naming convention", "", "- `<group>.crud.spec.ts` \u2014 non-scenario CRUD tests.", "- `<group>.crud.<sub>[.<sub>...].spec.ts` \u2014 focused CRUD sub-test.", "- `<group>.scenario.spec.ts` \u2014 generic multi-step scenario tests.", "- `<group>.scenario.<sub>[.<sub>...].spec.ts` \u2014 focused scenario sub-bucket.", "", "Drift forms are still parsed but flagged \u2014 see the `Rename suggestion` column.");
36005
+ lines.push(
36006
+ "",
36007
+ "## Naming convention",
36008
+ "",
36009
+ "- `<group>.crud.spec.ts` \u2014 non-scenario CRUD tests.",
36010
+ "- `<group>.crud.<sub>[.<sub>...].spec.ts` \u2014 focused CRUD sub-test.",
36011
+ "- `<group>.scenario.spec.ts` \u2014 generic multi-step scenario tests.",
36012
+ "- `<group>.scenario.<sub>[.<sub>...].spec.ts` \u2014 focused scenario sub-bucket.",
36013
+ "",
36014
+ "Drift forms are still parsed but flagged \u2014 see the `Rename suggestion` column."
36015
+ );
35914
36016
  }
35915
36017
  function appendDriftSummary(lines, catalog) {
35916
36018
  const driftFiles = [];
@@ -36056,7 +36158,14 @@ function stringify(value) {
36056
36158
 
36057
36159
  // packages/dbx-cli/model-test/src/lib/fixture/format.markdown.ts
36058
36160
  function formatListAsMarkdown(extraction) {
36059
- const lines = [`# App fixtures \u2014 ${extraction.fixturePath}`, "", `Detected workspace prefix: \`${extraction.prefix ?? "(none)"}\``, `Identity imports: ${extraction.identityImports.length === 0 ? "_None._" : extraction.identityImports.map((s) => "`" + s + "`").join(", ")}`, "", `## Fixtures (${extraction.entries.length})`];
36161
+ const lines = [
36162
+ `# App fixtures \u2014 ${extraction.fixturePath}`,
36163
+ "",
36164
+ `Detected workspace prefix: \`${extraction.prefix ?? "(none)"}\``,
36165
+ `Identity imports: ${extraction.identityImports.length === 0 ? "_None._" : extraction.identityImports.map((s) => "`" + s + "`").join(", ")}`,
36166
+ "",
36167
+ `## Fixtures (${extraction.entries.length})`
36168
+ ];
36060
36169
  if (extraction.entries.length === 0) {
36061
36170
  lines.push("", "_No model fixtures found._");
36062
36171
  } else {
@@ -36113,7 +36222,15 @@ function nonModelDescription(entry) {
36113
36222
  return "Non-Firestore-model fixture.";
36114
36223
  }
36115
36224
  function appendLookupTriplet(lines, entry) {
36116
- lines.push("## Triplet", "", `- Fixture: \`${entry.fixtureClassName}\` (lines ${entry.fixtureLine}-${entry.fixtureEndLine})`, `- Instance: \`${entry.instanceClassName}\` (lines ${entry.instanceLine}-${entry.instanceEndLine})`, `- Params: \`${entry.paramsTypeName}\``, `- Factory: \`${entry.factoryName ?? "\u2014"}\``, `- Singleton: \`${entry.singletonName ?? "\u2014"}\``);
36225
+ lines.push(
36226
+ "## Triplet",
36227
+ "",
36228
+ `- Fixture: \`${entry.fixtureClassName}\` (lines ${entry.fixtureLine}-${entry.fixtureEndLine})`,
36229
+ `- Instance: \`${entry.instanceClassName}\` (lines ${entry.instanceLine}-${entry.instanceEndLine})`,
36230
+ `- Params: \`${entry.paramsTypeName}\``,
36231
+ `- Factory: \`${entry.factoryName ?? "\u2014"}\``,
36232
+ `- Singleton: \`${entry.singletonName ?? "\u2014"}\``
36233
+ );
36117
36234
  }
36118
36235
  function appendLookupGenerics(lines, entry) {
36119
36236
  if (entry.fixtureExtendsGenerics.length > 0) {
@@ -36283,11 +36400,19 @@ function renderParamsBlock(input, paramsTypeName, todos) {
36283
36400
  }
36284
36401
  function renderInstanceBlock(input) {
36285
36402
  input.todos.push(`Add instance methods to \`${input.instanceClassName}\`.`);
36286
- return [`export class ${input.instanceClassName}<F extends FirebaseAdminFunctionTestContextInstance = FirebaseAdminFunctionTestContextInstance> extends ModelTestContextInstance<${input.model}, ${input.modelDocumentTypeName}, ${input.prefix}FunctionContextFixtureInstance<F>> {`, " // TODO: implement instance-side methods (these own the real logic).", "}"].join("\n");
36403
+ return [
36404
+ `export class ${input.instanceClassName}<F extends FirebaseAdminFunctionTestContextInstance = FirebaseAdminFunctionTestContextInstance> extends ModelTestContextInstance<${input.model}, ${input.modelDocumentTypeName}, ${input.prefix}FunctionContextFixtureInstance<F>> {`,
36405
+ " // TODO: implement instance-side methods (these own the real logic).",
36406
+ "}"
36407
+ ].join("\n");
36287
36408
  }
36288
36409
  function renderFixtureBlock(input) {
36289
36410
  input.todos.push(`Add forwarder methods to \`${input.fixtureClassName}\` after defining instance methods.`);
36290
- return [`export class ${input.fixtureClassName}<F extends FirebaseAdminFunctionTestContextInstance = FirebaseAdminFunctionTestContextInstance> extends ModelTestContextFixture<${input.model}, ${input.modelDocumentTypeName}, ${input.prefix}FunctionContextFixtureInstance<F>, ${input.prefix}FunctionContextFixture<F>, ${input.instanceClassName}<F>> {`, " // TODO: forward instance methods. Run `dbx_model_fixture_forward` after adding instance methods.", "}"].join("\n");
36411
+ return [
36412
+ `export class ${input.fixtureClassName}<F extends FirebaseAdminFunctionTestContextInstance = FirebaseAdminFunctionTestContextInstance> extends ModelTestContextFixture<${input.model}, ${input.modelDocumentTypeName}, ${input.prefix}FunctionContextFixtureInstance<F>, ${input.prefix}FunctionContextFixture<F>, ${input.instanceClassName}<F>> {`,
36413
+ " // TODO: forward instance methods. Run `dbx_model_fixture_forward` after adding instance methods.",
36414
+ "}"
36415
+ ].join("\n");
36291
36416
  }
36292
36417
  function renderFactoryBlock(input) {
36293
36418
  const generics = renderFactoryGenerics(input);
@@ -36307,7 +36432,15 @@ function renderFactoryBlock(input) {
36307
36432
  return lines.join("\n");
36308
36433
  }
36309
36434
  function renderFactoryGenerics(input) {
36310
- const baseGenerics = [input.model, input.modelDocumentTypeName, input.paramsTypeName, `${input.prefix}FunctionContextFixtureInstance<FirebaseAdminFunctionTestContextInstance>`, `${input.prefix}FunctionContextFixture<FirebaseAdminFunctionTestContextInstance>`, `${input.instanceClassName}<FirebaseAdminFunctionTestContextInstance>`, `${input.fixtureClassName}<FirebaseAdminFunctionTestContextInstance>`];
36435
+ const baseGenerics = [
36436
+ input.model,
36437
+ input.modelDocumentTypeName,
36438
+ input.paramsTypeName,
36439
+ `${input.prefix}FunctionContextFixtureInstance<FirebaseAdminFunctionTestContextInstance>`,
36440
+ `${input.prefix}FunctionContextFixture<FirebaseAdminFunctionTestContextInstance>`,
36441
+ `${input.instanceClassName}<FirebaseAdminFunctionTestContextInstance>`,
36442
+ `${input.fixtureClassName}<FirebaseAdminFunctionTestContextInstance>`
36443
+ ];
36311
36444
  if (input.collectionGenericArg) {
36312
36445
  return [...baseGenerics, input.collectionGenericArg];
36313
36446
  }
@@ -36329,10 +36462,13 @@ function renderFactoryCallbacks(input) {
36329
36462
  } else {
36330
36463
  callbacks.push(`getCollection: (fi) => fi.${defaultCollectionsAccessor(input.prefix)}.${camelInitial(input.model)}Collection`);
36331
36464
  }
36332
- callbacks.push(`makeInstance: (delegate, ref, testInstance) => new ${input.instanceClassName}(delegate, ref, testInstance)`, `makeRef: async (collection, params, _p) => {
36465
+ callbacks.push(
36466
+ `makeInstance: (delegate, ref, testInstance) => new ${input.instanceClassName}(delegate, ref, testInstance)`,
36467
+ `makeRef: async (collection, params, _p) => {
36333
36468
  // TODO: choose the correct ref strategy (auto id vs deterministic id).
36334
36469
  return collection.documentAccessor().newDocument().documentRef;
36335
- }`);
36470
+ }`
36471
+ );
36336
36472
  if (input.withInitDocument) {
36337
36473
  callbacks.push(`initDocument: async (instance, params) => {
36338
36474
  // TODO: seed the new document with sensible defaults from \`params\`.
@@ -37532,7 +37668,18 @@ function formatReportAsJson4(report) {
37532
37668
  // packages/dbx-cli/validate/src/lib/dbx-asset-list-app/format.markdown.ts
37533
37669
  function formatReportAsMarkdown4(report) {
37534
37670
  const basename12 = report.componentDir.split("/").pop() ?? report.componentDir;
37535
- const lines = [`# App assets \u2014 ${basename12}`, "", `Component: \`${report.componentDir}\``, `App: \`${report.apiDir}\``, "", `\`src/lib/assets.ts\`: ${formatBool(report.assetsFileExists)}`, `Barrel re-exports \`./assets\`: ${formatBool(report.barrelReExportsAssets)}`, `\`provideDbxAssetLoader()\` called in \`src/root.app.config.ts\`: ${formatBool(report.providerWiredInApp)}`, "", `## Assets (${report.assets.length})`];
37671
+ const lines = [
37672
+ `# App assets \u2014 ${basename12}`,
37673
+ "",
37674
+ `Component: \`${report.componentDir}\``,
37675
+ `App: \`${report.apiDir}\``,
37676
+ "",
37677
+ `\`src/lib/assets.ts\`: ${formatBool(report.assetsFileExists)}`,
37678
+ `Barrel re-exports \`./assets\`: ${formatBool(report.barrelReExportsAssets)}`,
37679
+ `\`provideDbxAssetLoader()\` called in \`src/root.app.config.ts\`: ${formatBool(report.providerWiredInApp)}`,
37680
+ "",
37681
+ `## Assets (${report.assets.length})`
37682
+ ];
37536
37683
  if (report.assets.length === 0) {
37537
37684
  lines.push("", "_None found._");
37538
37685
  } else {
@@ -37923,7 +38070,18 @@ function formatReportAsJson6(report) {
37923
38070
  function formatReportAsMarkdown6(report) {
37924
38071
  const basename12 = report.componentDir.split("/").pop() ?? report.componentDir;
37925
38072
  const uploadServiceFactoryText = report.uploadServiceFactoryName ? code7(report.uploadServiceFactoryName) : "_Not defined._";
37926
- const lines = [`# App storagefiles \u2014 ${basename12}`, "", `Component: \`${report.componentDir}\``, `API: \`${report.apiDir}\``, "", `Upload service factory: ${uploadServiceFactoryText}`, `Wired via \`StorageFileInitializeFromUploadService\` provider: ${formatBool3(report.uploadServiceWiredInApi)}`, `Processing handler call present: ${formatBool3(report.processingHandlerWiredInApi)}`, "", `## Purposes (${report.purposes.length})`];
38073
+ const lines = [
38074
+ `# App storagefiles \u2014 ${basename12}`,
38075
+ "",
38076
+ `Component: \`${report.componentDir}\``,
38077
+ `API: \`${report.apiDir}\``,
38078
+ "",
38079
+ `Upload service factory: ${uploadServiceFactoryText}`,
38080
+ `Wired via \`StorageFileInitializeFromUploadService\` provider: ${formatBool3(report.uploadServiceWiredInApi)}`,
38081
+ `Processing handler call present: ${formatBool3(report.processingHandlerWiredInApi)}`,
38082
+ "",
38083
+ `## Purposes (${report.purposes.length})`
38084
+ ];
37927
38085
  if (report.purposes.length === 0) {
37928
38086
  lines.push("", "_None found._");
37929
38087
  } else {
@@ -41602,7 +41760,14 @@ function rootDocumentStoreBody(args) {
41602
41760
  if (args.functionsClass !== void 0) {
41603
41761
  packageImports.push(args.functionsClass);
41604
41762
  }
41605
- const lines = [`import { Injectable, inject } from '@angular/core';`, `import { ${joinUnique(dbxFirebaseImports)} } from '@dereekb/dbx-firebase';`, `import { ${joinUnique(packageImports)} } from '${args.firebasePackage}';`, "", `@Injectable()`, `export class ${storeClass} extends ${baseClass}<${args.modelName}, ${docType}> {`];
41763
+ const lines = [
41764
+ `import { Injectable, inject } from '@angular/core';`,
41765
+ `import { ${joinUnique(dbxFirebaseImports)} } from '@dereekb/dbx-firebase';`,
41766
+ `import { ${joinUnique(packageImports)} } from '${args.firebasePackage}';`,
41767
+ "",
41768
+ `@Injectable()`,
41769
+ `export class ${storeClass} extends ${baseClass}<${args.modelName}, ${docType}> {`
41770
+ ];
41606
41771
  if (args.functionsClass !== void 0) {
41607
41772
  lines.push(` readonly ${functionsFieldName(args.functionsClass)} = inject(${args.functionsClass});`, "");
41608
41773
  }
@@ -41645,11 +41810,29 @@ function subDocumentStoreBody(args) {
41645
41810
  if (args.functionsClass !== void 0) {
41646
41811
  packageImports.push(args.functionsClass);
41647
41812
  }
41648
- const lines = [`import { Injectable, inject } from '@angular/core';`, `import { ${joinUnique(dbxFirebaseImports)} } from '@dereekb/dbx-firebase';`, `import { ${joinUnique(packageImports)} } from '${args.firebasePackage}';`, `import { ${parentStore} } from './${parentBaseFile}.document.store';`, "", `@Injectable()`, `export class ${storeClass} extends ${baseClass}<${args.modelName}, ${parent.name}, ${docType}, ${parent.documentType}> {`];
41813
+ const lines = [
41814
+ `import { Injectable, inject } from '@angular/core';`,
41815
+ `import { ${joinUnique(dbxFirebaseImports)} } from '@dereekb/dbx-firebase';`,
41816
+ `import { ${joinUnique(packageImports)} } from '${args.firebasePackage}';`,
41817
+ `import { ${parentStore} } from './${parentBaseFile}.document.store';`,
41818
+ "",
41819
+ `@Injectable()`,
41820
+ `export class ${storeClass} extends ${baseClass}<${args.modelName}, ${parent.name}, ${docType}, ${parent.documentType}> {`
41821
+ ];
41649
41822
  if (args.functionsClass !== void 0) {
41650
41823
  lines.push(` readonly ${functionsFieldName(args.functionsClass)} = inject(${args.functionsClass});`, "");
41651
41824
  }
41652
- lines.push(` constructor() {`, ` const collections = inject(${collections});`, ` super({ collectionFactory: collections.${args.factoryAccessor}, firestoreCollectionLike: collections.${args.groupAccessor} });`, ` const parent = inject(${parentStore}, { optional: true });`, "", ` if (parent) {`, ` this.setParentStore(parent);`, ` }`, ` }`);
41825
+ lines.push(
41826
+ ` constructor() {`,
41827
+ ` const collections = inject(${collections});`,
41828
+ ` super({ collectionFactory: collections.${args.factoryAccessor}, firestoreCollectionLike: collections.${args.groupAccessor} });`,
41829
+ ` const parent = inject(${parentStore}, { optional: true });`,
41830
+ "",
41831
+ ` if (parent) {`,
41832
+ ` this.setParentStore(parent);`,
41833
+ ` }`,
41834
+ ` }`
41835
+ );
41653
41836
  const crudLines = renderCrudLines(args, "this");
41654
41837
  if (crudLines.length > 0) {
41655
41838
  lines.push("", ...crudLines);
@@ -41694,7 +41877,14 @@ function systemStateAccessorBody(args) {
41694
41877
  if (args.functionsClass !== void 0) {
41695
41878
  packageImports.push(args.functionsClass);
41696
41879
  }
41697
- const lines = [`import { Injectable, inject } from '@angular/core';`, `import { ${joinUnique(dbxFirebaseImports)} } from '@dereekb/dbx-firebase';`, `import { ${joinUnique(packageImports)} } from '${args.firebasePackage}';`, "", `@Injectable()`, `export class ${accessorClass} extends AbstractSystemStateDocumentStoreAccessor<${dataType}> {`];
41880
+ const lines = [
41881
+ `import { Injectable, inject } from '@angular/core';`,
41882
+ `import { ${joinUnique(dbxFirebaseImports)} } from '@dereekb/dbx-firebase';`,
41883
+ `import { ${joinUnique(packageImports)} } from '${args.firebasePackage}';`,
41884
+ "",
41885
+ `@Injectable()`,
41886
+ `export class ${accessorClass} extends AbstractSystemStateDocumentStoreAccessor<${dataType}> {`
41887
+ ];
41698
41888
  if (args.functionsClass !== void 0) {
41699
41889
  lines.push(` readonly ${functionsFieldName(args.functionsClass)} = inject(${args.functionsClass});`, "");
41700
41890
  }
@@ -42277,7 +42467,17 @@ var ForwardArgsType = type62({
42277
42467
  });
42278
42468
  var TOOL8 = {
42279
42469
  name: "dbx_model_fixture_forward",
42280
- description: ["Insert the missing Fixture forwarder methods for one model in `<apiDir>/src/test/fixture.ts`. Every public Instance method that lacks a Fixture method gains a thin wrapper:", "", "```ts", "async <name>(<paramText>): <returnType> {", " return this.instance.<name>(<args>);", "}", "```", "", "Existing forwarders are left untouched (idempotent). Pass `methods` to limit the run to specific method names; omit to forward everything that's missing."].join("\n"),
42470
+ description: [
42471
+ "Insert the missing Fixture forwarder methods for one model in `<apiDir>/src/test/fixture.ts`. Every public Instance method that lacks a Fixture method gains a thin wrapper:",
42472
+ "",
42473
+ "```ts",
42474
+ "async <name>(<paramText>): <returnType> {",
42475
+ " return this.instance.<name>(<args>);",
42476
+ "}",
42477
+ "```",
42478
+ "",
42479
+ "Existing forwarders are left untouched (idempotent). Pass `methods` to limit the run to specific method names; omit to forward everything that's missing."
42480
+ ].join("\n"),
42281
42481
  inputSchema: {
42282
42482
  type: "object",
42283
42483
  properties: {
@@ -42756,7 +42956,11 @@ function bucketSummary(group, bucket) {
42756
42956
  }
42757
42957
  return "Multi-step scenarios using fixture chains (mirrors real workflows). Add focused buckets as `" + group + ".scenario.<sub>.spec.ts` (chain subgroups freely, e.g. `.scenario.requirement.worker.spec.ts`).";
42758
42958
  }
42759
- var DRIFT_RULES = ["`<group>.<sub>.crud.spec.ts` \u2192 rename to `<group>.crud.<sub>.spec.ts` (`crud` belongs directly after the group name).", "`<group>.<sub>.scenario.spec.ts` \u2192 rename to `<group>.scenario.<sub>.spec.ts` (`scenario` belongs directly after the group name).", "`<group>.<rest>.spec.ts` with no `crud` / `scenario` segment \u2192 add one (default to `scenario` when in doubt)."];
42959
+ var DRIFT_RULES = [
42960
+ "`<group>.<sub>.crud.spec.ts` \u2192 rename to `<group>.crud.<sub>.spec.ts` (`crud` belongs directly after the group name).",
42961
+ "`<group>.<sub>.scenario.spec.ts` \u2192 rename to `<group>.scenario.<sub>.spec.ts` (`scenario` belongs directly after the group name).",
42962
+ "`<group>.<rest>.spec.ts` with no `crud` / `scenario` segment \u2192 add one (default to `scenario` when in doubt)."
42963
+ ];
42760
42964
  function formatMarkdown(report) {
42761
42965
  const lines = [];
42762
42966
  lines.push(`# Spec convention for \`${report.group}\``, "", `Tests live under \`${report.apiDir}/src/app/function/${report.group}/\`. Canonical name segments:`, "");
@@ -43536,7 +43740,14 @@ var ARCHETYPE_RECOMMEND_TOOL = {
43536
43740
  import { type as type70 } from "arktype";
43537
43741
  var DBX_MODEL_ARCHETYPE_LOOKUP_TOOL = {
43538
43742
  name: "dbx_model_archetype_lookup",
43539
- description: ["Look up an archetype in the model-archetype catalog. Accepts:", ' \u2022 a slug (`"root-entity"`, `"denormalised-aggregate"`, \u2026);', ' \u2022 the literal `"list"` for the full catalog grouped by family.', "", "Optional inputs:", ' \u2022 `axes`: optional axis filter (`{ "subPurpose": "private" }`) \u2014 when set, the description shifts to focus on the matched axis.'].join("\n"),
43743
+ description: [
43744
+ "Look up an archetype in the model-archetype catalog. Accepts:",
43745
+ ' \u2022 a slug (`"root-entity"`, `"denormalised-aggregate"`, \u2026);',
43746
+ ' \u2022 the literal `"list"` for the full catalog grouped by family.',
43747
+ "",
43748
+ "Optional inputs:",
43749
+ ' \u2022 `axes`: optional axis filter (`{ "subPurpose": "private" }`) \u2014 when set, the description shifts to focus on the matched axis.'
43750
+ ].join("\n"),
43540
43751
  inputSchema: {
43541
43752
  type: "object",
43542
43753
  properties: {
@@ -44929,7 +45140,18 @@ function formatStateInteraction(entry) {
44929
45140
  return result;
44930
45141
  }
44931
45142
  function formatDirectiveEntry(entry, depth) {
44932
- const lines = [`# ${entry.className}`, "", entry.description, "", bullet("selector", `\`${entry.selector}\``), bullet("module", `\`${entry.module}\``), bullet("slug", `\`${entry.slug}\``), bullet("produces context", entry.producesContext ? "yes (provides `ActionContextStore` via DI)" : "no"), bullet("consumes context", entry.consumesContext ? "yes (injects `DbxActionContextStoreSourceInstance`)" : "no"), ""];
45143
+ const lines = [
45144
+ `# ${entry.className}`,
45145
+ "",
45146
+ entry.description,
45147
+ "",
45148
+ bullet("selector", `\`${entry.selector}\``),
45149
+ bullet("module", `\`${entry.module}\``),
45150
+ bullet("slug", `\`${entry.slug}\``),
45151
+ bullet("produces context", entry.producesContext ? "yes (provides `ActionContextStore` via DI)" : "no"),
45152
+ bullet("consumes context", entry.consumesContext ? "yes (injects `DbxActionContextStoreSourceInstance`)" : "no"),
45153
+ ""
45154
+ ];
44933
45155
  if (depth === "full") {
44934
45156
  lines.push("## Inputs", "", entry.inputs.length > 0 ? formatInputsTable2(entry.inputs) : "_No inputs._", "");
44935
45157
  if (entry.outputs.length > 0) {
@@ -45692,7 +45914,15 @@ export class TagEditorComponent {
45692
45914
  };
45693
45915
 
45694
45916
  // packages/dbx-components-mcp/src/tools/data/patterns/action-patterns.ts
45695
- var ACTION_EXAMPLE_PATTERNS = [ACTION_PATTERN_BUTTON_CONFIRM_DELETE, ACTION_PATTERN_FORM_SUBMIT, ACTION_PATTERN_FORM_WITH_SECONDARY, ACTION_PATTERN_AUTO_TRIGGER_ON_MODIFY, ACTION_PATTERN_DISABLED_BY_KEY, ACTION_PATTERN_VALUE_GETTER_ON_TRIGGER, ACTION_PATTERN_PROVIDE_CONTEXT_UP];
45917
+ var ACTION_EXAMPLE_PATTERNS = [
45918
+ ACTION_PATTERN_BUTTON_CONFIRM_DELETE,
45919
+ ACTION_PATTERN_FORM_SUBMIT,
45920
+ ACTION_PATTERN_FORM_WITH_SECONDARY,
45921
+ ACTION_PATTERN_AUTO_TRIGGER_ON_MODIFY,
45922
+ ACTION_PATTERN_DISABLED_BY_KEY,
45923
+ ACTION_PATTERN_VALUE_GETTER_ON_TRIGGER,
45924
+ ACTION_PATTERN_PROVIDE_CONTEXT_UP
45925
+ ];
45696
45926
  function getActionExamplePattern(slug) {
45697
45927
  const lowered = slug.trim().toLowerCase();
45698
45928
  return ACTION_EXAMPLE_PATTERNS.find((p) => p.slug === lowered);
@@ -46016,7 +46246,27 @@ function renderScaffold3(args) {
46016
46246
  const directivesUsed = buildDirectivesUsed(args);
46017
46247
  const examplePattern = relatedExamplePattern(args);
46018
46248
  const lines = [];
46019
- lines.push(`# Action scaffold \u2014 ${args.useCase}`, "", `Trigger: \`${args.trigger}\` \xB7 Confirm: \`${args.confirm}\` \xB7 Success feedback: \`${args.successFeedback}\` \xB7 Context: \`${args.contextProvider}\``, "", "## Template", "", "```html", template, "```", "", "## Component class", "", "```ts", handler, "```", "", "## Imports", "", "```ts");
46249
+ lines.push(
46250
+ `# Action scaffold \u2014 ${args.useCase}`,
46251
+ "",
46252
+ `Trigger: \`${args.trigger}\` \xB7 Confirm: \`${args.confirm}\` \xB7 Success feedback: \`${args.successFeedback}\` \xB7 Context: \`${args.contextProvider}\``,
46253
+ "",
46254
+ "## Template",
46255
+ "",
46256
+ "```html",
46257
+ template,
46258
+ "```",
46259
+ "",
46260
+ "## Component class",
46261
+ "",
46262
+ "```ts",
46263
+ handler,
46264
+ "```",
46265
+ "",
46266
+ "## Imports",
46267
+ "",
46268
+ "```ts"
46269
+ );
46020
46270
  for (const line of imports) {
46021
46271
  lines.push(line);
46022
46272
  }
@@ -46460,7 +46710,13 @@ function formatMissingRouteModelLine(param) {
46460
46710
  // packages/dbx-components-mcp/src/tools/route-lookup.tool.ts
46461
46711
  var DBX_ROUTE_LOOKUP_TOOL = {
46462
46712
  name: "dbx_route_lookup",
46463
- description: ["Look up a single UIRouter state by name, full URL, or component class name. Returns the state's URL, component, parent chain, params, resolves, siblings, and children.", "", "Provide at least one of `sources` / `paths` / `glob` to point the tool at the source set, plus a `topic`.", "", "Resolution order: exact state name \u2192 URL path \u2192 component class name \u2192 fuzzy substring (returns up to 5 candidates)."].join("\n"),
46713
+ description: [
46714
+ "Look up a single UIRouter state by name, full URL, or component class name. Returns the state's URL, component, parent chain, params, resolves, siblings, and children.",
46715
+ "",
46716
+ "Provide at least one of `sources` / `paths` / `glob` to point the tool at the source set, plus a `topic`.",
46717
+ "",
46718
+ "Resolution order: exact state name \u2192 URL path \u2192 component class name \u2192 fuzzy substring (returns up to 5 candidates)."
46719
+ ].join("\n"),
46464
46720
  inputSchema: {
46465
46721
  type: "object",
46466
46722
  properties: {
@@ -46720,7 +46976,11 @@ var ROUTE_LOOKUP_TOOL = {
46720
46976
  import { type as type82 } from "arktype";
46721
46977
  var DBX_ROUTE_SEARCH_TOOL = {
46722
46978
  name: "dbx_route_search",
46723
- description: ["Search the UIRouter state tree of a dbx-components app. Matches against state names, full URLs, component class names, resolve keys, and source-file paths. Returns the top 10 results with state name, URL, component, and source location.", "", "Provide at least one of `sources` / `paths` / `glob` plus a `query`. `scope` narrows to a single axis (`name`, `url`, `component`, `resolve`)."].join("\n"),
46979
+ description: [
46980
+ "Search the UIRouter state tree of a dbx-components app. Matches against state names, full URLs, component class names, resolve keys, and source-file paths. Returns the top 10 results with state name, URL, component, and source location.",
46981
+ "",
46982
+ "Provide at least one of `sources` / `paths` / `glob` plus a `query`. `scope` narrows to a single axis (`name`, `url`, `component`, `resolve`)."
46983
+ ].join("\n"),
46724
46984
  inputSchema: {
46725
46985
  type: "object",
46726
46986
  properties: {
@@ -47812,7 +48072,25 @@ function formatScaffold2(args) {
47812
48072
  if (args.presetKeys.length > 0) {
47813
48073
  lines.push("## Presets", "", "```ts", renderPresetConstants(args), "```", "");
47814
48074
  }
47815
- lines.push("## Template", "", "```html", renderTemplate(args), "```", "", "## Component class", "", "```ts", ...renderImports(args), "", renderClass(args), "```", "", "## Notes", "", "- The outer `[dbxFilterSourceConnector]` provides both `FilterSource` and `FilterSourceConnector`. Children that own the form use `[dbxFilterSource]` + `[dbxFilterConnectSource]` to wire up.");
48075
+ lines.push(
48076
+ "## Template",
48077
+ "",
48078
+ "```html",
48079
+ renderTemplate(args),
48080
+ "```",
48081
+ "",
48082
+ "## Component class",
48083
+ "",
48084
+ "```ts",
48085
+ ...renderImports(args),
48086
+ "",
48087
+ renderClass(args),
48088
+ "```",
48089
+ "",
48090
+ "## Notes",
48091
+ "",
48092
+ "- The outer `[dbxFilterSourceConnector]` provides both `FilterSource` and `FilterSourceConnector`. Children that own the form use `[dbxFilterSource]` + `[dbxFilterConnectSource]` to wire up."
48093
+ );
47816
48094
  if (args.presetKeys.length > 0) {
47817
48095
  lines.push("- Each `ClickableFilterPreset` listed in the preset array becomes a chip; clicking sets `presetValue` on the filter source.");
47818
48096
  }
@@ -47879,7 +48157,20 @@ function formatArgsTable(args) {
47879
48157
  return lines.join("\n");
47880
48158
  }
47881
48159
  function formatEntry4(entry, depth) {
47882
- const lines = [`# ${entry.className}`, "", entry.description, "", bullet3("slug", `\`${entry.slug}\``), bullet3("pipe", `\`${entry.pipeName}\``), bullet3("category", `\`${entry.category}\``), bullet3("purity", `\`${entry.purity}\``), bullet3("input", `\`${entry.inputType}\``), bullet3("output", `\`${entry.outputType}\``), bullet3("module", `\`${entry.module}\``), ""];
48160
+ const lines = [
48161
+ `# ${entry.className}`,
48162
+ "",
48163
+ entry.description,
48164
+ "",
48165
+ bullet3("slug", `\`${entry.slug}\``),
48166
+ bullet3("pipe", `\`${entry.pipeName}\``),
48167
+ bullet3("category", `\`${entry.category}\``),
48168
+ bullet3("purity", `\`${entry.purity}\``),
48169
+ bullet3("input", `\`${entry.inputType}\``),
48170
+ bullet3("output", `\`${entry.outputType}\``),
48171
+ bullet3("module", `\`${entry.module}\``),
48172
+ ""
48173
+ ];
47883
48174
  if (depth === "full") {
47884
48175
  if (entry.args.length > 0) {
47885
48176
  lines.push("## Args", "", formatArgsTable(entry.args), "");
@@ -48041,7 +48332,20 @@ function formatSearchResults5(input) {
48041
48332
  const lines = [`# Search: \`${query}\``, "", `Tokens: \`${tokenDisplay}\` \xB7 ${hits.length} result${hits.length === 1 ? "" : "s"}`, ""];
48042
48333
  for (const hit of hits) {
48043
48334
  const entry = hit.entry;
48044
- lines.push(`## \`${entry.slug}\` \xB7 ${entry.category} \xB7 score ${hit.score}`, "", `- **pipe:** \`${entry.pipeName}\``, `- **class:** \`${entry.className}\``, `- **input:** \`${entry.inputType}\``, `- **output:** \`${entry.outputType}\``, `- **matched:** \`${hit.matchedTokens.join(", ")}\``, "", entry.description, "", `\u2192 \`dbx_pipe_lookup topic="${entry.slug}"\` for full docs.`, "");
48335
+ lines.push(
48336
+ `## \`${entry.slug}\` \xB7 ${entry.category} \xB7 score ${hit.score}`,
48337
+ "",
48338
+ `- **pipe:** \`${entry.pipeName}\``,
48339
+ `- **class:** \`${entry.className}\``,
48340
+ `- **input:** \`${entry.inputType}\``,
48341
+ `- **output:** \`${entry.outputType}\``,
48342
+ `- **matched:** \`${hit.matchedTokens.join(", ")}\``,
48343
+ "",
48344
+ entry.description,
48345
+ "",
48346
+ `\u2192 \`dbx_pipe_lookup topic="${entry.slug}"\` for full docs.`,
48347
+ ""
48348
+ );
48045
48349
  }
48046
48350
  return lines.join("\n").trimEnd();
48047
48351
  }
@@ -48317,7 +48621,20 @@ function formatCatalogHit(input) {
48317
48621
  const tagDisplay = entry.tags.length > 0 ? `
48318
48622
  - **tags:** ${tagBadges}` : "";
48319
48623
  const badge = options.headerBadge ?? "";
48320
- return [`## \`${entry.slug}\` \xB7 \`${entry.name}\` \xB7 ${entry.kind}${badge} \xB7 score ${hit.score}`, "", `- **module:** \`${entry.module}\``, `- **category:** \`${entry.category}\``, `- **subpath:** \`${entry.subpath}\``, `- **signature:** \`${entry.signature}\``, `- **matched:** \`${hit.matchedTokens.join(", ")}\`${tagDisplay}`, "", entry.description.split("\n")[0], "", `\u2192 \`${options.lookupTool} topic="${entry.slug}"\` for full docs.`, ""];
48624
+ return [
48625
+ `## \`${entry.slug}\` \xB7 \`${entry.name}\` \xB7 ${entry.kind}${badge} \xB7 score ${hit.score}`,
48626
+ "",
48627
+ `- **module:** \`${entry.module}\``,
48628
+ `- **category:** \`${entry.category}\``,
48629
+ `- **subpath:** \`${entry.subpath}\``,
48630
+ `- **signature:** \`${entry.signature}\``,
48631
+ `- **matched:** \`${hit.matchedTokens.join(", ")}\`${tagDisplay}`,
48632
+ "",
48633
+ entry.description.split("\n")[0],
48634
+ "",
48635
+ `\u2192 \`${options.lookupTool} topic="${entry.slug}"\` for full docs.`,
48636
+ ""
48637
+ ];
48321
48638
  }
48322
48639
  function formatCatalogSearchResults(input) {
48323
48640
  const { query, tokens, hits, filters, entityLabel, emptyHint, hitOptions } = input;
@@ -48493,7 +48810,20 @@ function appendFullEntrySections2(lines, entry) {
48493
48810
  }
48494
48811
  function formatEntry6(entry, depth) {
48495
48812
  const heading = `# ${entry.name}`;
48496
- const lines = [heading, "", entry.description, "", bullet5("slug", `\`${entry.slug}\``), bullet5("kind", `\`${entry.kind}\``), bullet5("category", `\`${entry.category}\``), bullet5("module", `\`${entry.module}\``), bullet5("subpath", `\`${entry.subpath}\``), bullet5("optional", entry.optional ? "yes (use this for `Maybe<T>` fields)" : "no (required)"), bullet5("signature", `\`${entry.signature}\``), ""];
48813
+ const lines = [
48814
+ heading,
48815
+ "",
48816
+ entry.description,
48817
+ "",
48818
+ bullet5("slug", `\`${entry.slug}\``),
48819
+ bullet5("kind", `\`${entry.kind}\``),
48820
+ bullet5("category", `\`${entry.category}\``),
48821
+ bullet5("module", `\`${entry.module}\``),
48822
+ bullet5("subpath", `\`${entry.subpath}\``),
48823
+ bullet5("optional", entry.optional ? "yes (use this for `Maybe<T>` fields)" : "no (required)"),
48824
+ bullet5("signature", `\`${entry.signature}\``),
48825
+ ""
48826
+ ];
48497
48827
  appendDeprecationNotice2(lines, entry);
48498
48828
  if (depth === "full") {
48499
48829
  appendFullEntrySections2(lines, entry);
@@ -49823,7 +50153,14 @@ function appendTaggedSections(lines, tagged) {
49823
50153
  }
49824
50154
  function appendUntaggedSection(lines, untagged) {
49825
50155
  if (untagged.length === 0) return;
49826
- lines.push("## Untagged candidates", "", "Exported functions returning `FirestoreQueryConstraint[]` without an `@dbxModelFirebaseIndex` block. Tag them so the validator can verify their indexes \u2014 or annotate with `@dbxModelFirebaseIndexSkip` to record that they were considered.", "", "| Function | File |", "| --- | --- |");
50156
+ lines.push(
50157
+ "## Untagged candidates",
50158
+ "",
50159
+ "Exported functions returning `FirestoreQueryConstraint[]` without an `@dbxModelFirebaseIndex` block. Tag them so the validator can verify their indexes \u2014 or annotate with `@dbxModelFirebaseIndexSkip` to record that they were considered.",
50160
+ "",
50161
+ "| Function | File |",
50162
+ "| --- | --- |"
50163
+ );
49827
50164
  for (const candidate of untagged) {
49828
50165
  lines.push(`| \`${candidate.name}\` | \`${candidate.subpath}:${candidate.line}\` |`);
49829
50166
  }
@@ -50523,7 +50860,14 @@ function formatReportAsMarkdown17(report) {
50523
50860
  } else {
50524
50861
  lines.push("## \u2705 In sync", "");
50525
50862
  }
50526
- lines.push(`- generated composites: ${report.generatedComposites}`, `- existing composites: ${report.existingComposites}`, `- generated fieldOverrides: ${report.generatedFieldOverrides}`, `- existing fieldOverrides: ${report.existingFieldOverrides}`, `- ${report.errorCount} error(s), ${report.warningCount} warning(s)`, "");
50863
+ lines.push(
50864
+ `- generated composites: ${report.generatedComposites}`,
50865
+ `- existing composites: ${report.existingComposites}`,
50866
+ `- generated fieldOverrides: ${report.generatedFieldOverrides}`,
50867
+ `- existing fieldOverrides: ${report.existingFieldOverrides}`,
50868
+ `- ${report.errorCount} error(s), ${report.warningCount} warning(s)`,
50869
+ ""
50870
+ );
50527
50871
  if (report.violations.length > 0) {
50528
50872
  appendViolationSection(
50529
50873
  lines,
@@ -50941,7 +51285,10 @@ function renderStorageFilePurpose(input, ctx) {
50941
51285
  tokens: ctx.tokens,
50942
51286
  files: [componentFile, handlerFile],
50943
51287
  wiring,
50944
- summary: applyTokens(`Scaffolds a new \`<<SCREAMING>>_PURPOSE\` storage-file purpose end-to-end: component-side constants + path helpers (append to \`storagefile.ts\`) and an API-side upload-initializer factory in \`handlers/\`. After applying, run \`dbx_validate_app_storagefiles\` and \`dbx_validate_storagefile_folder\` against your project \u2014 both should PASS.`, ctx)
51288
+ summary: applyTokens(
51289
+ `Scaffolds a new \`<<SCREAMING>>_PURPOSE\` storage-file purpose end-to-end: component-side constants + path helpers (append to \`storagefile.ts\`) and an API-side upload-initializer factory in \`handlers/\`. After applying, run \`dbx_validate_app_storagefiles\` and \`dbx_validate_storagefile_folder\` against your project \u2014 both should PASS.`,
51290
+ ctx
51291
+ )
50945
51292
  };
50946
51293
  return result;
50947
51294
  }
@@ -50949,19 +51296,28 @@ function renderNotificationTemplate(input, ctx) {
50949
51296
  const componentFile = {
50950
51297
  status: "append",
50951
51298
  path: applyTokens("<<componentDir>>/src/lib/model/notification/notification.ts", ctx),
50952
- description: applyTokens(`Append the \`<<Pascal>>\` notification block to \`notification.ts\`: type constant + info + data interface + template factory. Reuses the file's existing imports \u2014 no new top-level imports required as long as \`NotificationTemplateType\`, \`NotificationTemplateTypeInfo\`, \`CreateNotificationTemplate\`, \`createNotificationTemplate\`, \`FirebaseAuthUserId\`, \`profileIdentity\`, and \`ProfileDocument\` are already in scope.`, ctx),
51299
+ description: applyTokens(
51300
+ `Append the \`<<Pascal>>\` notification block to \`notification.ts\`: type constant + info + data interface + template factory. Reuses the file's existing imports \u2014 no new top-level imports required as long as \`NotificationTemplateType\`, \`NotificationTemplateTypeInfo\`, \`CreateNotificationTemplate\`, \`createNotificationTemplate\`, \`FirebaseAuthUserId\`, \`profileIdentity\`, and \`ProfileDocument\` are already in scope.`,
51301
+ ctx
51302
+ ),
50953
51303
  content: applyTokens(NOTIFICATION_TEMPLATE_COMPONENT_TEMPLATE, ctx)
50954
51304
  };
50955
51305
  const factoryFile = {
50956
51306
  status: "append",
50957
51307
  path: applyTokens("<<apiDir>>/src/app/common/model/notification/notification.factory.ts", ctx),
50958
- description: applyTokens(`Append the \`<<appCamel>><<Pascal>>NotificationFactory\` to \`notification.factory.ts\`. Reuses the file's existing imports for \`NotificationMessageFunctionFactoryConfig\`, \`NotificationMessageInputContext\`, \`NotificationMessageContent\`, \`NotificationMessage\`, \`notificationMessageFunction\`, and \`NotificationTemplateServiceTypeConfig\`.`, ctx),
51308
+ description: applyTokens(
51309
+ `Append the \`<<appCamel>><<Pascal>>NotificationFactory\` to \`notification.factory.ts\`. Reuses the file's existing imports for \`NotificationMessageFunctionFactoryConfig\`, \`NotificationMessageInputContext\`, \`NotificationMessageContent\`, \`NotificationMessage\`, \`notificationMessageFunction\`, and \`NotificationTemplateServiceTypeConfig\`.`,
51310
+ ctx
51311
+ ),
50959
51312
  content: applyTokens(NOTIFICATION_TEMPLATE_FACTORY_TEMPLATE, ctx)
50960
51313
  };
50961
51314
  const wiring = [
50962
51315
  {
50963
51316
  file: applyTokens("<<apiDir>>/src/app/common/model/notification/notification.factory.ts", ctx),
50964
- description: applyTokens(`Two registrations are required: (1) add a call to \`<<appCamel>><<Pascal>>NotificationFactory(context)\` in the existing configs-array factory return list (so the handler is reachable through the template service); (2) add \`<<SCREAMING>>_NOTIFICATION_TEMPLATE_TYPE_INFO\` to the \`*_NOTIFICATION_TEMPLATE_TYPE_INFO_RECORD\` aggregator in \`notification.ts\` so the metadata path is wired.`, ctx),
51317
+ description: applyTokens(
51318
+ `Two registrations are required: (1) add a call to \`<<appCamel>><<Pascal>>NotificationFactory(context)\` in the existing configs-array factory return list (so the handler is reachable through the template service); (2) add \`<<SCREAMING>>_NOTIFICATION_TEMPLATE_TYPE_INFO\` to the \`*_NOTIFICATION_TEMPLATE_TYPE_INFO_RECORD\` aggregator in \`notification.ts\` so the metadata path is wired.`,
51319
+ ctx
51320
+ ),
50965
51321
  snippet: applyTokens(NOTIFICATION_TEMPLATE_WIRING_SNIPPET, ctx)
50966
51322
  }
50967
51323
  ];
@@ -50970,7 +51326,10 @@ function renderNotificationTemplate(input, ctx) {
50970
51326
  tokens: ctx.tokens,
50971
51327
  files: [componentFile, factoryFile],
50972
51328
  wiring,
50973
- summary: applyTokens(`Scaffolds a new \`<<SCREAMING>>_NOTIFICATION_TEMPLATE_TYPE\` end-to-end: component-side type + info + data + template factory (append to \`notification.ts\`) and an API-side template-service factory (append to \`notification.factory.ts\`). After applying the wiring, run \`dbx_notification_m_validate_app\` and \`dbx_notification_m_validate_folder\` against your project \u2014 both should PASS.`, ctx)
51329
+ summary: applyTokens(
51330
+ `Scaffolds a new \`<<SCREAMING>>_NOTIFICATION_TEMPLATE_TYPE\` end-to-end: component-side type + info + data + template factory (append to \`notification.ts\`) and an API-side template-service factory (append to \`notification.factory.ts\`). After applying the wiring, run \`dbx_notification_m_validate_app\` and \`dbx_notification_m_validate_folder\` against your project \u2014 both should PASS.`,
51331
+ ctx
51332
+ )
50974
51333
  };
50975
51334
  return result;
50976
51335
  }
@@ -50995,7 +51354,10 @@ function renderNotificationTask(input, ctx) {
50995
51354
  const wiring = [
50996
51355
  {
50997
51356
  file: applyTokens("<<apiDir>>/src/app/common/model/notification/notification.task.service.ts", ctx),
50998
- description: applyTokens(`Import the new factory, instantiate it bound to \`<<camel>>Handler\`, push that name into the existing \`handlers\` array, and add \`<<SCREAMING>>_NOTIFICATION_TASK_TYPE\` to the \`ALL_NOTIFICATION_TASK_TYPES\` aggregate in the component package's \`notification.task.ts\` so the validate-vs-handler parity check stays consistent.`, ctx),
51357
+ description: applyTokens(
51358
+ `Import the new factory, instantiate it bound to \`<<camel>>Handler\`, push that name into the existing \`handlers\` array, and add \`<<SCREAMING>>_NOTIFICATION_TASK_TYPE\` to the \`ALL_NOTIFICATION_TASK_TYPES\` aggregate in the component package's \`notification.task.ts\` so the validate-vs-handler parity check stays consistent.`,
51359
+ ctx
51360
+ ),
50999
51361
  snippet: applyTokens(NOTIFICATION_TASK_WIRING_SNIPPET, ctx)
51000
51362
  }
51001
51363
  ];
@@ -51004,7 +51366,10 @@ function renderNotificationTask(input, ctx) {
51004
51366
  tokens: ctx.tokens,
51005
51367
  files: [componentFile, handlerFile],
51006
51368
  wiring,
51007
- summary: applyTokens(`Scaffolds a new \`<<SCREAMING>>_NOTIFICATION_TASK_TYPE\` end-to-end: component-side type + checkpoint alias + data + template factory (append to \`notification.task.ts\`) and an API-side handler factory in \`handlers/\`. After applying the wiring (handlers array + ALL_NOTIFICATION_TASK_TYPES), run \`dbx_notification_m_validate_app\` and \`dbx_notification_m_validate_folder\` against your project \u2014 both should PASS.`, ctx)
51369
+ summary: applyTokens(
51370
+ `Scaffolds a new \`<<SCREAMING>>_NOTIFICATION_TASK_TYPE\` end-to-end: component-side type + checkpoint alias + data + template factory (append to \`notification.task.ts\`) and an API-side handler factory in \`handlers/\`. After applying the wiring (handlers array + ALL_NOTIFICATION_TASK_TYPES), run \`dbx_notification_m_validate_app\` and \`dbx_notification_m_validate_folder\` against your project \u2014 both should PASS.`,
51371
+ ctx
51372
+ )
51008
51373
  };
51009
51374
  return result;
51010
51375
  }
@@ -51269,7 +51634,11 @@ var STORAGEFILE_PURPOSE = {
51269
51634
  heading: "API upload handler",
51270
51635
  path: "<apiDir>/src/app/common/model/storagefile/handlers/upload.<name>.ts",
51271
51636
  altPaths: ["Inline in `<apiDir>/src/app/common/model/storagefile/storagefile.upload.service.ts` for tiny workspaces."],
51272
- body: ["**Required export:** `<camelName>StorageFileUploadInitializers(context): StorageFileInitializeFromUploadServiceInitializer[]`.", "", "**Wire into:** `<apiDir>/src/app/common/model/storagefile/storagefile.upload.service.ts` \u2014 spread into the `initializer: [...]` array passed to `storageFileInitializeFromUploadService({ initializer })`."].join("\n")
51637
+ body: [
51638
+ "**Required export:** `<camelName>StorageFileUploadInitializers(context): StorageFileInitializeFromUploadServiceInitializer[]`.",
51639
+ "",
51640
+ "**Wire into:** `<apiDir>/src/app/common/model/storagefile/storagefile.upload.service.ts` \u2014 spread into the `initializer: [...]` array passed to `storageFileInitializeFromUploadService({ initializer })`."
51641
+ ].join("\n")
51273
51642
  },
51274
51643
  {
51275
51644
  heading: "API processor (only if subtasks declared)",
@@ -51331,12 +51700,27 @@ var STORAGEFILE_PROCESSOR = {
51331
51700
  {
51332
51701
  heading: "API processor handler",
51333
51702
  path: "<apiDir>/src/app/common/model/notification/handlers/storagefile/task.handler.storagefile.<name>.ts",
51334
- body: ["**Required shape:**", "```ts", "export function <camelName>ProcessingNotificationTaskHandler(context): StorageFileProcessingPurposeSubtaskProcessorConfig<<Name>SubtaskMetadata, <Name>ProcessingSubtask> {", " const config: StorageFileProcessingPurposeSubtaskProcessorConfig<...> = {", " target: <NAME>_PURPOSE,", " flow: [", " { subtask: <NAME>_PURPOSE_<DESC>_SUBTASK, fn: async (input) => ... }", " ]", " };", " return config;", "}", "```"].join("\n")
51703
+ body: [
51704
+ "**Required shape:**",
51705
+ "```ts",
51706
+ "export function <camelName>ProcessingNotificationTaskHandler(context): StorageFileProcessingPurposeSubtaskProcessorConfig<<Name>SubtaskMetadata, <Name>ProcessingSubtask> {",
51707
+ " const config: StorageFileProcessingPurposeSubtaskProcessorConfig<...> = {",
51708
+ " target: <NAME>_PURPOSE,",
51709
+ " flow: [",
51710
+ " { subtask: <NAME>_PURPOSE_<DESC>_SUBTASK, fn: async (input) => ... }",
51711
+ " ]",
51712
+ " };",
51713
+ " return config;",
51714
+ "}",
51715
+ "```"
51716
+ ].join("\n")
51335
51717
  },
51336
51718
  {
51337
51719
  heading: "Wire into processing handler",
51338
51720
  path: "<apiDir>/src/app/common/model/notification/handlers/task.handler.storagefile.ts",
51339
- body: ["Push the new processor config into the `processors: [...]` array passed to `storageFileProcessingNotificationTaskHandler({ processors })`. The surrounding handler is itself wired into `notificationTaskService({ handlers })` \u2014 already covered by the notification-task plumbing, so no further wiring needed here."].join("\n")
51721
+ body: [
51722
+ "Push the new processor config into the `processors: [...]` array passed to `storageFileProcessingNotificationTaskHandler({ processors })`. The surrounding handler is itself wired into `notificationTaskService({ handlers })` \u2014 already covered by the notification-task plumbing, so no further wiring needed here."
51723
+ ].join("\n")
51340
51724
  }
51341
51725
  ],
51342
51726
  seeAlso: ["storagefile-purpose", "storagefile-processor-subtask", "notification-task"],
@@ -51350,7 +51734,12 @@ var STORAGEFILE_PROCESSOR_SUBTASK = {
51350
51734
  {
51351
51735
  heading: "Component declarations",
51352
51736
  path: "<componentDir>/src/lib/model/storagefile/storagefile.<name>.ts (the file that already declares the purpose)",
51353
- body: ["**Add:**", "- `<NAME>_PURPOSE_<DESC>_SUBTASK: StorageFileProcessingSubtask = '<descriptor>'` \u2014 the new subtask constant.", "- Extend the `<Name>ProcessingSubtask` union alias: `typeof <NAME>_PURPOSE_<DESC>_SUBTASK | ...existing`.", "- Optional: extend `<Name>ProcessingSubtaskMetadata` with any new fields the new subtask needs."].join("\n")
51737
+ body: [
51738
+ "**Add:**",
51739
+ "- `<NAME>_PURPOSE_<DESC>_SUBTASK: StorageFileProcessingSubtask = '<descriptor>'` \u2014 the new subtask constant.",
51740
+ "- Extend the `<Name>ProcessingSubtask` union alias: `typeof <NAME>_PURPOSE_<DESC>_SUBTASK | ...existing`.",
51741
+ "- Optional: extend `<Name>ProcessingSubtaskMetadata` with any new fields the new subtask needs."
51742
+ ].join("\n")
51354
51743
  },
51355
51744
  {
51356
51745
  heading: "API processor flow",
@@ -51382,13 +51771,19 @@ var NOTIFICATION_TEMPLATE = {
51382
51771
  {
51383
51772
  heading: "Component aggregator",
51384
51773
  path: "<componentDir>/src/lib/model/notification/notification.ts",
51385
- body: ["Add the new info to the call to `notificationTemplateTypeInfoRecord([...])` \u2014 either as a direct entry (`<NAME>_NOTIFICATION_TEMPLATE_TYPE_INFO`) or via a spread of the module aggregate (`...ALL_<MODULE>_NOTIFICATION_TEMPLATE_TYPE_INFOS`). The result constant is typically `<APP>_FIREBASE_NOTIFICATION_TEMPLATE_TYPE_INFO_RECORD`."].join("\n")
51774
+ body: [
51775
+ "Add the new info to the call to `notificationTemplateTypeInfoRecord([...])` \u2014 either as a direct entry (`<NAME>_NOTIFICATION_TEMPLATE_TYPE_INFO`) or via a spread of the module aggregate (`...ALL_<MODULE>_NOTIFICATION_TEMPLATE_TYPE_INFOS`). The result constant is typically `<APP>_FIREBASE_NOTIFICATION_TEMPLATE_TYPE_INFO_RECORD`."
51776
+ ].join("\n")
51386
51777
  },
51387
51778
  {
51388
51779
  heading: "API handler factory",
51389
51780
  path: "<apiDir>/src/app/common/model/notification/notification.factory.ts",
51390
51781
  altPaths: ["<apiDir>/src/app/common/model/notification/notification.factory.<name>.ts (split per-template for larger workspaces)"],
51391
- body: ["**Required export:** A factory function returning a `NotificationTemplateServiceTypeConfig` with `{ type: <NAME>_NOTIFICATION_TEMPLATE_TYPE, factory: async (...) => ... }`.", "", "**Wire into:** the top-level `<app>NotificationTemplateServiceConfigsArrayFactory(context)` \u2014 call the new factory inside its returned array. The configs-array factory is itself bound via `NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN` in the notification module."].join("\n")
51782
+ body: [
51783
+ "**Required export:** A factory function returning a `NotificationTemplateServiceTypeConfig` with `{ type: <NAME>_NOTIFICATION_TEMPLATE_TYPE, factory: async (...) => ... }`.",
51784
+ "",
51785
+ "**Wire into:** the top-level `<app>NotificationTemplateServiceConfigsArrayFactory(context)` \u2014 call the new factory inside its returned array. The configs-array factory is itself bound via `NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN` in the notification module."
51786
+ ].join("\n")
51392
51787
  }
51393
51788
  ],
51394
51789
  seeAlso: ["notification-task"],
@@ -51417,7 +51812,17 @@ var NOTIFICATION_TASK = {
51417
51812
  heading: "API handler config",
51418
51813
  path: "<apiDir>/src/app/common/model/notification/notification.task.service.ts",
51419
51814
  altPaths: ["<apiDir>/src/app/common/model/notification/handlers/task.handler.<name>.ts (per-task split for larger workspaces)"],
51420
- body: ["**Required shape:**", "```ts", "const <camelName>NotificationTaskHandler: NotificationTaskServiceTaskHandlerConfig<<Name>NotificationTaskData, <Name>NotificationTaskCheckpoint> = {", " type: <NAME>_NOTIFICATION_TASK_TYPE,", " flow: [", " { checkpoint: 'part_a', fn: async (notificationTask) => ... }", " ]", "};", "```"].join("\n")
51815
+ body: [
51816
+ "**Required shape:**",
51817
+ "```ts",
51818
+ "const <camelName>NotificationTaskHandler: NotificationTaskServiceTaskHandlerConfig<<Name>NotificationTaskData, <Name>NotificationTaskCheckpoint> = {",
51819
+ " type: <NAME>_NOTIFICATION_TASK_TYPE,",
51820
+ " flow: [",
51821
+ " { checkpoint: 'part_a', fn: async (notificationTask) => ... }",
51822
+ " ]",
51823
+ "};",
51824
+ "```"
51825
+ ].join("\n")
51421
51826
  },
51422
51827
  {
51423
51828
  heading: "Wire into task service",
@@ -52340,7 +52745,12 @@ function formatFirestoreRulesScanReportAsMarkdown(report) {
52340
52745
  }
52341
52746
  lines.push("");
52342
52747
  if (report.serverOnly.length > 0) {
52343
- lines.push(`**Server-only (${report.serverOnly.length}):** ${report.serverOnly.map((c) => `\`${c}\``).join(", ")}`, "", "Each of these must carry `@dbxModelServerOnly` on its model interface AND `serverOnly: true` on its `firebaseModelServiceFactory` config, so the model API refuses the read instead of authorizing it via `roleMapForModel`.", "");
52748
+ lines.push(
52749
+ `**Server-only (${report.serverOnly.length}):** ${report.serverOnly.map((c) => `\`${c}\``).join(", ")}`,
52750
+ "",
52751
+ "Each of these must carry `@dbxModelServerOnly` on its model interface AND `serverOnly: true` on its `firebaseModelServiceFactory` config, so the model API refuses the read instead of authorizing it via `roleMapForModel`.",
52752
+ ""
52753
+ );
52344
52754
  }
52345
52755
  if (report.gettableNotListable.length > 0) {
52346
52756
  lines.push(`**Gettable but not listable (${report.gettableNotListable.length}):** ${report.gettableNotListable.map((c) => `\`${c}\``).join(", ")}`, "", "`firestore-get` works on these; `firestore-query` does not \u2014 the rules grant reads by id only.", "");
@@ -52578,7 +52988,15 @@ function validateModelServerOnly(input) {
52578
52988
  }
52579
52989
  for (const iface of input.interfaces) {
52580
52990
  if (iface.serverOnly && !iface.hasModelTag) {
52581
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_TAG_WITHOUT_MODEL_TAG" /* MODEL_SERVER_ONLY_TAG_WITHOUT_MODEL_TAG */, severity: "warning", modelType: void 0, file: iface.file, message: `Interface \`${iface.name}\` carries \`@dbxModelServerOnly\` but no \`@dbxModel\` tag, so the model extractor skips it and \`serverOnly\` never lands on the generated model manifest.` }));
52991
+ violations.push(
52992
+ buildViolation({
52993
+ code: "MODEL_SERVER_ONLY_TAG_WITHOUT_MODEL_TAG" /* MODEL_SERVER_ONLY_TAG_WITHOUT_MODEL_TAG */,
52994
+ severity: "warning",
52995
+ modelType: void 0,
52996
+ file: iface.file,
52997
+ message: `Interface \`${iface.name}\` carries \`@dbxModelServerOnly\` but no \`@dbxModel\` tag, so the model extractor skips it and \`serverOnly\` never lands on the generated model manifest.`
52998
+ })
52999
+ );
52582
53000
  }
52583
53001
  }
52584
53002
  const errorCount = violations.filter((v) => v.severity === "error").length;
@@ -52600,22 +53018,78 @@ function appendServiceViolations(input) {
52600
53018
  const { modelType, collection, tag, flag, rules } = reconciliation;
52601
53019
  const declared = tag === true || flag;
52602
53020
  if (collection == null) {
52603
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_UNRESOLVED_IDENTITY" /* MODEL_SERVER_ONLY_UNRESOLVED_IDENTITY */, severity: "warning", modelType, file: service.exportName, message: `No \`firestoreModelIdentity(...)\` for model type \`${modelType}\` was found in the scanned dirs, so its collection name \u2014 and therefore the rules leg of the reconciliation \u2014 could not be resolved.` }));
53021
+ violations.push(
53022
+ buildViolation({
53023
+ code: "MODEL_SERVER_ONLY_UNRESOLVED_IDENTITY" /* MODEL_SERVER_ONLY_UNRESOLVED_IDENTITY */,
53024
+ severity: "warning",
53025
+ modelType,
53026
+ file: service.exportName,
53027
+ message: `No \`firestoreModelIdentity(...)\` for model type \`${modelType}\` was found in the scanned dirs, so its collection name \u2014 and therefore the rules leg of the reconciliation \u2014 could not be resolved.`
53028
+ })
53029
+ );
52604
53030
  } else if (rules === true && !flag) {
52605
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_MISSING_RUNTIME_FLAG" /* MODEL_SERVER_ONLY_MISSING_RUNTIME_FLAG */, severity: "error", modelType, file: service.exportName, message: `\`${collection}\` has no client read grant in the rules (get=${reconciliation.rulesGet}, list=${reconciliation.rulesList}) but \`${service.exportName}\` does not set \`serverOnly: true\`, so the model API will hand a client a document the rules would refuse.` }));
53031
+ violations.push(
53032
+ buildViolation({
53033
+ code: "MODEL_SERVER_ONLY_MISSING_RUNTIME_FLAG" /* MODEL_SERVER_ONLY_MISSING_RUNTIME_FLAG */,
53034
+ severity: "error",
53035
+ modelType,
53036
+ file: service.exportName,
53037
+ message: `\`${collection}\` has no client read grant in the rules (get=${reconciliation.rulesGet}, list=${reconciliation.rulesList}) but \`${service.exportName}\` does not set \`serverOnly: true\`, so the model API will hand a client a document the rules would refuse.`
53038
+ })
53039
+ );
52606
53040
  } else if (rules === false && declared) {
52607
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_RULES_ALLOW_READ" /* MODEL_SERVER_ONLY_RULES_ALLOW_READ */, severity: "error", modelType, file: service.exportName, message: `\`${modelType}\` declares server-only (${describeDeclarations(tag, flag)}) but \`${collection}\` IS client-readable in the rules (get=${reconciliation.rulesGet}, list=${reconciliation.rulesList}).` }));
53041
+ violations.push(
53042
+ buildViolation({
53043
+ code: "MODEL_SERVER_ONLY_RULES_ALLOW_READ" /* MODEL_SERVER_ONLY_RULES_ALLOW_READ */,
53044
+ severity: "error",
53045
+ modelType,
53046
+ file: service.exportName,
53047
+ message: `\`${modelType}\` declares server-only (${describeDeclarations(tag, flag)}) but \`${collection}\` IS client-readable in the rules (get=${reconciliation.rulesGet}, list=${reconciliation.rulesList}).`
53048
+ })
53049
+ );
52608
53050
  }
52609
53051
  if (rules === true && iface != null && !iface.serverOnly) {
52610
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_MISSING_TAG" /* MODEL_SERVER_ONLY_MISSING_TAG */, severity: "warning", modelType, file: iface.file, message: `\`${collection}\` has no client read grant in the rules but interface \`${iface.name}\` has no \`@dbxModelServerOnly\` tag, so the CLI cannot refuse the read locally.` }));
53052
+ violations.push(
53053
+ buildViolation({
53054
+ code: "MODEL_SERVER_ONLY_MISSING_TAG" /* MODEL_SERVER_ONLY_MISSING_TAG */,
53055
+ severity: "warning",
53056
+ modelType,
53057
+ file: iface.file,
53058
+ message: `\`${collection}\` has no client read grant in the rules but interface \`${iface.name}\` has no \`@dbxModelServerOnly\` tag, so the CLI cannot refuse the read locally.`
53059
+ })
53060
+ );
52611
53061
  }
52612
53062
  if (iface == null && flag) {
52613
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_NO_INTERFACE" /* MODEL_SERVER_ONLY_NO_INTERFACE */, severity: "warning", modelType, file: service.exportName, message: `\`${modelType}\` sets \`serverOnly: true\` but its data type \`${service.modelName ?? "(unknown)"}\` resolves to no interface in the scanned dirs, so there is nothing to carry \`@dbxModelServerOnly\`.` }));
53063
+ violations.push(
53064
+ buildViolation({
53065
+ code: "MODEL_SERVER_ONLY_NO_INTERFACE" /* MODEL_SERVER_ONLY_NO_INTERFACE */,
53066
+ severity: "warning",
53067
+ modelType,
53068
+ file: service.exportName,
53069
+ message: `\`${modelType}\` sets \`serverOnly: true\` but its data type \`${service.modelName ?? "(unknown)"}\` resolves to no interface in the scanned dirs, so there is nothing to carry \`@dbxModelServerOnly\`.`
53070
+ })
53071
+ );
52614
53072
  } else if (iface != null && tag !== flag) {
52615
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_TAG_FLAG_MISMATCH" /* MODEL_SERVER_ONLY_TAG_FLAG_MISMATCH */, severity: "error", modelType, file: iface.file, message: `\`${modelType}\`: interface \`${iface.name}\` ${tag === true ? "carries" : "does NOT carry"} \`@dbxModelServerOnly\` while \`${service.exportName}\` ${flag ? "DOES" : "does not"} set \`serverOnly: true\`.` }));
53073
+ violations.push(
53074
+ buildViolation({
53075
+ code: "MODEL_SERVER_ONLY_TAG_FLAG_MISMATCH" /* MODEL_SERVER_ONLY_TAG_FLAG_MISMATCH */,
53076
+ severity: "error",
53077
+ modelType,
53078
+ file: iface.file,
53079
+ message: `\`${modelType}\`: interface \`${iface.name}\` ${tag === true ? "carries" : "does NOT carry"} \`@dbxModelServerOnly\` while \`${service.exportName}\` ${flag ? "DOES" : "does not"} set \`serverOnly: true\`.`
53080
+ })
53081
+ );
52616
53082
  }
52617
53083
  if (manifestModelTypes != null && declared && !manifestModelTypes.has(modelType)) {
52618
- violations.push(buildViolation({ code: "MODEL_SERVER_ONLY_NOT_IN_MANIFEST" /* MODEL_SERVER_ONLY_NOT_IN_MANIFEST */, severity: "warning", modelType, file: manifestFile, message: `\`${modelType}\` is server-only but has no entry in the generated model manifest, so the CLI's local pre-transport refusal cannot fire for it \u2014 the read costs a round-trip to an API that refuses it.` }));
53084
+ violations.push(
53085
+ buildViolation({
53086
+ code: "MODEL_SERVER_ONLY_NOT_IN_MANIFEST" /* MODEL_SERVER_ONLY_NOT_IN_MANIFEST */,
53087
+ severity: "warning",
53088
+ modelType,
53089
+ file: manifestFile,
53090
+ message: `\`${modelType}\` is server-only but has no entry in the generated model manifest, so the CLI's local pre-transport refusal cannot fire for it \u2014 the read costs a round-trip to an API that refuses it.`
53091
+ })
53092
+ );
52619
53093
  }
52620
53094
  }
52621
53095
  function legsAgree(input) {
@@ -52724,7 +53198,6 @@ async function walkTypeScriptFiles(dir) {
52724
53198
  try {
52725
53199
  entries = (await readdir12(dir)).sort();
52726
53200
  } catch {
52727
- return out;
52728
53201
  }
52729
53202
  for (const entry of entries) {
52730
53203
  if (entry === "node_modules" || entry === "dist") continue;
@@ -55739,7 +56212,13 @@ async function tryReadBatchPath(path, cwd, reader) {
55739
56212
  import { type as type104 } from "arktype";
55740
56213
  var DBX_AUTH_CLAIM_LOOKUP_TOOL = {
55741
56214
  name: "dbx_auth_claim_lookup",
55742
- description: ["Look up a Firebase Auth custom-claim entry by key (`a`, `o`, `fr`) or by `*ApiAuthClaims` interface name (`DemoApiAuthClaims`).", "", "Returns the claim meaning, value type, role mapping through `authRoleClaimsService`, owning app + interface, and the source path:line where the claim is declared.", "", "Use the optional `app` parameter (`demo-api`) to scope the lookup when the same claim key exists in multiple apps."].join("\n"),
56215
+ description: [
56216
+ "Look up a Firebase Auth custom-claim entry by key (`a`, `o`, `fr`) or by `*ApiAuthClaims` interface name (`DemoApiAuthClaims`).",
56217
+ "",
56218
+ "Returns the claim meaning, value type, role mapping through `authRoleClaimsService`, owning app + interface, and the source path:line where the claim is declared.",
56219
+ "",
56220
+ "Use the optional `app` parameter (`demo-api`) to scope the lookup when the same claim key exists in multiple apps."
56221
+ ].join("\n"),
55743
56222
  inputSchema: {
55744
56223
  type: "object",
55745
56224
  properties: {
@@ -55962,7 +56441,14 @@ function sourceLink2(path, line) {
55962
56441
  import { type as type106 } from "arktype";
55963
56442
  var DBX_AUTH_ROLE_LOOKUP_TOOL = {
55964
56443
  name: "dbx_auth_role_lookup",
55965
- description: ["Look up auth roles forward (by role string / const name), by tag, or in reverse (model + verb \u2192 required roles/scopes).", "", '- `topic="admin"` or `topic="AUTH_ADMIN_ROLE"` \u2192 role entry + claims that set it.', '- `tag="privileged"` \u2192 every role tagged with `privileged`.', '- `model="StorageFile" verb="read"` \u2192 reverse lookup of the roles/scopes the gate would expect.', '- `topic="list"` \u2192 catalog.'].join("\n"),
56444
+ description: [
56445
+ "Look up auth roles forward (by role string / const name), by tag, or in reverse (model + verb \u2192 required roles/scopes).",
56446
+ "",
56447
+ '- `topic="admin"` or `topic="AUTH_ADMIN_ROLE"` \u2192 role entry + claims that set it.',
56448
+ '- `tag="privileged"` \u2192 every role tagged with `privileged`.',
56449
+ '- `model="StorageFile" verb="read"` \u2192 reverse lookup of the roles/scopes the gate would expect.',
56450
+ '- `topic="list"` \u2192 catalog.'
56451
+ ].join("\n"),
55966
56452
  inputSchema: {
55967
56453
  type: "object",
55968
56454
  properties: {
@@ -56145,7 +56631,11 @@ function sourceLink3(path, line) {
56145
56631
  import { type as type107 } from "arktype";
56146
56632
  var DBX_AUTH_TOKEN_EXPLAIN_TOOL = {
56147
56633
  name: "dbx_auth_token_explain",
56148
- description: ["Decode a JWT or claims object and annotate each claim with the catalogued meaning, role mapping, and source path:line.", "", "Pass `token` (raw JWT string) **or** `claims` (decoded JSON object). Reserved JWT claims (`iss`/`aud`/`exp`/`iat`/`sub`/`scope`) are listed separately. Flags expired tokens and unknown wrong-issuer claims."].join("\n"),
56634
+ description: [
56635
+ "Decode a JWT or claims object and annotate each claim with the catalogued meaning, role mapping, and source path:line.",
56636
+ "",
56637
+ "Pass `token` (raw JWT string) **or** `claims` (decoded JSON object). Reserved JWT claims (`iss`/`aud`/`exp`/`iat`/`sub`/`scope`) are listed separately. Flags expired tokens and unknown wrong-issuer claims."
56638
+ ].join("\n"),
56149
56639
  inputSchema: {
56150
56640
  type: "object",
56151
56641
  properties: {
@@ -56604,7 +57094,13 @@ function createAuthClusterTools(registry) {
56604
57094
  function registerTools(server, options = {}) {
56605
57095
  const underlyingServer = server.server;
56606
57096
  const tools = [...DBX_TOOLS];
56607
- tools.push(createUiExamplesTool({ examplesRegistry: options.dbxDocsUiExamplesRegistry }), createModelValidateTool({ ruleOptions: options.modelValidateRuleOptions }), createModelValidateFolderTool({ ruleOptions: options.modelValidateRuleOptions }), createModelFixtureValidateAppTool({ getRegistry: () => options.fixtureModelRegistry }), createLogSearchTool(options.logSearchConfig));
57097
+ tools.push(
57098
+ createUiExamplesTool({ examplesRegistry: options.dbxDocsUiExamplesRegistry }),
57099
+ createModelValidateTool({ ruleOptions: options.modelValidateRuleOptions }),
57100
+ createModelValidateFolderTool({ ruleOptions: options.modelValidateRuleOptions }),
57101
+ createModelFixtureValidateAppTool({ getRegistry: () => options.fixtureModelRegistry }),
57102
+ createLogSearchTool(options.logSearchConfig)
57103
+ );
56608
57104
  if (options.forgeFieldRegistry !== void 0) {
56609
57105
  tools.push(createLookupFormTool({ registry: options.forgeFieldRegistry }), createSearchFormTool({ registry: options.forgeFieldRegistry }), createFormScaffoldTool({ registry: options.forgeFieldRegistry }));
56610
57106
  }
@@ -56858,7 +57354,25 @@ async function loadServerContext(options = {}) {
56858
57354
  await emitDownstreamHints({ cwd, externalCounts, onDownstreamHints: options.onDownstreamHints });
56859
57355
  const context = {
56860
57356
  resources: { semanticTypeRegistry: registry, forgeFieldRegistry: forgeRegistry, pipeRegistry, utilRegistry, modelSnapshotFieldRegistry, modelFirebaseIndexRegistry, uiComponentRegistry: uiRegistry, actionRegistry, filterRegistry, tokenRegistry, cssUtilityRegistry, authRegistry, cwd },
56861
- tools: { semanticTypeRegistry: registry, forgeFieldRegistry: forgeRegistry, pipeRegistry, utilRegistry, modelSnapshotFieldRegistry, modelFirebaseIndexRegistry, uiComponentRegistry: uiRegistry, dbxDocsUiExamplesRegistry, actionRegistry, filterRegistry, tokenRegistry, cssUtilityRegistry, fixtureModelRegistry, modelValidateRuleOptions, authRegistry, cwd, logSearchConfig }
57357
+ tools: {
57358
+ semanticTypeRegistry: registry,
57359
+ forgeFieldRegistry: forgeRegistry,
57360
+ pipeRegistry,
57361
+ utilRegistry,
57362
+ modelSnapshotFieldRegistry,
57363
+ modelFirebaseIndexRegistry,
57364
+ uiComponentRegistry: uiRegistry,
57365
+ dbxDocsUiExamplesRegistry,
57366
+ actionRegistry,
57367
+ filterRegistry,
57368
+ tokenRegistry,
57369
+ cssUtilityRegistry,
57370
+ fixtureModelRegistry,
57371
+ modelValidateRuleOptions,
57372
+ authRegistry,
57373
+ cwd,
57374
+ logSearchConfig
57375
+ }
56862
57376
  };
56863
57377
  return context;
56864
57378
  }
@@ -56962,7 +57476,14 @@ async function resolveAuthRegistry(input) {
56962
57476
  }
56963
57477
  function reportAuthLoaderResult(result) {
56964
57478
  const { registry, scannedFiles, fileWarnings, extractWarnings, extractedAppCount, extractedClaimCount } = result;
56965
- const summary = [`[dbx-components-mcp] auth registry loaded`, ` sources: ${registry.loadedSources.join(", ") || "(none)"}`, ` scanned-files: ${scannedFiles.length}`, ` apps: ${registry.apps.length} (extracted: ${extractedAppCount})`, ` claims: ${registry.claims.length} (extracted: ${extractedClaimCount})`, ` warnings: ${fileWarnings.length + extractWarnings.length}`].join("\n");
57479
+ const summary = [
57480
+ `[dbx-components-mcp] auth registry loaded`,
57481
+ ` sources: ${registry.loadedSources.join(", ") || "(none)"}`,
57482
+ ` scanned-files: ${scannedFiles.length}`,
57483
+ ` apps: ${registry.apps.length} (extracted: ${extractedAppCount})`,
57484
+ ` claims: ${registry.claims.length} (extracted: ${extractedClaimCount})`,
57485
+ ` warnings: ${fileWarnings.length + extractWarnings.length}`
57486
+ ].join("\n");
56966
57487
  process.stderr.write(`${summary}
56967
57488
  `);
56968
57489
  for (const warning of fileWarnings) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-components-mcp",
3
- "version": "14.0.0",
3
+ "version": "14.1.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dbx-components-mcp": "dbx-components-mcp.js"
@@ -11,7 +11,7 @@
11
11
  "ts-morph": "^21.0.0"
12
12
  },
13
13
  "peerDependencies": {
14
- "@dereekb/dbx-cli": "14.0.0",
15
- "@dereekb/util": "14.0.0"
14
+ "@dereekb/dbx-cli": "14.1.0",
15
+ "@dereekb/util": "14.1.0"
16
16
  }
17
17
  }