@cosmicdrift/kumiko-framework 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 (59) hide show
  1. package/AGENTS.md +8 -5
  2. package/agents/api/framework-engine.md +31 -5
  3. package/agents/api/testing-stack.md +1 -0
  4. package/agents/api/types.md +16 -1
  5. package/agents/api.md +2 -2
  6. package/agents/boot-rules.md +98 -0
  7. package/agents/examples/relations.md +13 -3
  8. package/dist/config/index.d.ts +1 -1
  9. package/dist/db/read-gate-clause.js +23 -10
  10. package/dist/db/replay-migration-sql.js +1 -1
  11. package/dist/engine/boot-validator/api-ext-rules.d.ts +27 -0
  12. package/dist/engine/boot-validator/api-ext-rules.js +27 -0
  13. package/dist/engine/boot-validator/api-ext.js +25 -12
  14. package/dist/engine/boot-validator/boot-check-rules.d.ts +7 -0
  15. package/dist/engine/boot-validator/boot-check-rules.js +7 -0
  16. package/dist/engine/boot-validator/boot-check.js +13 -1
  17. package/dist/engine/boot-validator/boot-options.d.ts +5 -0
  18. package/dist/engine/boot-validator/boot-phases-app.js +5 -1
  19. package/dist/engine/boot-validator/boot-report.d.ts +1 -0
  20. package/dist/engine/boot-validator/boot-report.js +58 -7
  21. package/dist/engine/boot-validator/boot-rule.d.ts +20 -0
  22. package/dist/engine/boot-validator/boot-rule.js +33 -2
  23. package/dist/engine/boot-validator/boot-rules.d.ts +210 -0
  24. package/dist/engine/boot-validator/boot-rules.js +17 -0
  25. package/dist/engine/boot-validator/card-meta-overflow-rules.d.ts +7 -0
  26. package/dist/engine/boot-validator/card-meta-overflow-rules.js +7 -0
  27. package/dist/engine/boot-validator/card-meta-overflow.js +5 -1
  28. package/dist/engine/boot-validator/entity-list-screens-rules.d.ts +32 -0
  29. package/dist/engine/boot-validator/entity-list-screens-rules.js +32 -0
  30. package/dist/engine/boot-validator/entity-list-screens.js +74 -41
  31. package/dist/engine/boot-validator/field-conditions.js +19 -1
  32. package/dist/engine/boot-validator/i18n-keys-rules.d.ts +27 -0
  33. package/dist/engine/boot-validator/i18n-keys-rules.js +27 -0
  34. package/dist/engine/boot-validator/i18n-keys.d.ts +1 -0
  35. package/dist/engine/boot-validator/i18n-keys.js +43 -16
  36. package/dist/engine/boot-validator/pii-retention-rules.d.ts +107 -0
  37. package/dist/engine/boot-validator/pii-retention-rules.js +107 -0
  38. package/dist/engine/boot-validator/pii-retention.js +88 -34
  39. package/dist/engine/boot-validator/read-gate.js +12 -7
  40. package/dist/engine/boot-validator/screens-actions-rules.d.ts +7 -0
  41. package/dist/engine/boot-validator/screens-actions-rules.js +7 -0
  42. package/dist/engine/boot-validator/screens-actions.js +7 -3
  43. package/dist/engine/entity-handlers.d.ts +12 -5
  44. package/dist/engine/factories.d.ts +13 -1
  45. package/dist/engine/factories.js +18 -0
  46. package/dist/engine/index.d.ts +3 -1
  47. package/dist/engine/index.js +3 -1
  48. package/dist/engine/user-entity-reference.d.ts +2 -0
  49. package/dist/engine/user-entity-reference.js +7 -0
  50. package/dist/files/index.d.ts +2 -0
  51. package/dist/files/index.js +1 -0
  52. package/dist/files/rebind-file-ref.d.ts +41 -0
  53. package/dist/files/rebind-file-ref.js +170 -0
  54. package/dist/i18n/required-surface-keys.d.ts +6 -0
  55. package/dist/i18n/required-surface-keys.js +64 -0
  56. package/dist/schema-cli/validate-boot-input.js +2 -0
  57. package/dist/schema-cli/validate-command.js +4 -1
  58. package/package.json +3 -3
  59. package/src/changes.json +25 -0
