@cosmicdrift/kumiko-bundled-features 0.400.0 → 0.402.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/agent-tools/agent-manifest.js +123 -46
  2. package/dist/agent-tools/tool-catalog.d.ts +6 -0
  3. package/dist/agent-tools/tool-catalog.js +10 -11
  4. package/dist/agent-tools/types.d.ts +11 -10
  5. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +2 -2
  6. package/dist/documents/entity.d.ts +2 -0
  7. package/dist/documents/entity.js +4 -1
  8. package/dist/documents/feature.d.ts +14 -1
  9. package/dist/documents/feature.js +15 -4
  10. package/dist/documents/handlers/import.write.js +1 -1
  11. package/dist/documents/handlers/link.write.js +2 -0
  12. package/dist/documents/i18n.js +14 -0
  13. package/dist/documents/index.d.ts +1 -0
  14. package/dist/documents/tab.js +2 -2
  15. package/dist/documents/target-write-check.js +10 -3
  16. package/dist/inbound-mail-foundation/entities.js +1 -0
  17. package/dist/inbound-mail-foundation/watch-lease.d.ts +1 -1
  18. package/dist/inbound-mail-foundation/watch-lease.js +2 -2
  19. package/dist/inbound-mail-foundation/watch-supervisor-types.d.ts +5 -5
  20. package/dist/inbound-mail-foundation/watch-supervisor.js +5 -7
  21. package/dist/managed-pages/feature.js +9 -1
  22. package/dist/managed-pages/render-page-body.d.ts +12 -1
  23. package/dist/managed-pages/render-page-body.js +51 -14
  24. package/dist/managed-pages/screens/branding-screen.js +1 -1
  25. package/dist/managed-pages/web/i18n.js +1 -2
  26. package/dist/managed-pages/web/lossy-markdown.d.ts +1 -1
  27. package/dist/managed-pages/web/lossy-markdown.js +2 -16
  28. package/dist/managed-pages/web/markdown-to-document.js +2 -2
  29. package/dist/managed-pages/web/page-body-section.js +0 -1
  30. package/dist/readiness/handlers/run-list.query.js +2 -1
  31. package/dist/readiness/run-view.d.ts +4 -7
  32. package/dist/template-resolver/compile-document-body.js +2 -2
  33. package/dist/template-resolver/document-sanitizer.js +3 -3
  34. package/dist/template-resolver/index.d.ts +1 -1
  35. package/dist/template-resolver/index.js +1 -1
  36. package/dist/template-resolver/web/document-schema.d.ts +1 -0
  37. package/dist/template-resolver/web/document-schema.js +16 -1
  38. package/dist/template-resolver/web/i18n.js +2 -2
  39. package/dist/template-resolver/web/use-collection-entry-create.js +4 -3
  40. package/dist/user-data-rights/boot-checks.js +38 -31
  41. package/dist/user-data-rights/boot-rules.d.ts +17 -0
  42. package/dist/user-data-rights/boot-rules.js +17 -0
  43. package/dist/user-data-rights/web/i18n.js +4 -0
  44. package/dist/user-data-rights/web/request-deletion-screen.js +3 -3
  45. package/package.json +11 -11
  46. package/src/agent-tools/changes.json +19 -0
  47. package/src/inbound-mail-foundation/changes.json +7 -0
  48. package/src/managed-pages/changes.json +6 -0
  49. package/src/readiness/changes.json +6 -0
  50. package/src/template-resolver/changes.json +12 -0
  51. package/src/user-data-rights/changes.json +12 -0
