@cosmicdrift/kumiko-framework 0.401.0 → 0.403.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 +29 -1
- package/agents/api/framework-ui-types.md +4 -0
- package/agents/api/renderer-web.md +1 -1
- package/agents/api/types.md +34 -0
- package/agents/api.md +1 -1
- package/agents/boot-rules.md +110 -0
- package/agents/examples/dashboard-stats.md +58 -3
- 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-phases-app.js +5 -0
- 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 +246 -0
- package/dist/engine/boot-validator/boot-rules.js +19 -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/dashboard-stat-navigate.d.ts +2 -0
- package/dist/engine/boot-validator/dashboard-stat-navigate.js +77 -0
- package/dist/engine/boot-validator/entity-list-screens-rules.d.ts +37 -0
- package/dist/engine/boot-validator/entity-list-screens-rules.js +37 -0
- package/dist/engine/boot-validator/entity-list-screens.js +86 -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/named-filter-rules.d.ts +32 -0
- package/dist/engine/boot-validator/named-filter-rules.js +32 -0
- 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/projection-list-screens.js +13 -0
- 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/entity-handlers.d.ts +1 -0
- package/dist/engine/entity-handlers.js +13 -1
- package/dist/engine/factories.d.ts +2 -0
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +3 -0
- package/dist/engine/resolve-named-list-filters.d.ts +3 -0
- package/dist/engine/resolve-named-list-filters.js +30 -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 +18 -0
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
unknownNamedFilter: {
|
|
28
|
+
id: "entity-list/unknown-named-filter",
|
|
29
|
+
summary: "Every screen namedFilters id is declared in the entity's namedFilters.",
|
|
30
|
+
fix: "Declare the rule in EntityDefinition.namedFilters or remove the id from the screen.",
|
|
31
|
+
},
|
|
32
|
+
unknownColumn: {
|
|
33
|
+
id: "entity-list/unknown-column",
|
|
34
|
+
summary: "Every column without a label names an entity field, derived field or row-meta column.",
|
|
35
|
+
fix: "Fix the column field name or give the column a label.",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -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,99 @@ 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
|
+
function checkNamedFilters(feature, screen) {
|
|
99
|
+
const declared = feature.entities?.[screen.entity]?.namedFilters ?? {};
|
|
100
|
+
collectBootRuleViolations(screen.namedFilters ?? [], (ref) => {
|
|
101
|
+
// skip: id is declared on the entity
|
|
102
|
+
if (Object.hasOwn(declared, ref.id))
|
|
103
|
+
return;
|
|
104
|
+
throw bootRuleError(ENTITY_LIST_RULES.unknownNamedFilter, {
|
|
105
|
+
message: `${screenPrefix(feature, screen)}: namedFilters id "${ref.id}" is not declared in entity "${screen.entity}" namedFilters. Known: ${Object.keys(declared).sort().join(", ") || "(none)"}.`,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const ENTITY_LIST_CHECKS = [
|
|
110
|
+
checkSearchable,
|
|
111
|
+
checkDefaultSortRequired,
|
|
112
|
+
checkDefaultSortColumn,
|
|
113
|
+
checkDetailNavigate,
|
|
114
|
+
checkColumns,
|
|
115
|
+
checkNamedFilters,
|
|
116
|
+
];
|
|
69
117
|
export function validateEntityListScreens(features) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
validateOneEntityListScreen(feature, screen);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
118
|
+
const screens = features.flatMap((feature) => Object.values(feature.screens).flatMap((screen) => screen.type === "entityList" ? [{ feature, screen }] : []));
|
|
119
|
+
collectBootRuleViolations(screens, ({ feature, screen }) => {
|
|
120
|
+
collectBootRuleViolations(ENTITY_LIST_CHECKS, (check) => check(feature, screen));
|
|
121
|
+
});
|
|
77
122
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const NAMED_FILTER_RULES: {
|
|
2
|
+
readonly projectionQueryAcceptsNamedFilter: {
|
|
3
|
+
readonly id: "named-filter/projection-query-accepts-named-filter";
|
|
4
|
+
readonly summary: "A projectionList with namedFilters binds a query whose schema has a namedFilter key.";
|
|
5
|
+
readonly fix: "Add namedFilter: z.string().optional() to the query schema, resolve it with resolveNamedListFilters and pass the conditions into executor.list filters, the only path that applies field-read and PII guards.";
|
|
6
|
+
};
|
|
7
|
+
readonly statNavigateTarget: {
|
|
8
|
+
readonly id: "named-filter/stat-navigate-target";
|
|
9
|
+
readonly summary: "A stat navigate.screen resolves to an entityList or projectionList screen.";
|
|
10
|
+
readonly fix: "Point navigate.screen at a registered list screen.";
|
|
11
|
+
};
|
|
12
|
+
readonly statNavigateFilterDeclared: {
|
|
13
|
+
readonly id: "named-filter/stat-navigate-filter-declared";
|
|
14
|
+
readonly summary: "A stat navigate.namedFilter is declared in the target screen's namedFilters.";
|
|
15
|
+
readonly fix: "Add { id, label } to namedFilters of the target screen.";
|
|
16
|
+
};
|
|
17
|
+
readonly statNavigateSameQuery: {
|
|
18
|
+
readonly id: "named-filter/stat-navigate-same-query";
|
|
19
|
+
readonly summary: "A stat with navigate counts with the target list's own query.";
|
|
20
|
+
readonly fix: "Set the stat query to the target list's query.";
|
|
21
|
+
};
|
|
22
|
+
readonly statNavigateSameNamedFilter: {
|
|
23
|
+
readonly id: "named-filter/stat-navigate-same-named-filter";
|
|
24
|
+
readonly summary: "A stat's params.namedFilter equals navigate.namedFilter.";
|
|
25
|
+
readonly fix: "Set params.namedFilter to the same id as navigate.namedFilter.";
|
|
26
|
+
};
|
|
27
|
+
readonly statNavigateStaticFilter: {
|
|
28
|
+
readonly id: "named-filter/stat-navigate-static-filter";
|
|
29
|
+
readonly summary: "A stat repeats the target screen's static filter in params.filter.";
|
|
30
|
+
readonly fix: "Set params.filter to the target screen's filter.";
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const NAMED_FILTER_RULES = {
|
|
2
|
+
projectionQueryAcceptsNamedFilter: {
|
|
3
|
+
id: "named-filter/projection-query-accepts-named-filter",
|
|
4
|
+
summary: "A projectionList with namedFilters binds a query whose schema has a namedFilter key.",
|
|
5
|
+
fix: "Add namedFilter: z.string().optional() to the query schema, resolve it with resolveNamedListFilters and pass the conditions into executor.list filters, the only path that applies field-read and PII guards.",
|
|
6
|
+
},
|
|
7
|
+
statNavigateTarget: {
|
|
8
|
+
id: "named-filter/stat-navigate-target",
|
|
9
|
+
summary: "A stat navigate.screen resolves to an entityList or projectionList screen.",
|
|
10
|
+
fix: "Point navigate.screen at a registered list screen.",
|
|
11
|
+
},
|
|
12
|
+
statNavigateFilterDeclared: {
|
|
13
|
+
id: "named-filter/stat-navigate-filter-declared",
|
|
14
|
+
summary: "A stat navigate.namedFilter is declared in the target screen's namedFilters.",
|
|
15
|
+
fix: "Add { id, label } to namedFilters of the target screen.",
|
|
16
|
+
},
|
|
17
|
+
statNavigateSameQuery: {
|
|
18
|
+
id: "named-filter/stat-navigate-same-query",
|
|
19
|
+
summary: "A stat with navigate counts with the target list's own query.",
|
|
20
|
+
fix: "Set the stat query to the target list's query.",
|
|
21
|
+
},
|
|
22
|
+
statNavigateSameNamedFilter: {
|
|
23
|
+
id: "named-filter/stat-navigate-same-named-filter",
|
|
24
|
+
summary: "A stat's params.namedFilter equals navigate.namedFilter.",
|
|
25
|
+
fix: "Set params.namedFilter to the same id as navigate.namedFilter.",
|
|
26
|
+
},
|
|
27
|
+
statNavigateStaticFilter: {
|
|
28
|
+
id: "named-filter/stat-navigate-static-filter",
|
|
29
|
+
summary: "A stat repeats the target screen's static filter in params.filter.",
|
|
30
|
+
fix: "Set params.filter to the target screen's filter.",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export declare const PII_RULES: {
|
|
2
|
+
readonly multipleSubjectAnnotations: {
|
|
3
|
+
readonly id: "pii/multiple-subject-annotations";
|
|
4
|
+
readonly summary: "A field carries at most one subject-key annotation.";
|
|
5
|
+
readonly fix: "Keep a single personal annotation on the field.";
|
|
6
|
+
};
|
|
7
|
+
readonly textWithoutPersonalStance: {
|
|
8
|
+
readonly id: "pii/text-without-personal-stance";
|
|
9
|
+
readonly summary: "Every text and longText field declares whether it holds personal data.";
|
|
10
|
+
readonly fix: "Declare a personal stance, or personal: false with a reason stating why the value is not personal data.";
|
|
11
|
+
};
|
|
12
|
+
readonly plaintextUserReference: {
|
|
13
|
+
readonly id: "pii/plaintext-user-reference";
|
|
14
|
+
readonly summary: "A reference to the user entity is never plaintext.";
|
|
15
|
+
readonly fix: 'Use the default reference field or personal: "ref" instead of personal: false.';
|
|
16
|
+
};
|
|
17
|
+
readonly lookupableNonText: {
|
|
18
|
+
readonly id: "pii/lookupable-non-text";
|
|
19
|
+
readonly summary: "Blind-index lookups apply to text fields only.";
|
|
20
|
+
readonly fix: 'Use a text field or set find: "none".';
|
|
21
|
+
};
|
|
22
|
+
readonly lookupableWithoutSubject: {
|
|
23
|
+
readonly id: "pii/lookupable-without-subject";
|
|
24
|
+
readonly summary: "A blind-index lookup requires a subject annotation on the field.";
|
|
25
|
+
readonly fix: 'Add a personal annotation or set find: "none".';
|
|
26
|
+
};
|
|
27
|
+
readonly sensitiveWithoutCiphertext: {
|
|
28
|
+
readonly id: "pii/sensitive-without-ciphertext";
|
|
29
|
+
readonly summary: "A sensitive field is stored as ciphertext at rest.";
|
|
30
|
+
readonly fix: "Add a subject annotation or encrypted: true.";
|
|
31
|
+
};
|
|
32
|
+
readonly writeOnlyEmbeddedSubField: {
|
|
33
|
+
readonly id: "pii/write-only-embedded-sub-field";
|
|
34
|
+
readonly summary: "writeOnly is only supported on top-level text fields, not on embedded sub-fields.";
|
|
35
|
+
readonly fix: "Remove writeOnly from the sub-field or declare it on a top-level text field.";
|
|
36
|
+
};
|
|
37
|
+
readonly writeOnlyNonText: {
|
|
38
|
+
readonly id: "pii/write-only-non-text";
|
|
39
|
+
readonly summary: "writeOnly applies to text fields only.";
|
|
40
|
+
readonly fix: "Use a text field or remove writeOnly.";
|
|
41
|
+
};
|
|
42
|
+
readonly writeOnlyWithoutSensitive: {
|
|
43
|
+
readonly id: "pii/write-only-without-sensitive";
|
|
44
|
+
readonly summary: "A writeOnly field is also sensitive.";
|
|
45
|
+
readonly fix: 'Declare the field with find: "secret".';
|
|
46
|
+
};
|
|
47
|
+
readonly writeOnlyConflictingFlag: {
|
|
48
|
+
readonly id: "pii/write-only-conflicting-flag";
|
|
49
|
+
readonly summary: "A writeOnly field cannot be defaulted, searched, sorted, filtered or looked up.";
|
|
50
|
+
readonly fix: "Remove either writeOnly or the conflicting flag.";
|
|
51
|
+
};
|
|
52
|
+
readonly subjectKeySortable: {
|
|
53
|
+
readonly id: "pii/subject-key-sortable";
|
|
54
|
+
readonly summary: "A field with a subject-key annotation is not sortable, its column is ciphertext.";
|
|
55
|
+
readonly fix: 'Drop sortable, use find: "exact" for equality lookups, or keep the field plaintext.';
|
|
56
|
+
};
|
|
57
|
+
readonly sensitiveSearchable: {
|
|
58
|
+
readonly id: "pii/sensitive-searchable";
|
|
59
|
+
readonly summary: "A sensitive field is never searchable.";
|
|
60
|
+
readonly fix: "Drop searchable, or add writeOnly to never return the value.";
|
|
61
|
+
};
|
|
62
|
+
readonly personalJsonbFlag: {
|
|
63
|
+
readonly id: "pii/personal-jsonb-flag";
|
|
64
|
+
readonly summary: "A personal jsonb field cannot be sortable, filterable, searchable or lookupable, its value is a ciphertext string.";
|
|
65
|
+
readonly fix: "Remove the flag or the personal annotation.";
|
|
66
|
+
};
|
|
67
|
+
readonly personalJsonbIndex: {
|
|
68
|
+
readonly id: "pii/personal-jsonb-index";
|
|
69
|
+
readonly summary: "A personal jsonb field is not part of an entity index.";
|
|
70
|
+
readonly fix: "Drop the index or the personal annotation.";
|
|
71
|
+
};
|
|
72
|
+
readonly personalJsonbCustomFields: {
|
|
73
|
+
readonly id: "pii/personal-jsonb-custom-fields";
|
|
74
|
+
readonly summary: "The custom-fields jsonb column cannot carry a personal annotation.";
|
|
75
|
+
readonly fix: "Drop the personal annotation.";
|
|
76
|
+
};
|
|
77
|
+
readonly personalOfMissingOwnerName: {
|
|
78
|
+
readonly id: "pii/personal-of-missing-owner-name";
|
|
79
|
+
readonly summary: "personal: { of } names the owner field.";
|
|
80
|
+
readonly fix: 'Pass the name of the owner field: personal: { of: "<ownerField>" }.';
|
|
81
|
+
};
|
|
82
|
+
readonly personalOfUnknownOwner: {
|
|
83
|
+
readonly id: "pii/personal-of-unknown-owner";
|
|
84
|
+
readonly summary: "The owner field named by personal: { of } exists on the entity.";
|
|
85
|
+
readonly fix: "Point personal.of at an existing field of the entity.";
|
|
86
|
+
};
|
|
87
|
+
readonly personalOfOwnerType: {
|
|
88
|
+
readonly id: "pii/personal-of-owner-type";
|
|
89
|
+
readonly summary: "The owner field named by personal: { of } is a reference or text field.";
|
|
90
|
+
readonly fix: "Use a reference or text field as owner.";
|
|
91
|
+
};
|
|
92
|
+
readonly personalOfInvalidWhenAbsent: {
|
|
93
|
+
readonly id: "pii/personal-of-invalid-when-absent";
|
|
94
|
+
readonly summary: 'personal.whenAbsent is either "tenant" or "self".';
|
|
95
|
+
readonly fix: 'Remove whenAbsent or set it to "tenant" or "self".';
|
|
96
|
+
};
|
|
97
|
+
readonly personalOfOptionalOwner: {
|
|
98
|
+
readonly id: "pii/personal-of-optional-owner";
|
|
99
|
+
readonly summary: "An optional owner field of personal: { of } declares a whenAbsent fallback.";
|
|
100
|
+
readonly fix: 'Set whenAbsent to "tenant" or "self", or make the owner field required.';
|
|
101
|
+
};
|
|
102
|
+
readonly retentionUnknownReference: {
|
|
103
|
+
readonly id: "retention/unknown-reference";
|
|
104
|
+
readonly summary: "retention.reference names an existing field or a framework timestamp.";
|
|
105
|
+
readonly fix: "Point retention.reference at an existing field or a framework-managed timestamp.";
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const PII_RULES = {
|
|
2
|
+
multipleSubjectAnnotations: {
|
|
3
|
+
id: "pii/multiple-subject-annotations",
|
|
4
|
+
summary: "A field carries at most one subject-key annotation.",
|
|
5
|
+
fix: "Keep a single personal annotation on the field.",
|
|
6
|
+
},
|
|
7
|
+
textWithoutPersonalStance: {
|
|
8
|
+
id: "pii/text-without-personal-stance",
|
|
9
|
+
summary: "Every text and longText field declares whether it holds personal data.",
|
|
10
|
+
fix: "Declare a personal stance, or personal: false with a reason stating why the value is not personal data.",
|
|
11
|
+
},
|
|
12
|
+
plaintextUserReference: {
|
|
13
|
+
id: "pii/plaintext-user-reference",
|
|
14
|
+
summary: "A reference to the user entity is never plaintext.",
|
|
15
|
+
fix: 'Use the default reference field or personal: "ref" instead of personal: false.',
|
|
16
|
+
},
|
|
17
|
+
lookupableNonText: {
|
|
18
|
+
id: "pii/lookupable-non-text",
|
|
19
|
+
summary: "Blind-index lookups apply to text fields only.",
|
|
20
|
+
fix: 'Use a text field or set find: "none".',
|
|
21
|
+
},
|
|
22
|
+
lookupableWithoutSubject: {
|
|
23
|
+
id: "pii/lookupable-without-subject",
|
|
24
|
+
summary: "A blind-index lookup requires a subject annotation on the field.",
|
|
25
|
+
fix: 'Add a personal annotation or set find: "none".',
|
|
26
|
+
},
|
|
27
|
+
sensitiveWithoutCiphertext: {
|
|
28
|
+
id: "pii/sensitive-without-ciphertext",
|
|
29
|
+
summary: "A sensitive field is stored as ciphertext at rest.",
|
|
30
|
+
fix: "Add a subject annotation or encrypted: true.",
|
|
31
|
+
},
|
|
32
|
+
writeOnlyEmbeddedSubField: {
|
|
33
|
+
id: "pii/write-only-embedded-sub-field",
|
|
34
|
+
summary: "writeOnly is only supported on top-level text fields, not on embedded sub-fields.",
|
|
35
|
+
fix: "Remove writeOnly from the sub-field or declare it on a top-level text field.",
|
|
36
|
+
},
|
|
37
|
+
writeOnlyNonText: {
|
|
38
|
+
id: "pii/write-only-non-text",
|
|
39
|
+
summary: "writeOnly applies to text fields only.",
|
|
40
|
+
fix: "Use a text field or remove writeOnly.",
|
|
41
|
+
},
|
|
42
|
+
writeOnlyWithoutSensitive: {
|
|
43
|
+
id: "pii/write-only-without-sensitive",
|
|
44
|
+
summary: "A writeOnly field is also sensitive.",
|
|
45
|
+
fix: 'Declare the field with find: "secret".',
|
|
46
|
+
},
|
|
47
|
+
writeOnlyConflictingFlag: {
|
|
48
|
+
id: "pii/write-only-conflicting-flag",
|
|
49
|
+
summary: "A writeOnly field cannot be defaulted, searched, sorted, filtered or looked up.",
|
|
50
|
+
fix: "Remove either writeOnly or the conflicting flag.",
|
|
51
|
+
},
|
|
52
|
+
subjectKeySortable: {
|
|
53
|
+
id: "pii/subject-key-sortable",
|
|
54
|
+
summary: "A field with a subject-key annotation is not sortable, its column is ciphertext.",
|
|
55
|
+
fix: 'Drop sortable, use find: "exact" for equality lookups, or keep the field plaintext.',
|
|
56
|
+
},
|
|
57
|
+
sensitiveSearchable: {
|
|
58
|
+
id: "pii/sensitive-searchable",
|
|
59
|
+
summary: "A sensitive field is never searchable.",
|
|
60
|
+
fix: "Drop searchable, or add writeOnly to never return the value.",
|
|
61
|
+
},
|
|
62
|
+
personalJsonbFlag: {
|
|
63
|
+
id: "pii/personal-jsonb-flag",
|
|
64
|
+
summary: "A personal jsonb field cannot be sortable, filterable, searchable or lookupable, its value is a ciphertext string.",
|
|
65
|
+
fix: "Remove the flag or the personal annotation.",
|
|
66
|
+
},
|
|
67
|
+
personalJsonbIndex: {
|
|
68
|
+
id: "pii/personal-jsonb-index",
|
|
69
|
+
summary: "A personal jsonb field is not part of an entity index.",
|
|
70
|
+
fix: "Drop the index or the personal annotation.",
|
|
71
|
+
},
|
|
72
|
+
personalJsonbCustomFields: {
|
|
73
|
+
id: "pii/personal-jsonb-custom-fields",
|
|
74
|
+
summary: "The custom-fields jsonb column cannot carry a personal annotation.",
|
|
75
|
+
fix: "Drop the personal annotation.",
|
|
76
|
+
},
|
|
77
|
+
personalOfMissingOwnerName: {
|
|
78
|
+
id: "pii/personal-of-missing-owner-name",
|
|
79
|
+
summary: "personal: { of } names the owner field.",
|
|
80
|
+
fix: 'Pass the name of the owner field: personal: { of: "<ownerField>" }.',
|
|
81
|
+
},
|
|
82
|
+
personalOfUnknownOwner: {
|
|
83
|
+
id: "pii/personal-of-unknown-owner",
|
|
84
|
+
summary: "The owner field named by personal: { of } exists on the entity.",
|
|
85
|
+
fix: "Point personal.of at an existing field of the entity.",
|
|
86
|
+
},
|
|
87
|
+
personalOfOwnerType: {
|
|
88
|
+
id: "pii/personal-of-owner-type",
|
|
89
|
+
summary: "The owner field named by personal: { of } is a reference or text field.",
|
|
90
|
+
fix: "Use a reference or text field as owner.",
|
|
91
|
+
},
|
|
92
|
+
personalOfInvalidWhenAbsent: {
|
|
93
|
+
id: "pii/personal-of-invalid-when-absent",
|
|
94
|
+
summary: 'personal.whenAbsent is either "tenant" or "self".',
|
|
95
|
+
fix: 'Remove whenAbsent or set it to "tenant" or "self".',
|
|
96
|
+
},
|
|
97
|
+
personalOfOptionalOwner: {
|
|
98
|
+
id: "pii/personal-of-optional-owner",
|
|
99
|
+
summary: "An optional owner field of personal: { of } declares a whenAbsent fallback.",
|
|
100
|
+
fix: 'Set whenAbsent to "tenant" or "self", or make the owner field required.',
|
|
101
|
+
},
|
|
102
|
+
retentionUnknownReference: {
|
|
103
|
+
id: "retention/unknown-reference",
|
|
104
|
+
summary: "retention.reference names an existing field or a framework timestamp.",
|
|
105
|
+
fix: "Point retention.reference at an existing field or a framework-managed timestamp.",
|
|
106
|
+
},
|
|
107
|
+
};
|