@@ -30,4 +30,9 @@ export type ValidateBootOptions = {
30
30
  * (locale packages count). Without it a key can ship in one language and
31
31
  * silently fall back at runtime for users of the others. */
32
32
  readonly requiredLocales?: readonly string[];
33
+ /** Require translations for the values that entityList columns and entityEdit
34
+ * fields display: select/multiSelect option labels and the column headers
35
+ * and select options of embedded lists. Without them the UI shows raw
36
+ * option values and sub-field names. Opt-in (default false). */
37
+ readonly requireFieldValueTranslations?: boolean;
33
38
  };
@@ -9,7 +9,7 @@ import { validateExtensionPreSaveWiring } from "./entity-handler.js";
9
9
  import { validateEntityListScreens } from "./entity-list-screens.js";
10
10
  import { validateGdprStoragePersistence } from "./gdpr-storage.js";
11
11
  import { validateGlobalTenancyEntities } from "./global-tenancy.js";
12
- import { validateI18nSurfaceKeys, validateRequiredLocales, validateTranslationLocaleConsistency, } from "./i18n-keys.js";
12
+ import { validateFieldValueTranslations, validateI18nSurfaceKeys, validateRequiredLocales, validateTranslationLocaleConsistency, } from "./i18n-keys.js";
13
13
  import { validateNavCycles } from "./nav.js";
14
14
  import { validateProjectionListScreens, validateRelatedListSectionQueries, } from "./projection-list-screens.js";
15
15
  import { validateQueryOutputColumns } from "./query-output-columns.js";
@@ -25,6 +25,10 @@ const APP_CHECKS_AFTER_I18N = [
25
25
  check: "validateRequiredLocales",
26
26
  run: (c) => validateRequiredLocales(c.features, c.options?.requiredLocales),
27
27
  },
28
+ {
29
+ check: "validateFieldValueTranslations",
30
+ run: (c) => validateFieldValueTranslations(c.features, c.options?.requireFieldValueTranslations),
31
+ },
28
32
  { check: "validateEntityListScreens", run: (c) => validateEntityListScreens(c.features) },
29
33
  {
30
34
  check: "validateSubmitPrefilledHiddenFields",
@@ -4,3 +4,4 @@ export type BootReport = {
4
4
  finish(): void;
5
5
  };
6
6
  export declare function createBootReport(): BootReport;
7
+ export declare function collectBootRuleViolations<T>(items: Iterable<T>, run: (item: T) => void): void;
@@ -1,4 +1,4 @@
1
- import { BootValidationError } from "./boot-rule.js";
1
+ import { BootRuleFailures, BootRuleViolation, BootValidationError, } from "./boot-rule.js";
2
2
  // A runtime crash is a bug in a validator (or in the input it assumed), not a
3
3
  // rule violation: it must surface as itself instead of masquerading as a finding.
4
4
  function isRuntimeCrash(thrown) {
@@ -10,6 +10,33 @@ function isRuntimeCrash(thrown) {
10
10
  thrown instanceof EvalError ||
11
11
  thrown instanceof URIError);
12
12
  }
13
+ function violationsOf(failure, checkName, feature) {
14
+ if (failure instanceof BootRuleFailures) {
15
+ return failure.failures.flatMap((nested) => violationsOf(nested, checkName, feature));
16
+ }
17
+ if (failure instanceof BootValidationError)
18
+ return failure.violations;
19
+ const featurePart = feature === undefined ? {} : { feature };
20
+ if (failure instanceof BootRuleViolation) {
21
+ return [
22
+ {
23
+ ruleId: failure.rule.id,
24
+ check: checkName,
25
+ ...featurePart,
26
+ message: failure.message,
27
+ fix: failure.rule.fix,
28
+ },
29
+ ];
30
+ }
31
+ return [
32
+ {
33
+ ruleId: `unclassified/${checkName}`,
34
+ check: checkName,
35
+ ...featurePart,
36
+ message: failure.message,
37
+ },
38
+ ];
39
+ }
13
40
  export function createBootReport() {
14
41
  const violations = [];
15
42
  let firstCrash;
@@ -31,12 +58,7 @@ export function createBootReport() {
31
58
  firstCrash ??= { thrown };
32
59
  }
33
60
  else if (thrown instanceof Error) {
34
- violations.push({
35
- ruleId: `unclassified/${checkName}`,
36
- check: checkName,
37
- ...(feature === undefined ? {} : { feature }),
38
- message: thrown.message,
39
- });
61
+ violations.push(...violationsOf(thrown, checkName, feature));
40
62
  }
41
63
  return false;
42
64
  }
@@ -45,3 +67,32 @@ export function createBootReport() {
45
67
  finish: endPhase,
46
68
  };
47
69
  }
