@cosmicdrift/kumiko-framework 0.401.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.
- package/AGENTS.md +4 -2
- package/agents/api/framework-engine.md +24 -0
- package/agents/api/types.md +1 -0
- package/agents/api.md +1 -1
- package/agents/boot-rules.md +98 -0
- package/dist/db/replay-migration-sql.js +1 -1
- package/dist/engine/boot-validator/api-ext-rules.d.ts +27 -0
- package/dist/engine/boot-validator/api-ext-rules.js +27 -0
- package/dist/engine/boot-validator/api-ext.js +25 -12
- package/dist/engine/boot-validator/boot-check-rules.d.ts +7 -0
- package/dist/engine/boot-validator/boot-check-rules.js +7 -0
- package/dist/engine/boot-validator/boot-check.js +13 -1
- package/dist/engine/boot-validator/boot-report.d.ts +1 -0
- package/dist/engine/boot-validator/boot-report.js +58 -7
- package/dist/engine/boot-validator/boot-rule.d.ts +20 -0
- package/dist/engine/boot-validator/boot-rule.js +33 -2
- package/dist/engine/boot-validator/boot-rules.d.ts +210 -0
- package/dist/engine/boot-validator/boot-rules.js +17 -0
- package/dist/engine/boot-validator/card-meta-overflow-rules.d.ts +7 -0
- package/dist/engine/boot-validator/card-meta-overflow-rules.js +7 -0
- package/dist/engine/boot-validator/card-meta-overflow.js +5 -1
- package/dist/engine/boot-validator/entity-list-screens-rules.d.ts +32 -0
- package/dist/engine/boot-validator/entity-list-screens-rules.js +32 -0
- package/dist/engine/boot-validator/entity-list-screens.js +74 -41
- package/dist/engine/boot-validator/i18n-keys-rules.d.ts +27 -0
- package/dist/engine/boot-validator/i18n-keys-rules.js +27 -0
- package/dist/engine/boot-validator/i18n-keys.js +27 -16
- package/dist/engine/boot-validator/pii-retention-rules.d.ts +107 -0
- package/dist/engine/boot-validator/pii-retention-rules.js +107 -0
- package/dist/engine/boot-validator/pii-retention.js +76 -33
- package/dist/engine/boot-validator/screens-actions-rules.d.ts +7 -0
- package/dist/engine/boot-validator/screens-actions-rules.js +7 -0
- package/dist/engine/boot-validator/screens-actions.js +7 -3
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.js +2 -0
- package/dist/i18n/required-surface-keys.js +3 -0
- package/dist/schema-cli/validate-command.js +4 -1
- package/package.json +3 -3
- package/src/changes.json +12 -0
|
@@ -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
|
|
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
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { LIST_ROW_META_COLUMNS } from "../../ui-types/list-row-meta.js";
|
|
2
2
|
import { normalizeListColumn } from "../screen-helpers.js";
|
|
3
|
+
import { collectBootRuleViolations } from "./boot-report.js";
|
|
4
|
+
import { bootRuleError } from "./boot-rule.js";
|
|
3
5
|
import { isSortableSqlDerivedField } from "./derived-sql-fields.js";
|
|
6
|
+
import { ENTITY_LIST_RULES } from "./entity-list-screens-rules.js";
|
|
4
7
|
/** Operator lists default searchable; low-cardinality audit trails stay opt-out. */
|
|
5
8
|
export const SEARCHABLE_FALSE_WHITELIST = new Set(["download-attempt-list", "cap-list"]);
|
|
6
9
|
// Same set computeListViewModel (headless) can actually render — NOT
|
|
@@ -21,57 +24,87 @@ function hasFilterableFields(feature, entityName) {
|
|
|
21
24
|
function hasEntityEditDetail(feature, entityName) {
|
|
22
25
|
return Object.values(feature.screens).some((s) => s.type === "entityEdit" && s.entity === entityName);
|
|
23
26
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
27
|
+
function screenPrefix(feature, screen) {
|
|
28
|
+
return `[entityList] Feature "${feature.name}" screen "${screen.id}"`;
|
|
29
|
+
}
|
|
30
|
+
function checkSearchable(feature, screen) {
|
|
26
31
|
if (screen.searchable === false && !SEARCHABLE_FALSE_WHITELIST.has(screen.id)) {
|
|
27
|
-
throw
|
|
32
|
+
throw bootRuleError(ENTITY_LIST_RULES.searchableFalse, {
|
|
33
|
+
message: `${screenPrefix(feature, screen)}: searchable defaults to true for operator lists — set searchable: true or add "${screen.id}" to the whitelist`,
|
|
34
|
+
});
|
|
28
35
|
}
|
|
36
|
+
}
|
|
37
|
+
function checkDefaultSortRequired(feature, screen) {
|
|
29
38
|
const filtersActive = hasFilterableFields(feature, screen.entity);
|
|
30
39
|
const searchable = screen.searchable !== false;
|
|
31
40
|
if ((searchable || filtersActive) && screen.defaultSort === undefined) {
|
|
32
|
-
throw
|
|
41
|
+
throw bootRuleError(ENTITY_LIST_RULES.defaultSortRequired, {
|
|
42
|
+
message: `${screenPrefix(feature, screen)}: defaultSort required when searchable or filterable fields are active. Set defaultSort: { field, direction } on the screen.`,
|
|
43
|
+
});
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
45
|
+
}
|
|
46
|
+
function checkDefaultSortColumn(feature, screen) {
|
|
47
|
+
// skip: a missing defaultSort is reported by checkDefaultSortRequired
|
|
48
|
+
if (screen.defaultSort === undefined)
|
|
49
|
+
return;
|
|
50
|
+
const prefix = screenPrefix(feature, screen);
|
|
51
|
+
const sortField = screen.defaultSort.field;
|
|
52
|
+
const col = screen.columns.find((c) => normalizeListColumn(c).field === sortField);
|
|
53
|
+
if (col === undefined) {
|
|
54
|
+
throw bootRuleError(ENTITY_LIST_RULES.defaultSortNotListed, {
|
|
55
|
+
message: `${prefix}: defaultSort.field "${sortField}" is not a listed column. Pick a field from columns or add it there.`,
|
|
56
|
+
});
|
|
47
57
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
const entityDef = feature.entities?.[screen.entity];
|
|
59
|
+
const fieldDef = entityDef?.fields[sortField];
|
|
60
|
+
const isSortable = (entityDef !== undefined && isSortableSqlDerivedField(entityDef, sortField)) ||
|
|
61
|
+
(fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true);
|
|
62
|
+
if (!isSortable) {
|
|
63
|
+
throw bootRuleError(ENTITY_LIST_RULES.defaultSortNotSortable, {
|
|
64
|
+
message: `${prefix}: defaultSort column "${sortField}" must be sortable`,
|
|
65
|
+
});
|
|
53
66
|
}
|
|
54
|
-
|
|
67
|
+
}
|
|
68
|
+
function checkDetailNavigate(feature, screen) {
|
|
69
|
+
// skip: only lists with a detail screen need a navigate row action
|
|
70
|
+
if (!hasEntityEditDetail(feature, screen.entity))
|
|
71
|
+
return;
|
|
72
|
+
const hasNavigate = (screen.rowActions ?? []).some((a) => a.kind === "navigate" && (a.id === "view" || a.id === "edit"));
|
|
73
|
+
if (!hasNavigate) {
|
|
74
|
+
throw bootRuleError(ENTITY_LIST_RULES.detailWithoutNavigate, {
|
|
75
|
+
message: `${screenPrefix(feature, screen)}: detail screen exists — add a navigate rowAction with id "view" or "edit"`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function checkColumns(feature, screen) {
|
|
80
|
+
collectBootRuleViolations(screen.columns, (col) => {
|
|
55
81
|
const normalized = normalizeListColumn(col);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
// skip: a labelled column is free text, not a field reference
|
|
83
|
+
if (normalized.label !== undefined)
|
|
84
|
+
return;
|
|
85
|
+
const entity = feature.entities?.[screen.entity];
|
|
86
|
+
const isDerived = entity?.derivedFields?.[normalized.field] !== undefined;
|
|
87
|
+
// Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) is a
|
|
88
|
+
// real base-table column without being a declared entity field — e.g.
|
|
89
|
+
// a SystemAdmin cross-tenant list exposing tenantId.
|
|
90
|
+
const isRowMeta = LIST_ROW_META_COLUMN_NAMES.has(normalized.field);
|
|
91
|
+
if (!entity?.fields[normalized.field] && !isDerived && !isRowMeta) {
|
|
92
|
+
throw bootRuleError(ENTITY_LIST_RULES.unknownColumn, {
|
|
93
|
+
message: `${screenPrefix(feature, screen)}: unknown column field "${normalized.field}"`,
|
|
94
|
+
});
|
|
66
95
|
}
|
|
67
|
-
}
|
|
96
|
+
});
|
|
68
97
|
}
|
|
98
|
+
const ENTITY_LIST_CHECKS = [
|
|
99
|
+
checkSearchable,
|
|
100
|
+
checkDefaultSortRequired,
|
|
101
|
+
checkDefaultSortColumn,
|
|
102
|
+
checkDetailNavigate,
|
|
103
|
+
checkColumns,
|
|
104
|
+
];
|
|
69
105
|
export function validateEntityListScreens(features) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
validateOneEntityListScreen(feature, screen);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
106
|
+
const screens = features.flatMap((feature) => Object.values(feature.screens).flatMap((screen) => screen.type === "entityList" ? [{ feature, screen }] : []));
|
|
107
|
+
collectBootRuleViolations(screens, ({ feature, screen }) => {
|
|
108
|
+
collectBootRuleViolations(ENTITY_LIST_CHECKS, (check) => check(feature, screen));
|
|
109
|
+
});
|
|
77
110
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const I18N_RULES: {
|
|
2
|
+
readonly missingFeatureKey: {
|
|
3
|
+
readonly id: "i18n/missing-feature-key";
|
|
4
|
+
readonly summary: "Every translation key a feature surface requires is defined.";
|
|
5
|
+
readonly fix: "Add the key via r.translations(...) in the feature.";
|
|
6
|
+
};
|
|
7
|
+
readonly missingSettingsHubKey: {
|
|
8
|
+
readonly id: "i18n/missing-settings-hub-key";
|
|
9
|
+
readonly summary: "Every translation key the generated settings hub requires is defined.";
|
|
10
|
+
readonly fix: "Add the key via r.translations(...) in the owning feature.";
|
|
11
|
+
};
|
|
12
|
+
readonly localeInconsistent: {
|
|
13
|
+
readonly id: "i18n/locale-inconsistent";
|
|
14
|
+
readonly summary: "Every locale a feature uses is maintained for every one of its translation keys.";
|
|
15
|
+
readonly fix: "Add the missing locale to the key, or move that locale into a locale package.";
|
|
16
|
+
};
|
|
17
|
+
readonly missingRequiredLocale: {
|
|
18
|
+
readonly id: "i18n/missing-required-locale";
|
|
19
|
+
readonly summary: "Every translation key exists in every locale the app requires.";
|
|
20
|
+
readonly fix: "Load the matching locale package or add the missing translations.";
|
|
21
|
+
};
|
|
22
|
+
readonly missingFieldValueTranslation: {
|
|
23
|
+
readonly id: "i18n/missing-field-value-translation";
|
|
24
|
+
readonly summary: "With requireFieldValueTranslations, select options and embedded cells are translated.";
|
|
25
|
+
readonly fix: "Add the keys via r.translations(...) in the owning feature.";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const I18N_RULES = {
|
|
2
|
+
missingFeatureKey: {
|
|
3
|
+
id: "i18n/missing-feature-key",
|
|
4
|
+
summary: "Every translation key a feature surface requires is defined.",
|
|
5
|
+
fix: "Add the key via r.translations(...) in the feature.",
|
|
6
|
+
},
|
|
7
|
+
missingSettingsHubKey: {
|
|
8
|
+
id: "i18n/missing-settings-hub-key",
|
|
9
|
+
summary: "Every translation key the generated settings hub requires is defined.",
|
|
10
|
+
fix: "Add the key via r.translations(...) in the owning feature.",
|
|
11
|
+
},
|
|
12
|
+
localeInconsistent: {
|
|
13
|
+
id: "i18n/locale-inconsistent",
|
|
14
|
+
summary: "Every locale a feature uses is maintained for every one of its translation keys.",
|
|
15
|
+
fix: "Add the missing locale to the key, or move that locale into a locale package.",
|
|
16
|
+
},
|
|
17
|
+
missingRequiredLocale: {
|
|
18
|
+
id: "i18n/missing-required-locale",
|
|
19
|
+
summary: "Every translation key exists in every locale the app requires.",
|
|
20
|
+
fix: "Load the matching locale package or add the missing translations.",
|
|
21
|
+
},
|
|
22
|
+
missingFieldValueTranslation: {
|
|
23
|
+
id: "i18n/missing-field-value-translation",
|
|
24
|
+
summary: "With requireFieldValueTranslations, select options and embedded cells are translated.",
|
|
25
|
+
fix: "Add the keys via r.translations(...) in the owning feature.",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { buildEffectiveTranslationKeys, featureHasI18nSurface, fieldValueTranslationKeysFromFeature, findRequiredLocaleGaps, findTranslationLocaleGaps, requiredKeysFromFeature, requiredKeysFromNav, requiredKeysFromScreen, requiredKeysFromWorkspace, screenTitleKey, } from "../../i18n/required-surface-keys.js";
|
|
2
2
|
import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, } from "../build-config-feature-schema.js";
|
|
3
3
|
import { EXTENSION_SELECTOR_HINT_KEY } from "../extension-selector-plugins.js";
|
|
4
|
+
import { collectBootRuleViolations } from "./boot-report.js";
|
|
5
|
+
import { bootRuleError } from "./boot-rule.js";
|
|
6
|
+
import { I18N_RULES } from "./i18n-keys-rules.js";
|
|
4
7
|
function isDormantSecretsScreen(screen) {
|
|
5
8
|
return screen.type === "secretsEdit" && screen.dormant === true;
|
|
6
9
|
}
|
|
@@ -67,28 +70,32 @@ export function validateI18nSurfaceKeys(features, registry) {
|
|
|
67
70
|
// declared secret would fail boot on its own generated keys.
|
|
68
71
|
for (const key of Object.keys(generatedConfigHub.translations ?? {}))
|
|
69
72
|
defined.add(key);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
73
|
+
const featureKeys = features.filter(featureHasI18nSurface).flatMap((feature) => requiredKeysFromFeature(feature)
|
|
74
|
+
.filter((key) => !isFrameworkOwnedI18nKey(key))
|
|
75
|
+
.map((key) => ({ featureName: feature.name, key })));
|
|
76
|
+
const checkFeatureKeys = () => collectBootRuleViolations(featureKeys, ({ featureName, key }) => {
|
|
77
|
+
if (!hasDefinedTranslation(defined, key)) {
|
|
78
|
+
throw bootRuleError(I18N_RULES.missingFeatureKey, {
|
|
79
|
+
message: `[i18n] Feature "${featureName}": required translation key missing: "${key}". Add it via r.translations(...) in this feature.`,
|
|
80
|
+
});
|
|
79
81
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
+
});
|
|
83
|
+
const checkSettingsHubKeys = () => collectBootRuleViolations(requiredKeysFromGeneratedConfigHub(generatedConfigHub), (key) => {
|
|
82
84
|
if (!hasDefinedTranslation(defined, key)) {
|
|
83
|
-
throw
|
|
85
|
+
throw bootRuleError(I18N_RULES.missingSettingsHubKey, {
|
|
86
|
+
message: `[i18n] Settings-Hub: required translation key missing: "${key}". Add it via r.translations(...) in the owning feature.`,
|
|
87
|
+
});
|
|
84
88
|
}
|
|
85
|
-
}
|
|
89
|
+
});
|
|
90
|
+
collectBootRuleViolations([checkFeatureKeys, checkSettingsHubKeys], (run) => run());
|
|
86
91
|
}
|
|
87
92
|
export function validateTranslationLocaleConsistency(features) {
|
|
88
93
|
const gaps = findTranslationLocaleGaps(features);
|
|
89
94
|
if (gaps.length > 0) {
|
|
90
95
|
const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing locale(s) ${gap.missingLocales.join(", ")}.`);
|
|
91
|
-
throw
|
|
96
|
+
throw bootRuleError(I18N_RULES.localeInconsistent, {
|
|
97
|
+
message: `${lines.join("\n")}\nEvery locale a feature uses must be maintained for every one of its translation keys; otherwise move that locale into a locale package.`,
|
|
98
|
+
});
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
export function validateRequiredLocales(features, requiredLocales) {
|
|
@@ -97,7 +104,9 @@ export function validateRequiredLocales(features, requiredLocales) {
|
|
|
97
104
|
: findRequiredLocaleGaps(features, requiredLocales);
|
|
98
105
|
if (requiredLocales !== undefined && gaps.length > 0) {
|
|
99
106
|
const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing required locale(s) ${gap.missingLocales.join(", ")}.`);
|
|
100
|
-
throw
|
|
107
|
+
throw bootRuleError(I18N_RULES.missingRequiredLocale, {
|
|
108
|
+
message: `${lines.join("\n")}\nThe app requires locales [${requiredLocales.join(", ")}] for every translation key; load the matching locale package or add the missing translations.`,
|
|
109
|
+
});
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
export function validateFieldValueTranslations(features, enabled) {
|
|
@@ -113,6 +122,8 @@ export function validateFieldValueTranslations(features, enabled) {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
if (missing.length > 0) {
|
|
116
|
-
throw
|
|
125
|
+
throw bootRuleError(I18N_RULES.missingFieldValueTranslation, {
|
|
126
|
+
message: `[i18n] ${missing.length} select option / embedded-cell translation key(s) missing (requireFieldValueTranslations):\n${missing.join("\n")}\nAdd them via r.translations(...) in the owning feature.`,
|
|
127
|
+
});
|
|
117
128
|
}
|
|
118
129
|
}
|