@@ -1,5 +1,6 @@
1
- import { EXT_USER_DATA, } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { bootRuleError, collectBootRuleViolations, EXT_USER_DATA, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { entitiesOf } from "../shared/index.js";
3
+ import { USER_DATA_RIGHTS_BOOT_RULES } from "./boot-rules.js";
3
4
  // V2: export-without-erase gate. A feature that registers an EXT_USER_DATA
4
5
  // export hook without a matching delete hook exports data under Art.20 but
5
6
  // never erases it on forget — an Art.17 violation. Hard boot failure: no app
@@ -14,22 +15,25 @@ import { entitiesOf } from "../shared/index.js";
14
15
  // fails otherwise); running this from user-data-rights' own bootCheck is
15
16
  // equivalent to the framework-internal call it replaces.
16
17
  export function validateGdprHookCompleteness(features) {
17
- for (const feature of features) {
18
- for (const usage of feature.extensionUsages) {
19
- if (usage.extensionName !== EXT_USER_DATA)
20
- continue;
21
- const hasExport = typeof usage.options?.["export"] === "function";
22
- const hasDelete = typeof usage.options?.["delete"] === "function";
23
- // The compile-time "at least one hook" type is bypassed by string-typed
24
- // extension names and casts; without this an empty bag registers nothing.
25
- if (!hasExport && !hasDelete) {
26
- throw new Error(`[kumiko:boot] Feature "${feature.name}" registers EXT_USER_DATA for entity "${usage.entityName}" with neither an export nor a delete hook — the entity silently stays out of the Art.15/17/20 pipeline. Register at least one hook.`);
27
- }
28
- if (hasExport && !hasDelete) {
29
- throw new Error(`[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 violation). Add a delete hook. If erasure is intentionally handled elsewhere (e.g. crypto-shredding key-erase, parent cascade), register a no-op delete: async () => {} with a comment explaining why.`);
30
- }
18
+ const usages = features.flatMap((feature) => feature.extensionUsages
19
+ .filter((usage) => usage.extensionName === EXT_USER_DATA)
20
+ .map((usage) => ({ feature, usage })));
21
+ collectBootRuleViolations(usages, ({ feature, usage }) => {
22
+ const hasExport = typeof usage.options?.["export"] === "function";
23
+ const hasDelete = typeof usage.options?.["delete"] === "function";
24
+ // The compile-time "at least one hook" type is bypassed by string-typed
25
+ // extension names and casts; without this an empty bag registers nothing.
26
+ if (!hasExport && !hasDelete) {
27
+ throw bootRuleError(USER_DATA_RIGHTS_BOOT_RULES.emptyHook, {
28
+ message: `[kumiko:boot] Feature "${feature.name}" registers EXT_USER_DATA for entity "${usage.entityName}" with neither an export nor a delete hook — the entity silently stays out of the Art.15/17/20 pipeline. Register at least one hook.`,
29
+ });
31
30
  }
32
- }
31
+ if (hasExport && !hasDelete) {
32
+ throw bootRuleError(USER_DATA_RIGHTS_BOOT_RULES.exportWithoutDelete, {
33
+ message: `[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 violation). Add a delete hook. If erasure is intentionally handled elsewhere (e.g. crypto-shredding key-erase, parent cascade), register a no-op delete: async () => {} with a comment explaining why.`,
34
+ });
35
+ }
36
+ });
33
37
  }
34
38
  // V3: PII-entity-without-hook gate. V2 checks registered hooks for
35
39
  // completeness; V3 catches the entity nobody registered at all — fields
@@ -51,19 +55,22 @@ export function validateGdprPiiHookCoverage(features) {
51
55
  hookedEntities.add(usage.entityName);
52
56
  }
53
57
  }
54
- for (const feature of features) {
55
- for (const [entityName, entity] of entitiesOf(feature)) {
56
- if (hookedEntities.has(entityName))
57
- continue;
58
- const subjectFields = Object.entries(entity.fields)
59
- .filter(([, field]) => {
60
- const annot = field; // @cast-boundary schema-walk
61
- return Boolean(annot.pii) || Boolean(annot.userOwned) || Boolean(annot.subjectRef);
62
- })
63
- .map(([name]) => name);
64
- if (subjectFields.length === 0)
65
- continue;
66
- throw new Error(`[kumiko:boot] Entity "${entityName}" (feature "${feature.name}") has user-subject fields (${subjectFields.join(", ")}) but no feature registers an EXT_USER_DATA hook for it — the data never appears in Art.15/20 exports and is never erased on forget (Art.17 gap). Register r.useExtension(EXT_USER_DATA, "${entityName}", { export, delete }) in the owning feature or a defaults feature. If this entity is intentionally out of the pipeline (e.g. crypto-shredding key-erase covers it), register a no-op hook { export: async () => null, delete: async () => {} } with a comment explaining why.`);
67
- }
68
- }
58
+ const entities = features.flatMap((feature) => [...entitiesOf(feature)].map(([entityName, entity]) => ({ feature, entityName, entity })));
59
+ collectBootRuleViolations(entities, ({ feature, entityName, entity }) => {
60
+ // skip: an entity with a registered hook is covered
61
+ if (hookedEntities.has(entityName))
62
+ return;
63
+ const subjectFields = Object.entries(entity.fields)
64
+ .filter(([, field]) => {
65
+ const annot = field; // @cast-boundary schema-walk
66
+ return Boolean(annot.pii) || Boolean(annot.userOwned) || Boolean(annot.subjectRef);
67
+ })
68
+ .map(([name]) => name);
69
+ // skip: an entity without user-subject fields stays out of the pipeline by design
70
+ if (subjectFields.length === 0)
71
+ return;
72
+ throw bootRuleError(USER_DATA_RIGHTS_BOOT_RULES.missingHook, {
73
+ message: `[kumiko:boot] Entity "${entityName}" (feature "${feature.name}") has user-subject fields (${subjectFields.join(", ")}) but no feature registers an EXT_USER_DATA hook for it — the data never appears in Art.15/20 exports and is never erased on forget (Art.17 gap). Register r.useExtension(EXT_USER_DATA, "${entityName}", { export, delete }) in the owning feature or a defaults feature. If this entity is intentionally out of the pipeline (e.g. crypto-shredding key-erase covers it), register a no-op hook { export: async () => null, delete: async () => {} } with a comment explaining why.`,
74
+ });
75
+ });
69
76
  }