70
+ // Boundary per item: every item runs, so one boot reports all independent violations
71
+ // instead of only the first. Crashes behave as in endPhase: dropped next to failures.
72
+ export function collectBootRuleViolations(items, run) {
73
+ const failures = [];
74
+ let firstCrash;
75
+ for (const item of items) {
76
+ try {
77
+ run(item);
78
+ }
79
+ catch (thrown) {
80
+ if (isRuntimeCrash(thrown)) {
81
+ firstCrash ??= { thrown };
82
+ }
83
+ else if (thrown instanceof BootRuleFailures) {
84
+ failures.push(...thrown.failures);
85
+ }
86
+ else if (thrown instanceof Error) {
87
+ failures.push(thrown);
88
+ }
89
+ }
90
+ }
91
+ const [onlyFailure] = failures;
92
+ if (onlyFailure !== undefined && failures.length === 1)
93
+ throw onlyFailure;
94
+ if (failures.length > 1)
95
+ throw new BootRuleFailures(failures);
96
+ if (firstCrash !== undefined)
97
+ throw firstCrash.thrown;
98
+ }
@@ -5,6 +5,26 @@ export type BootViolation = {
5
5
  readonly message: string;
6
6
  readonly fix?: string;
7
7
  };
8
+ export type BootRule = {
9
+ readonly id: string;
10
+ readonly summary: string;
11
+ readonly fix: string;
12
+ };
13
+ export type BootRuleTable = Readonly<Record<string, BootRule>>;
14
+ export declare class BootRuleViolation extends Error {
15
+ readonly rule: BootRule;
16
+ readonly name = "BootRuleViolation";
17
+ constructor(rule: BootRule, message: string);
18
+ }
19
+ export declare function bootRuleError(rule: BootRule, details: {
20
+ readonly message: string;
21
+ }): BootRuleViolation;
22
+ export declare class BootRuleFailures extends Error {
23
+ readonly failures: readonly Error[];
24
+ readonly name = "BootRuleFailures";
25
+ constructor(failures: readonly Error[]);
26
+ }
27
+ export declare function bootRuleDocLink(ruleId: string): string | undefined;
8
28
  export declare class BootValidationError extends Error {
9
29
  readonly violations: readonly BootViolation[];
10
30
  readonly name = "BootValidationError";
@@ -1,6 +1,37 @@
1
+ export class BootRuleViolation extends Error {
2
+ rule;
3
+ name = "BootRuleViolation";
4
+ constructor(rule, message) {
5
+ super(message);
6
+ this.rule = rule;
7
+ }
8
+ }
9
+ export function bootRuleError(rule, details) {
10
+ return new BootRuleViolation(rule, details.message);
11
+ }
12
+ export class BootRuleFailures extends Error {
13
+ failures;
14
+ name = "BootRuleFailures";
15
+ constructor(failures) {
16
+ super(failures.map((failure) => failure.message).join("\n"));
17
+ this.failures = failures;
18
+ }
19
+ }
20
+ const UNCLASSIFIED_RULE_PREFIX = "unclassified/";
21
+ // Must match anchorOf in packages/guards/src/_lib/agent-docs-boot-rules.ts.
22
+ export function bootRuleDocLink(ruleId) {
23
+ if (ruleId.startsWith(UNCLASSIFIED_RULE_PREFIX))
24
+ return undefined;
25
+ return `agents/boot-rules.md#${ruleId.replaceAll("/", "-")}`;
26
+ }
1
27
  function describeViolation(violation) {
2
- const line = `- [${violation.ruleId}] ${violation.message}`;
3
- return violation.fix === undefined ? line : `${line}\n Fix: ${violation.fix}`;
28
+ const lines = [`- [${violation.ruleId}] ${violation.message}`];
29
+ if (violation.fix !== undefined)
30
+ lines.push(` Fix: ${violation.fix}`);
31
+ const docLink = bootRuleDocLink(violation.ruleId);
32
+ if (docLink !== undefined)
33
+ lines.push(` Rule: ${docLink}`);
34
+ return lines.join("\n");
4
35
  }
5
36
  export class BootValidationError extends Error {
6
37
  violations;
@@ -0,0 +1,210 @@
1
+ import type { BootRule } from "./boot-rule.ts";
2
+ export declare const FRAMEWORK_BOOT_RULE_TABLES: readonly [{
3
+ readonly multipleSubjectAnnotations: {
4
+ readonly id: "pii/multiple-subject-annotations";
5
+ readonly summary: "A field carries at most one subject-key annotation.";
6
+ readonly fix: "Keep a single personal annotation on the field.";
7
+ };
8
+ readonly textWithoutPersonalStance: {
9
+ readonly id: "pii/text-without-personal-stance";
10
+ readonly summary: "Every text and longText field declares whether it holds personal data.";
11
+ readonly fix: "Declare a personal stance, or personal: false with a reason stating why the value is not personal data.";
12
+ };
13
+ readonly plaintextUserReference: {
14
+ readonly id: "pii/plaintext-user-reference";
15
+ readonly summary: "A reference to the user entity is never plaintext.";
16
+ readonly fix: 'Use the default reference field or personal: "ref" instead of personal: false.';
17
+ };
18
+ readonly lookupableNonText: {
19
+ readonly id: "pii/lookupable-non-text";
20
+ readonly summary: "Blind-index lookups apply to text fields only.";
21
+ readonly fix: 'Use a text field or set find: "none".';
22
+ };
23
+ readonly lookupableWithoutSubject: {
24
+ readonly id: "pii/lookupable-without-subject";
25
+ readonly summary: "A blind-index lookup requires a subject annotation on the field.";
26
+ readonly fix: 'Add a personal annotation or set find: "none".';
27
+ };
28
+ readonly sensitiveWithoutCiphertext: {
29
+ readonly id: "pii/sensitive-without-ciphertext";
30
+ readonly summary: "A sensitive field is stored as ciphertext at rest.";
31
+ readonly fix: "Add a subject annotation or encrypted: true.";
32
+ };
33
+ readonly writeOnlyEmbeddedSubField: {
34
+ readonly id: "pii/write-only-embedded-sub-field";
35
+ readonly summary: "writeOnly is only supported on top-level text fields, not on embedded sub-fields.";
36
+ readonly fix: "Remove writeOnly from the sub-field or declare it on a top-level text field.";
37
+ };
38
+ readonly writeOnlyNonText: {
39
+ readonly id: "pii/write-only-non-text";
40
+ readonly summary: "writeOnly applies to text fields only.";
41
+ readonly fix: "Use a text field or remove writeOnly.";
42
+ };
43
+ readonly writeOnlyWithoutSensitive: {
44
+ readonly id: "pii/write-only-without-sensitive";
45
+ readonly summary: "A writeOnly field is also sensitive.";
46
+ readonly fix: 'Declare the field with find: "secret".';
47
+ };
48
+ readonly writeOnlyConflictingFlag: {
49
+ readonly id: "pii/write-only-conflicting-flag";
50
+ readonly summary: "A writeOnly field cannot be defaulted, searched, sorted, filtered or looked up.";
51
+ readonly fix: "Remove either writeOnly or the conflicting flag.";
52
+ };
53
+ readonly subjectKeySortable: {
54
+ readonly id: "pii/subject-key-sortable";
55
+ readonly summary: "A field with a subject-key annotation is not sortable, its column is ciphertext.";
56
+ readonly fix: 'Drop sortable, use find: "exact" for equality lookups, or keep the field plaintext.';
57
+ };
58
+ readonly sensitiveSearchable: {
59
+ readonly id: "pii/sensitive-searchable";
60
+ readonly summary: "A sensitive field is never searchable.";
61
+ readonly fix: "Drop searchable, or add writeOnly to never return the value.";
62
+ };
63
+ readonly personalJsonbFlag: {
64
+ readonly id: "pii/personal-jsonb-flag";
65
+ readonly summary: "A personal jsonb field cannot be sortable, filterable, searchable or lookupable, its value is a ciphertext string.";
66
+ readonly fix: "Remove the flag or the personal annotation.";
67
+ };
68
+ readonly personalJsonbIndex: {
69
+ readonly id: "pii/personal-jsonb-index";
70
+ readonly summary: "A personal jsonb field is not part of an entity index.";
71
+ readonly fix: "Drop the index or the personal annotation.";
72
+ };
73
+ readonly personalJsonbCustomFields: {
74
+ readonly id: "pii/personal-jsonb-custom-fields";
75
+ readonly summary: "The custom-fields jsonb column cannot carry a personal annotation.";
76
+ readonly fix: "Drop the personal annotation.";
77
+ };
78
+ readonly personalOfMissingOwnerName: {
79
+ readonly id: "pii/personal-of-missing-owner-name";
80
+ readonly summary: "personal: { of } names the owner field.";
81
+ readonly fix: 'Pass the name of the owner field: personal: { of: "<ownerField>" }.';
82
+ };
83
+ readonly personalOfUnknownOwner: {
84
+ readonly id: "pii/personal-of-unknown-owner";
85
+ readonly summary: "The owner field named by personal: { of } exists on the entity.";
86
+ readonly fix: "Point personal.of at an existing field of the entity.";
87
+ };
88
+ readonly personalOfOwnerType: {
89
+ readonly id: "pii/personal-of-owner-type";
90
+ readonly summary: "The owner field named by personal: { of } is a reference or text field.";
91
+ readonly fix: "Use a reference or text field as owner.";
92
+ };
93
+ readonly personalOfInvalidWhenAbsent: {
94
+ readonly id: "pii/personal-of-invalid-when-absent";
95
+ readonly summary: 'personal.whenAbsent is either "tenant" or "self".';
96
+ readonly fix: 'Remove whenAbsent or set it to "tenant" or "self".';
97
+ };
98
+ readonly personalOfOptionalOwner: {
99
+ readonly id: "pii/personal-of-optional-owner";
100
+ readonly summary: "An optional owner field of personal: { of } declares a whenAbsent fallback.";
101
+ readonly fix: 'Set whenAbsent to "tenant" or "self", or make the owner field required.';
102
+ };
103
+ readonly retentionUnknownReference: {
104
+ readonly id: "retention/unknown-reference";
105
+ readonly summary: "retention.reference names an existing field or a framework timestamp.";
106
+ readonly fix: "Point retention.reference at an existing field or a framework-managed timestamp.";
107
+ };
108
+ }, {
109
+ readonly searchableFalse: {
110
+ readonly id: "entity-list/searchable-false";
111
+ readonly summary: "Operator lists are searchable unless they are on the searchable-false whitelist.";
112
+ readonly fix: "Set searchable: true or add the screen id to the whitelist.";
113
+ };
114
+ readonly defaultSortRequired: {
115
+ readonly id: "entity-list/default-sort-required";
116
+ readonly summary: "A list with active search or filterable fields declares a defaultSort.";
117
+ readonly fix: "Set defaultSort: { field, direction } on the screen.";
118
+ };
119
+ readonly defaultSortNotListed: {
120
+ readonly id: "entity-list/default-sort-not-listed";
121
+ readonly summary: "defaultSort.field is one of the listed columns.";
122
+ readonly fix: "Pick a field from columns or add it there.";
123
+ };
124
+ readonly defaultSortNotSortable: {
125
+ readonly id: "entity-list/default-sort-not-sortable";
126
+ readonly summary: "The defaultSort column is sortable.";
127
+ readonly fix: "Mark the field sortable or pick another defaultSort field.";
128
+ };
129
+ readonly detailWithoutNavigate: {
130
+ readonly id: "entity-list/detail-without-navigate";
131
+ readonly summary: "A list whose entity has a detail screen offers a navigate row action.";
132
+ readonly fix: 'Add a navigate rowAction with id "view" or "edit".';
133
+ };
134
+ readonly unknownColumn: {
135
+ readonly id: "entity-list/unknown-column";
136
+ readonly summary: "Every column without a label names an entity field, derived field or row-meta column.";
137
+ readonly fix: "Fix the column field name or give the column a label.";
138
+ };
139
+ }, {
140
+ readonly missingFeatureKey: {
141
+ readonly id: "i18n/missing-feature-key";
142
+ readonly summary: "Every translation key a feature surface requires is defined.";
143
+ readonly fix: "Add the key via r.translations(...) in the feature.";
144
+ };
145
+ readonly missingSettingsHubKey: {
146
+ readonly id: "i18n/missing-settings-hub-key";
147
+ readonly summary: "Every translation key the generated settings hub requires is defined.";
148
+ readonly fix: "Add the key via r.translations(...) in the owning feature.";
149
+ };
150
+ readonly localeInconsistent: {
151
+ readonly id: "i18n/locale-inconsistent";
152
+ readonly summary: "Every locale a feature uses is maintained for every one of its translation keys.";
153
+ readonly fix: "Add the missing locale to the key, or move that locale into a locale package.";
154
+ };
155
+ readonly missingRequiredLocale: {
156
+ readonly id: "i18n/missing-required-locale";
157
+ readonly summary: "Every translation key exists in every locale the app requires.";
158
+ readonly fix: "Load the matching locale package or add the missing translations.";
159
+ };
160
+ readonly missingFieldValueTranslation: {
161
+ readonly id: "i18n/missing-field-value-translation";
162
+ readonly summary: "With requireFieldValueTranslations, select options and embedded cells are translated.";
163
+ readonly fix: "Add the keys via r.translations(...) in the owning feature.";
164
+ };
165
+ }, {
166
+ readonly overflow: {
167
+ readonly id: "list/card-meta-overflow";
168
+ readonly summary: "A card list shows at most the maximum number of meta columns.";
169
+ readonly fix: "Reduce each list to the maximum number of meta columns or mark the others hideOnNarrow: true.";
170
+ };
171
+ }, {
172
+ readonly actionIcon: {
173
+ readonly id: "screen/action-icon";
174
+ readonly summary: "Every screen action resolves to a registered icon.";
175
+ readonly fix: "Set icon to a registered NAV_ICONS key or use an action id that resolves one via the default icon map.";
176
+ };
177
+ }, {
178
+ readonly unknownApi: {
179
+ readonly id: "api/unknown-api";
180
+ readonly summary: "Every r.usesApi name is exposed by some mounted feature.";
181
+ readonly fix: "Mount the feature that exposes the API or fix the API name.";
182
+ };
183
+ readonly providerNotRequired: {
184
+ readonly id: "api/provider-not-required";
185
+ readonly summary: "A feature using an API lists the exposing feature in requires or optionalRequires.";
186
+ readonly fix: "Add the exposing feature via r.requires({ features: [...] }).";
187
+ };
188
+ readonly providerNotMounted: {
189
+ readonly id: "api/provider-not-mounted";
190
+ readonly summary: "The feature exposing a used API is part of the app's feature list.";
191
+ readonly fix: "Register the provider feature in the app's feature list.";
192
+ };
193
+ readonly unknownExtension: {
194
+ readonly id: "extension/unknown-extension";
195
+ readonly summary: "Every extension a feature uses is defined by some mounted feature.";
196
+ readonly fix: "Register the feature that defines the extension or fix the extension name.";
197
+ };
198
+ readonly extensionProviderNotRequired: {
199
+ readonly id: "extension/provider-not-required";
200
+ readonly summary: "A feature using an extension lists the defining feature in requires or optionalRequires.";
201
+ readonly fix: "Add the defining feature via r.requires({ features: [...] }).";
202
+ };
203
+ }, {
204
+ readonly failed: {
205
+ readonly id: "boot-check/failed";
206
+ readonly summary: "A feature's r.bootCheck invariant holds for the mounted feature set.";
207
+ readonly fix: "Satisfy the invariant named in the message or unmount the feature that declares it.";
208
+ };
209
+ }];
210
+ export declare const FRAMEWORK_BOOT_RULES: readonly BootRule[];
@@ -0,0 +1,17 @@
1
+ import { API_EXT_RULES } from "./api-ext-rules.js";
2
+ import { BOOT_CHECK_RULES } from "./boot-check-rules.js";
3
+ import { CARD_META_OVERFLOW_RULES } from "./card-meta-overflow-rules.js";
4
+ import { ENTITY_LIST_RULES } from "./entity-list-screens-rules.js";
5
+ import { I18N_RULES } from "./i18n-keys-rules.js";
6
+ import { PII_RULES } from "./pii-retention-rules.js";
7
+ import { SCREEN_ACTION_RULES } from "./screens-actions-rules.js";
8
+ export const FRAMEWORK_BOOT_RULE_TABLES = [
9
+ PII_RULES,
10
+ ENTITY_LIST_RULES,
11
+ I18N_RULES,
12
+ CARD_META_OVERFLOW_RULES,
13
+ SCREEN_ACTION_RULES,
14
+ API_EXT_RULES,
15
+ BOOT_CHECK_RULES,
16
+ ];
17
+ export const FRAMEWORK_BOOT_RULES = FRAMEWORK_BOOT_RULE_TABLES.flatMap((table) => Object.values(table));
@@ -0,0 +1,7 @@
1
+ export declare const CARD_META_OVERFLOW_RULES: {
2
+ readonly overflow: {
3
+ readonly id: "list/card-meta-overflow";
4
+ readonly summary: "A card list shows at most the maximum number of meta columns.";
5
+ readonly fix: "Reduce each list to the maximum number of meta columns or mark the others hideOnNarrow: true.";
6
+ };
7
+ };
@@ -0,0 +1,7 @@
1
+ export const CARD_META_OVERFLOW_RULES = {
2
+ overflow: {
3
+ id: "list/card-meta-overflow",
4
+ summary: "A card list shows at most the maximum number of meta columns.",
5
+ fix: "Reduce each list to the maximum number of meta columns or mark the others hideOnNarrow: true.",
6
+ },
7
+ };
@@ -2,6 +2,8 @@ import { CARD_META_MAX, cardColumnRoles, cardFileDisplayKind, } from "../../ui-t
2
2
  import { LIST_ROW_META_COLUMNS } from "../../ui-types/list-row-meta.js";
3
3
  import { parseRefTarget } from "../parse-ref-target.js";
4
4
  import { normalizeListColumn } from "../screen-helpers.js";
5
+ import { bootRuleError } from "./boot-rule.js";
6
+ import { CARD_META_OVERFLOW_RULES } from "./card-meta-overflow-rules.js";
5
7
  // Mirrors the type resolution of headless computeListViewModel; only the
6
8
  // "select" outcome matters for the card status role.
7
9
  function resolveColumnType(spec, storedType, entity) {
@@ -94,5 +96,7 @@ export function validateCardMetaOverflow(features) {
94
96
  if (overflows.length === 0)
95
97
  return;
96
98
  const lines = overflows.map((o) => ` - Feature "${o.feature}" screen "${o.screen}"${o.location !== undefined ? ` ${o.location}` : ""}: ${o.count} meta columns (${o.fields.join(", ")})`);
97
- throw new Error(`List cards show at most ${CARD_META_MAX} meta columns (all columns except title, status select and hideOnNarrow ones); more would be cut off on narrow screens:\n${lines.join("\n")}\nReduce each to ${CARD_META_MAX} columns or mark the others hideOnNarrow: true.`);
99
+ throw bootRuleError(CARD_META_OVERFLOW_RULES.overflow, {
100
+ message: `List cards show at most ${CARD_META_MAX} meta columns (all columns except title, status select and hideOnNarrow ones); more would be cut off on narrow screens:\n${lines.join("\n")}\nReduce each to ${CARD_META_MAX} columns or mark the others hideOnNarrow: true.`,
101
+ });
98
102
  }
@@ -0,0 +1,32 @@
1
+ export declare const ENTITY_LIST_RULES: {
2
+ readonly searchableFalse: {
3
+ readonly id: "entity-list/searchable-false";
4
+ readonly summary: "Operator lists are searchable unless they are on the searchable-false whitelist.";
5
+ readonly fix: "Set searchable: true or add the screen id to the whitelist.";
6
+ };
7
+ readonly defaultSortRequired: {
8
+ readonly id: "entity-list/default-sort-required";
9
+ readonly summary: "A list with active search or filterable fields declares a defaultSort.";
10
+ readonly fix: "Set defaultSort: { field, direction } on the screen.";
11
+ };
12
+ readonly defaultSortNotListed: {
13
+ readonly id: "entity-list/default-sort-not-listed";
14
+ readonly summary: "defaultSort.field is one of the listed columns.";
15
+ readonly fix: "Pick a field from columns or add it there.";
16
+ };
17
+ readonly defaultSortNotSortable: {
18
+ readonly id: "entity-list/default-sort-not-sortable";
19
+ readonly summary: "The defaultSort column is sortable.";
20
+ readonly fix: "Mark the field sortable or pick another defaultSort field.";
21
+ };
22
+ readonly detailWithoutNavigate: {
23
+ readonly id: "entity-list/detail-without-navigate";
24
+ readonly summary: "A list whose entity has a detail screen offers a navigate row action.";
25
+ readonly fix: 'Add a navigate rowAction with id "view" or "edit".';
26
+ };
27
+ readonly unknownColumn: {
28
+ readonly id: "entity-list/unknown-column";
29
+ readonly summary: "Every column without a label names an entity field, derived field or row-meta column.";
30
+ readonly fix: "Fix the column field name or give the column a label.";
31
+ };
32
+ };
@@ -0,0 +1,32 @@
1
+ export const ENTITY_LIST_RULES = {
2
+ searchableFalse: {
3
+ id: "entity-list/searchable-false",
4
+ summary: "Operator lists are searchable unless they are on the searchable-false whitelist.",
5
+ fix: "Set searchable: true or add the screen id to the whitelist.",
6
+ },
7
+ defaultSortRequired: {
8
+ id: "entity-list/default-sort-required",
9
+ summary: "A list with active search or filterable fields declares a defaultSort.",
10
+ fix: "Set defaultSort: { field, direction } on the screen.",
11
+ },
12
+ defaultSortNotListed: {
13
+ id: "entity-list/default-sort-not-listed",
14
+ summary: "defaultSort.field is one of the listed columns.",
15
+ fix: "Pick a field from columns or add it there.",
16
+ },
17
+ defaultSortNotSortable: {
18
+ id: "entity-list/default-sort-not-sortable",
19
+ summary: "The defaultSort column is sortable.",
20
+ fix: "Mark the field sortable or pick another defaultSort field.",
21
+ },
22
+ detailWithoutNavigate: {
23
+ id: "entity-list/detail-without-navigate",
24
+ summary: "A list whose entity has a detail screen offers a navigate row action.",
25
+ fix: 'Add a navigate rowAction with id "view" or "edit".',
26
+ },
27
+ unknownColumn: {
28
+ id: "entity-list/unknown-column",
29
+ summary: "Every column without a label names an entity field, derived field or row-meta column.",
30
+ fix: "Fix the column field name or give the column a label.",
31
+ },
32
+ };