@@ -0,0 +1,17 @@
1
+ export declare const USER_DATA_RIGHTS_BOOT_RULES: {
2
+ readonly emptyHook: {
3
+ readonly id: "user-data-rights/empty-hook";
4
+ readonly summary: "An EXT_USER_DATA registration declares at least one of an export or a delete hook.";
5
+ readonly fix: "Register at least one hook.";
6
+ };
7
+ readonly exportWithoutDelete: {
8
+ readonly id: "user-data-rights/export-without-delete";
9
+ readonly summary: "An entity exported via EXT_USER_DATA also registers a delete hook.";
10
+ readonly fix: "Add a delete hook, or a documented no-op delete if erasure is handled elsewhere.";
11
+ };
12
+ readonly missingHook: {
13
+ readonly id: "user-data-rights/missing-hook";
14
+ readonly summary: "An entity with user-subject fields registers an EXT_USER_DATA hook.";
15
+ readonly fix: "Register r.useExtension(EXT_USER_DATA, ...) for the entity, or a documented no-op hook if it is covered elsewhere.";
16
+ };
17
+ };
@@ -0,0 +1,17 @@
1
+ export const USER_DATA_RIGHTS_BOOT_RULES = {
2
+ emptyHook: {
3
+ id: "user-data-rights/empty-hook",
4
+ summary: "An EXT_USER_DATA registration declares at least one of an export or a delete hook.",
5
+ fix: "Register at least one hook.",
6
+ },
7
+ exportWithoutDelete: {
8
+ id: "user-data-rights/export-without-delete",
9
+ summary: "An entity exported via EXT_USER_DATA also registers a delete hook.",
10
+ fix: "Add a delete hook, or a documented no-op delete if erasure is handled elsewhere.",
11
+ },
12
+ missingHook: {
13
+ id: "user-data-rights/missing-hook",
14
+ summary: "An entity with user-subject fields registers an EXT_USER_DATA hook.",
15
+ fix: "Register r.useExtension(EXT_USER_DATA, ...) for the entity, or a documented no-op hook if it is covered elsewhere.",
16
+ },
17
+ };
@@ -17,6 +17,10 @@ export const USER_DATA_RIGHTS_I18N = {
17
17
  },
18
18
  "user-data-rights:entity:export-job:field:userId": { en: "User" },
19
19
  "user-data-rights:entity:export-job:field:status": { en: "Status" },
20
+ "user-data-rights:entity:export-job:field:status:option:pending": { en: "Pending" },
21
+ "user-data-rights:entity:export-job:field:status:option:running": { en: "Running" },
22
+ "user-data-rights:entity:export-job:field:status:option:done": { en: "Done" },
23
+ "user-data-rights:entity:export-job:field:status:option:failed": { en: "Failed" },
20
24
  "user-data-rights:entity:export-job:field:requestedAt": { en: "Requested" },
21
25
  "user-data-rights:entity:export-job:field:completedAt": { en: "Completed" },
22
26
  "user-data-rights:entity:export-job:field:expiresAt": { en: "Expires" },
@@ -46,7 +46,7 @@ export function RequestAccountDeletionScreen({ title, }) {
46
46
  // kumiko-lint-ignore primitives-discipline narrow centered confirmation card, not a screen container
47
47
  className: "w-full max-w-sm mx-auto", options: { padded: false }, slots: {
48
48
  header: (_jsx("div", { className: "flex flex-col space-y-1.5 p-6 pb-4", children: _jsx("h1", { className: "text-xl font-semibold tracking-tight", children: title ?? t("userDataRights.deletion.request.title") }) })),
49
- }, children: done ? (_jsx("div", { className: "p-6 pt-0", children: _jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("userDataRights.deletion.request.successTitle") }), _jsx("p", { className: "mt-1", children: t("userDataRights.deletion.request.successBody") })] }) })) : (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("userDataRights.deletion.request.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "delete-email", label: t("userDataRights.deletion.request.email"), required: true, children: _jsx(Input, { kind: "text", id: "delete-email", name: "delete-email", value: email, onChange: setEmail, disabled: submitting, required: true }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
50
- ? t("userDataRights.deletion.request.submitting")
51
- : t("userDataRights.deletion.request.submit") })] })] })) }));
49
+ }, children: done ? (_jsx("div", { className: "p-6 pt-0", children: _jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("userDataRights.deletion.request.successTitle") }), _jsx("p", { className: "mt-1", children: t("userDataRights.deletion.request.successBody") })] }) })) : (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("userDataRights.deletion.request.intro") }), _jsx(Form, { chromeless: true, onSubmit: onSubmit, children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Field, { id: "delete-email", label: t("userDataRights.deletion.request.email"), required: true, children: _jsx(Input, { kind: "text", id: "delete-email", name: "delete-email", value: email, onChange: setEmail, disabled: submitting, required: true }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
50
+ ? t("userDataRights.deletion.request.submitting")
51
+ : t("userDataRights.deletion.request.submit") })] }) })] })) }));
52
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.400.0",
3
+ "version": "0.402.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -570,9 +570,9 @@
570
570
  }
571
571
  },
572
572
  "dependencies": {
573
- "@cosmicdrift/kumiko-framework": "0.400.0",
574
- "@cosmicdrift/kumiko-renderer": "0.400.0",
575
- "@cosmicdrift/kumiko-types": "0.400.0",
573
+ "@cosmicdrift/kumiko-framework": "0.402.0",
574
+ "@cosmicdrift/kumiko-renderer": "0.402.0",
575
+ "@cosmicdrift/kumiko-types": "0.402.0",
576
576
  "@mollie/api-client": "^4.5.0",
577
577
  "@node-rs/argon2": "^2.0.2",
578
578
  "@tiptap/core": "^3.31.3",
@@ -1166,17 +1166,17 @@
1166
1166
  ],
1167
1167
  "devDependencies": {
1168
1168
  "@testing-library/user-event": "^14.6.8",
1169
- "@cosmicdrift/kumiko-locale-de": "0.400.0",
1170
- "@cosmicdrift/kumiko-locale-es": "0.400.0",
1171
- "@cosmicdrift/kumiko-locale-fr": "0.400.0",
1172
- "@cosmicdrift/kumiko-locale-it": "0.400.0",
1173
- "@cosmicdrift/kumiko-renderer-mail-html": "0.400.0",
1169
+ "@cosmicdrift/kumiko-locale-de": "0.402.0",
1170
+ "@cosmicdrift/kumiko-locale-es": "0.402.0",
1171
+ "@cosmicdrift/kumiko-locale-fr": "0.402.0",
1172
+ "@cosmicdrift/kumiko-locale-it": "0.402.0",
1173
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.402.0",
1174
1174
  "jsqr": "^1.4.0",
1175
1175
  "unpdf": "^1.8.1",
1176
- "@cosmicdrift/kumiko-testing": "0.400.0"
1176
+ "@cosmicdrift/kumiko-testing": "0.402.0"
1177
1177
  },
1178
1178
  "peerDependencies": {
1179
- "@cosmicdrift/kumiko-testing": "0.400.0"
1179
+ "@cosmicdrift/kumiko-testing": "0.402.0"
1180
1180
  },
1181
1181
  "peerDependenciesMeta": {
1182
1182
  "@cosmicdrift/kumiko-testing": {
@@ -1,4 +1,23 @@
1
1
  [
2
+ {
3
+ "version": "0.402.0",
4
+ "type": "breaking",
5
+ "title": "Agent manifest labels are one string in the requested locale",
6
+ "detail": "`buildAgentManifest` used to emit every label in every locale (`labels: { de, en, es }`, often `{}`), although the caller names one `locale`. Entities, fields, navs and workspaces now carry `label?: string`, screens `title?: string`, picked in the requested locale, then English, then the first other locale, and left out when untranslated. Empty `params` and `workspaces` on screens, empty `workspaces` on navs and an empty handler `description` are left out as well (`description`, `params` and `workspaces` are optional in the types). `buildToolCatalog` reads the new fields.",
7
+ "migration": "Read `entity.label`, `field.label`, `nav.label`, `workspace.label` and `screen.title` instead of the `labels`/`titles` maps. Code that only passes the manifest to `buildToolCatalog` or serialises it needs no change."
8
+ },
9
+ {
10
+ "version": "0.402.0",
11
+ "type": "improvement",
12
+ "title": "Agent manifest shows only the first sentence of a feature description",
13
+ "detail": "`buildAgentManifest` emits the first sentence of `FeatureDefinition.description` (a `. ` before a capital letter ends it, so \"e.g.\" and \"Art. 15\" stay whole), at most 300 characters. The full text stays on the definition for the docs generators. In an app with 77 features the descriptions drop from about 46.6k to 12.2k characters."
14
+ },
15
+ {
16
+ "version": "0.402.0",
17
+ "type": "improvement",
18
+ "title": "Agent manifest features and entities follow the role",
19
+ "detail": "`buildAgentManifest` now leaves out a feature when none of its handlers, screens or navs remain in the manifest for the role, and an entity when no remaining handler or screen refers to it, no `get_`/`list_` tool of the role reads it and no reachable entity points at it through a reference field (one hop). Before, `features` and `entities` were identical for every role, so a restricted role paid for schemas it could not use in every system prompt. A translation-only feature such as `locale-de` no longer appears in `features`."
20
+ },
2
21
  {
3
22
  "version": "0.389.0",
4
23
  "type": "improvement",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.402.0",
4
+ "type": "breaking",
5
+ "title": "The inbound mail supervisor requires a lock and the dedup anchor is unique",
6
+ "detail": "`createInboundMailSupervisor` takes a required `lock` (a DistributedLock) instead of an optional one, so only one worker holds the IMAP watch per account; the single-process path without a lock is gone. The unmanaged table store_mail_seen_messages gets a unique index on (account_id, provider_message_id), which the ingest comment always promised; ingest-message already serialized concurrent ingests of one mail with a per-message advisory lock, so the second one reports duplicate.",
7
+ "migration": "Pass `lock: createDistributedLock(redis, \"kumiko:lock:inbound-mail:watch:\")` to createInboundMailSupervisor. Run `kumiko-schema generate <name>` in each app that mounts inbound-mail-foundation to get the migration for the unique index on store_mail_seen_messages; existing rows must hold no duplicate (account_id, provider_message_id)."
8
+ },
2
9
  {
3
10
  "version": "0.394.0",
4
11
  "type": "improvement",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.402.0",
4
+ "type": "improvement",
5
+ "title": "The public render caches a sanitized document body per page version",
6
+ "detail": "The render cache holds up to 500 entries keyed on tenant, slug, language, version and format. Bodies larger than 256 KB are sanitized on every render and not cached."
7
+ },
2
8
  {
3
9
  "version": "0.395.0",
4
10
  "type": "breaking",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.402.0",
4
+ "type": "fix",
5
+ "title": "System-check history keeps runs of the same second in order",
6
+ "detail": "`probe-run:list` returned `requestedAt` as `Instant.toString()`, which drops trailing zeros in the fraction, and `pageRows` sorts that field as a string. Two runs in the same second could then come out reversed (`…:20Z` sorted after `…:20.001Z`), so the history showed the older run on top and a click opened it. `requestedAt` now always carries six fractional digits, so string order matches time order."
7
+ },
2
8
  {
3
9
  "version": "0.392.0",
4
10
  "type": "improvement",
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.402.0",
4
+ "type": "improvement",
5
+ "title": "Document anchors accept relative links",
6
+ "detail": "An anchor href in a document body may be relative (/path, ./, ../, #, ?); never // or a backslash. Other URL attributes (image src and similar) and mail links still accept only explicit schemes or placeholders. The sanitizer, the editor schema, compileDocumentBody and the confirmation mail use the same split."
7
+ },
8
+ {
9
+ "version": "0.402.0",
10
+ "type": "improvement",
11
+ "title": "Collection nav takes entryLabel for the create title",
12
+ "detail": "A collection's nav takes an optional entryLabel (singular i18n key, new in the nav type and in the required surface keys of all locale packages). The create screen title reads \"New {label}\", or \"New entry\" without it."
13
+ },
2
14
  {
3
15
  "version": "0.395.0",
4
16
  "type": "breaking",
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.402.0",
4
+ "type": "improvement",
5
+ "title": "user-data-rights boot checks report rule ids",
6
+ "detail": "The bootChecks of user-data-rights report `user-data-rights/empty-hook`, `user-data-rights/export-without-delete` and `user-data-rights/missing-hook` with a fix, and list every missing or empty hook of a run instead of stopping at the first. Message texts are unchanged."
7
+ },
8
+ {
9
+ "version": "0.402.0",
10
+ "type": "fix",
11
+ "title": "request-deletion-screen shows one deletion card",
12
+ "detail": "The request form no longer draws its own card inside the screen's card, so the screen shows one deletion card."
13
+ },
2
14
  {
3
15
  "version": "0.390.0",
4
16
  "type": "fix",