@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
package/AGENTS.md
CHANGED
|
@@ -15,6 +15,7 @@ not exist in your version:
|
|
|
15
15
|
| `agents/api/<entry>.md` | Signatures of one entry point; options and screen types as property lists |
|
|
16
16
|
| `agents/api/types.md` | Types the entry points use but do not export, such as nested option shapes |
|
|
17
17
|
| `agents/widgets.md` | Every value export of `@cosmicdrift/kumiko-renderer/web` with its short description |
|
|
18
|
+
| `agents/boot-rules.md` | Every boot rule by id: summary, fix and source file; a boot violation links its entry |
|
|
18
19
|
| `agents/examples.md` | Index of the worked examples, one per concept |
|
|
19
20
|
| `agents/examples/<name>.md` | One tested sample app: README plus every source and test file |
|
|
20
21
|
|
|
@@ -33,7 +34,7 @@ Order of work for a new feature:
|
|
|
33
34
|
`agents/api.md` instead of `dist/*.d.ts`.
|
|
34
35
|
4. Build the feature with the declarative building blocks below.
|
|
35
36
|
5. Write an integration test against the real stack.
|
|
36
|
-
6. Run boot validation and the tests. Boot errors name the fix; apply it.
|
|
37
|
+
6. Run boot validation and the tests. Boot errors name the fix; apply it. Each violation carries a rule id whose entry is in `agents/boot-rules.md`.
|
|
37
38
|
|
|
38
39
|
## Mental model
|
|
39
40
|
|
|
@@ -293,7 +294,8 @@ produces code that compiles and then fails in production.
|
|
|
293
294
|
unknown field in a column, a nav pointing at a missing screen, an
|
|
294
295
|
encrypted field marked searchable. The message names the
|
|
295
296
|
feature, the place and the fix. Apply the fix the message names instead
|
|
296
|
-
of working around the check.
|
|
297
|
+
of working around the check. Each violation shows a rule id and, for
|
|
298
|
+
classified rules, a `Rule:` link to its entry in `agents/boot-rules.md`.
|
|
297
299
|
|
|
298
300
|
## I need X, use Y
|
|
299
301
|
|
|
@@ -11,6 +11,7 @@ function assertNotificationVariables(registry: Pick<Registry, "getAllNotificatio
|
|
|
11
11
|
function assignableAppRolesFromUsages(usages: { entityName: string; options?: Record<string, unknown> | undefined; }[]): AssignableAppRoles;
|
|
12
12
|
function assignableAppRolesOf(registry: Pick<Registry, "getExtensionUsages">): AssignableAppRoles;
|
|
13
13
|
function attachOutsideTransaction<T extends object, O extends object | undefined>(db: T, outsideTransaction: O): T & { outsideTransaction: O; };
|
|
14
|
+
function bootRuleError(rule: BootRule, details: { message: string; }): BootRuleViolation;
|
|
14
15
|
function bridgeStub(opts?: { user?: SessionUser; log?: HandlerObservability["log"]; dispatch?: Partial<HandlerDispatch>; services?: Partial<ContextServices>; }): Pick<HandlerContext, "dispatch" | "events" | "auth" | "obs" | "hasFeature" | "tz" | "locale" | "user" | "services" | "lifecycle" | "tenants">;
|
|
15
16
|
function buildAppSchema(registry: Registry, options?: BuildAppSchemaOptions): AppSchema;
|
|
16
17
|
function buildConfigFeatureSchema(registry: Registry): ConfigFeatureSchema;
|
|
@@ -20,6 +21,7 @@ function buildPipelineSteps<TPayload, TAwaits extends Record<string, AwaitedEven
|
|
|
20
21
|
function buildSessionRoles(globalRoles: string[], membershipRoles: string[]): string[];
|
|
21
22
|
function buildUpdateSchema(entity: EntityDefinition, currencies?: string[], excludedFields?: string[]): z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
22
23
|
function canActorAssignRole(actorRoles: string[], role: string, assignableAppRoles?: AssignableAppRoles): boolean;
|
|
24
|
+
function collectBootRuleViolations<T>(items: Iterable<T>, run: (item: T) => void): void;
|
|
23
25
|
function collectCardMetaOverflow(features: FeatureDefinition[]): CardMetaOverflow[];
|
|
24
26
|
function collectWriteHandlerQns(features: FeatureDefinition[]): Set<string>;
|
|
25
27
|
function compareVersions(a: string, b: string): number;
|
|
@@ -225,6 +227,14 @@ const templateVariable: { text; link; richText; money; date; percent; images; li
|
|
|
225
227
|
## Classes
|
|
226
228
|
|
|
227
229
|
```ts
|
|
230
|
+
class BootRuleViolation extends Error {
|
|
231
|
+
constructor(rule: BootRule, message: string);
|
|
232
|
+
name: unknown;
|
|
233
|
+
}
|
|
234
|
+
class BootValidationError extends Error {
|
|
235
|
+
constructor(violations: BootViolation[]);
|
|
236
|
+
name: unknown;
|
|
237
|
+
}
|
|
228
238
|
class ProbeFailure extends Error {
|
|
229
239
|
constructor(text: ProbeText);
|
|
230
240
|
}
|
|
@@ -315,6 +325,19 @@ type BeforeTransactionResult<TPrepared = unknown> = // Outcome of a write handle
|
|
|
315
325
|
| WriteFailure
|
|
316
326
|
;
|
|
317
327
|
type BootOptionName = "mail" | "kmsMode" | "fileStorage" | "billing" | "featureToggles" | "rateLimit"; // runProdApp options a stack can depend on; runProdApp checks the set at boot.
|
|
328
|
+
type BootRule = {
|
|
329
|
+
id: string;
|
|
330
|
+
summary: string;
|
|
331
|
+
fix: string;
|
|
332
|
+
};
|
|
333
|
+
type BootRuleTable = Readonly<Record<string, BootRule>>;
|
|
334
|
+
type BootViolation = {
|
|
335
|
+
ruleId: string;
|
|
336
|
+
check: string;
|
|
337
|
+
feature?: string;
|
|
338
|
+
message: string;
|
|
339
|
+
fix?: string;
|
|
340
|
+
};
|
|
318
341
|
type BundledListsOptions = { // One `table` block for every entityList, projectionList and relatedList of the bundled features.
|
|
319
342
|
table: ListTableOptions;
|
|
320
343
|
pinFirstColumn?: boolean; // Pins the first column left on every bundled list where no column is pinned.
|
|
@@ -427,6 +450,7 @@ type ContentCollectionDefinition = {
|
|
|
427
450
|
variableSchema?: Readonly<Record<string, string>>;
|
|
428
451
|
nav: {
|
|
429
452
|
label: string;
|
|
453
|
+
entryLabel?: string;
|
|
430
454
|
icon?: NavIconKey;
|
|
431
455
|
parent?: string;
|
|
432
456
|
order?: number;
|
package/agents/api/types.md
CHANGED
|
@@ -4783,6 +4783,7 @@ type MetricSpec = {
|
|
|
4783
4783
|
navigate?: MetricNavigate;
|
|
4784
4784
|
tone?: DashboardStatTone; // Tile color as on dashboard stat panels; not applied while the value is 0.
|
|
4785
4785
|
icon?: NavIconKey; // Icon chip of the tile; only drawn with `metricsStyle: "tiles"`.
|
|
4786
|
+
format?: FormatSpec; // Formats the displayed value (e.g. `date`, `enumOption`); the tone still resolves on the raw value.
|
|
4786
4787
|
};
|
|
4787
4788
|
type RecordHeaderSpec = {
|
|
4788
4789
|
title: string;
|
package/agents/api.md
CHANGED
|
@@ -8,7 +8,7 @@ Format: one line per function or constant, object and options types as property
|
|
|
8
8
|
|
|
9
9
|
| Import path | File | Exports |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| `@cosmicdrift/kumiko-framework/engine` | `api/framework-engine.md` |
|
|
11
|
+
| `@cosmicdrift/kumiko-framework/engine` | `api/framework-engine.md` | 419 |
|
|
12
12
|
| `@cosmicdrift/kumiko-framework/errors` | `api/framework-errors.md` | 25 |
|
|
13
13
|
| `@cosmicdrift/kumiko-framework/ui-types` | `api/framework-ui-types.md` | 154 |
|
|
14
14
|
| `@cosmicdrift/kumiko-framework/db` | `api/framework-db.md` | 150 |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from boot rule tables (`*-rules.ts`, `boot-rules.ts`). Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# Boot rules
|
|
4
|
+
|
|
5
|
+
`validateBoot` reports every violation as `- [<id>] <message>`, followed by a `Fix:` and a `Rule:` line that links the entry below. Violations with an `unclassified/<check>` id have no entry yet. `kumiko-schema validate --json` returns the same ids.
|
|
6
|
+
|
|
7
|
+
## api
|
|
8
|
+
|
|
9
|
+
| Rule | Summary | Fix | Source |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| <a id="api-provider-not-mounted"></a>`api/provider-not-mounted` | The feature exposing a used API is part of the app's feature list. | Register the provider feature in the app's feature list. | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
12
|
+
| <a id="api-provider-not-required"></a>`api/provider-not-required` | A feature using an API lists the exposing feature in requires or optionalRequires. | Add the exposing feature via r.requires({ features: [...] }). | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
13
|
+
| <a id="api-unknown-api"></a>`api/unknown-api` | Every r.usesApi name is exposed by some mounted feature. | Mount the feature that exposes the API or fix the API name. | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
14
|
+
|
|
15
|
+
## boot-check
|
|
16
|
+
|
|
17
|
+
| Rule | Summary | Fix | Source |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| <a id="boot-check-failed"></a>`boot-check/failed` | A feature's r.bootCheck invariant holds for the mounted feature set. | Satisfy the invariant named in the message or unmount the feature that declares it. | `packages/framework/src/engine/boot-validator/boot-check-rules.ts` |
|
|
20
|
+
|
|
21
|
+
## entity-list
|
|
22
|
+
|
|
23
|
+
| Rule | Summary | Fix | Source |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| <a id="entity-list-default-sort-not-listed"></a>`entity-list/default-sort-not-listed` | defaultSort.field is one of the listed columns. | Pick a field from columns or add it there. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
26
|
+
| <a id="entity-list-default-sort-not-sortable"></a>`entity-list/default-sort-not-sortable` | The defaultSort column is sortable. | Mark the field sortable or pick another defaultSort field. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
27
|
+
| <a id="entity-list-default-sort-required"></a>`entity-list/default-sort-required` | A list with active search or filterable fields declares a defaultSort. | Set defaultSort: { field, direction } on the screen. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
28
|
+
| <a id="entity-list-detail-without-navigate"></a>`entity-list/detail-without-navigate` | A list whose entity has a detail screen offers a navigate row action. | Add a navigate rowAction with id "view" or "edit". | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
29
|
+
| <a id="entity-list-searchable-false"></a>`entity-list/searchable-false` | Operator lists are searchable unless they are on the searchable-false whitelist. | Set searchable: true or add the screen id to the whitelist. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
30
|
+
| <a id="entity-list-unknown-column"></a>`entity-list/unknown-column` | Every column without a label names an entity field, derived field or row-meta column. | Fix the column field name or give the column a label. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
31
|
+
|
|
32
|
+
## extension
|
|
33
|
+
|
|
34
|
+
| Rule | Summary | Fix | Source |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| <a id="extension-provider-not-required"></a>`extension/provider-not-required` | A feature using an extension lists the defining feature in requires or optionalRequires. | Add the defining feature via r.requires({ features: [...] }). | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
37
|
+
| <a id="extension-unknown-extension"></a>`extension/unknown-extension` | Every extension a feature uses is defined by some mounted feature. | Register the feature that defines the extension or fix the extension name. | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
38
|
+
|
|
39
|
+
## i18n
|
|
40
|
+
|
|
41
|
+
| Rule | Summary | Fix | Source |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| <a id="i18n-locale-inconsistent"></a>`i18n/locale-inconsistent` | Every locale a feature uses is maintained for every one of its translation keys. | Add the missing locale to the key, or move that locale into a locale package. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
44
|
+
| <a id="i18n-missing-feature-key"></a>`i18n/missing-feature-key` | Every translation key a feature surface requires is defined. | Add the key via r.translations(...) in the feature. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
45
|
+
| <a id="i18n-missing-field-value-translation"></a>`i18n/missing-field-value-translation` | With requireFieldValueTranslations, select options and embedded cells are translated. | Add the keys via r.translations(...) in the owning feature. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
46
|
+
| <a id="i18n-missing-required-locale"></a>`i18n/missing-required-locale` | Every translation key exists in every locale the app requires. | Load the matching locale package or add the missing translations. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
47
|
+
| <a id="i18n-missing-settings-hub-key"></a>`i18n/missing-settings-hub-key` | Every translation key the generated settings hub requires is defined. | Add the key via r.translations(...) in the owning feature. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
48
|
+
|
|
49
|
+
## list
|
|
50
|
+
|
|
51
|
+
| Rule | Summary | Fix | Source |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| <a id="list-card-meta-overflow"></a>`list/card-meta-overflow` | A card list shows at most the maximum number of meta columns. | Reduce each list to the maximum number of meta columns or mark the others hideOnNarrow: true. | `packages/framework/src/engine/boot-validator/card-meta-overflow-rules.ts` |
|
|
54
|
+
|
|
55
|
+
## pii
|
|
56
|
+
|
|
57
|
+
| Rule | Summary | Fix | Source |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| <a id="pii-lookupable-non-text"></a>`pii/lookupable-non-text` | Blind-index lookups apply to text fields only. | Use a text field or set find: "none". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
60
|
+
| <a id="pii-lookupable-without-subject"></a>`pii/lookupable-without-subject` | A blind-index lookup requires a subject annotation on the field. | Add a personal annotation or set find: "none". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
61
|
+
| <a id="pii-multiple-subject-annotations"></a>`pii/multiple-subject-annotations` | A field carries at most one subject-key annotation. | Keep a single personal annotation on the field. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
62
|
+
| <a id="pii-personal-jsonb-custom-fields"></a>`pii/personal-jsonb-custom-fields` | The custom-fields jsonb column cannot carry a personal annotation. | Drop the personal annotation. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
63
|
+
| <a id="pii-personal-jsonb-flag"></a>`pii/personal-jsonb-flag` | A personal jsonb field cannot be sortable, filterable, searchable or lookupable, its value is a ciphertext string. | Remove the flag or the personal annotation. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
64
|
+
| <a id="pii-personal-jsonb-index"></a>`pii/personal-jsonb-index` | A personal jsonb field is not part of an entity index. | Drop the index or the personal annotation. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
65
|
+
| <a id="pii-personal-of-invalid-when-absent"></a>`pii/personal-of-invalid-when-absent` | personal.whenAbsent is either "tenant" or "self". | Remove whenAbsent or set it to "tenant" or "self". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
66
|
+
| <a id="pii-personal-of-missing-owner-name"></a>`pii/personal-of-missing-owner-name` | personal: { of } names the owner field. | Pass the name of the owner field: personal: { of: "<ownerField>" }. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
67
|
+
| <a id="pii-personal-of-optional-owner"></a>`pii/personal-of-optional-owner` | An optional owner field of personal: { of } declares a whenAbsent fallback. | Set whenAbsent to "tenant" or "self", or make the owner field required. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
68
|
+
| <a id="pii-personal-of-owner-type"></a>`pii/personal-of-owner-type` | The owner field named by personal: { of } is a reference or text field. | Use a reference or text field as owner. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
69
|
+
| <a id="pii-personal-of-unknown-owner"></a>`pii/personal-of-unknown-owner` | The owner field named by personal: { of } exists on the entity. | Point personal.of at an existing field of the entity. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
70
|
+
| <a id="pii-plaintext-user-reference"></a>`pii/plaintext-user-reference` | A reference to the user entity is never plaintext. | Use the default reference field or personal: "ref" instead of personal: false. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
71
|
+
| <a id="pii-sensitive-searchable"></a>`pii/sensitive-searchable` | A sensitive field is never searchable. | Drop searchable, or add writeOnly to never return the value. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
72
|
+
| <a id="pii-sensitive-without-ciphertext"></a>`pii/sensitive-without-ciphertext` | A sensitive field is stored as ciphertext at rest. | Add a subject annotation or encrypted: true. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
73
|
+
| <a id="pii-subject-key-sortable"></a>`pii/subject-key-sortable` | A field with a subject-key annotation is not sortable, its column is ciphertext. | Drop sortable, use find: "exact" for equality lookups, or keep the field plaintext. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
74
|
+
| <a id="pii-text-without-personal-stance"></a>`pii/text-without-personal-stance` | Every text and longText field declares whether it holds personal data. | Declare a personal stance, or personal: false with a reason stating why the value is not personal data. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
75
|
+
| <a id="pii-write-only-conflicting-flag"></a>`pii/write-only-conflicting-flag` | A writeOnly field cannot be defaulted, searched, sorted, filtered or looked up. | Remove either writeOnly or the conflicting flag. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
76
|
+
| <a id="pii-write-only-embedded-sub-field"></a>`pii/write-only-embedded-sub-field` | writeOnly is only supported on top-level text fields, not on embedded sub-fields. | Remove writeOnly from the sub-field or declare it on a top-level text field. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
77
|
+
| <a id="pii-write-only-non-text"></a>`pii/write-only-non-text` | writeOnly applies to text fields only. | Use a text field or remove writeOnly. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
78
|
+
| <a id="pii-write-only-without-sensitive"></a>`pii/write-only-without-sensitive` | A writeOnly field is also sensitive. | Declare the field with find: "secret". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
79
|
+
|
|
80
|
+
## retention
|
|
81
|
+
|
|
82
|
+
| Rule | Summary | Fix | Source |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| <a id="retention-unknown-reference"></a>`retention/unknown-reference` | retention.reference names an existing field or a framework timestamp. | Point retention.reference at an existing field or a framework-managed timestamp. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
85
|
+
|
|
86
|
+
## screen
|
|
87
|
+
|
|
88
|
+
| Rule | Summary | Fix | Source |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| <a id="screen-action-icon"></a>`screen/action-icon` | Every screen action resolves to a registered icon. | Set icon to a registered NAV_ICONS key or use an action id that resolves one via the default icon map. | `packages/framework/src/engine/boot-validator/screens-actions-rules.ts` |
|
|
91
|
+
|
|
92
|
+
## user-data-rights
|
|
93
|
+
|
|
94
|
+
| Rule | Summary | Fix | Source |
|
|
95
|
+
| --- | --- | --- | --- |
|
|
96
|
+
| <a id="user-data-rights-empty-hook"></a>`user-data-rights/empty-hook` | An EXT_USER_DATA registration declares at least one of an export or a delete hook. | Register at least one hook. | `packages/bundled-features/src/user-data-rights/boot-rules.ts` |
|
|
97
|
+
| <a id="user-data-rights-export-without-delete"></a>`user-data-rights/export-without-delete` | An entity exported via EXT_USER_DATA also registers a delete hook. | Add a delete hook, or a documented no-op delete if erasure is handled elsewhere. | `packages/bundled-features/src/user-data-rights/boot-rules.ts` |
|
|
98
|
+
| <a id="user-data-rights-missing-hook"></a>`user-data-rights/missing-hook` | An entity with user-subject fields registers an EXT_USER_DATA hook. | Register r.useExtension(EXT_USER_DATA, ...) for the entity, or a documented no-op hook if it is covered elsewhere. | `packages/bundled-features/src/user-data-rights/boot-rules.ts` |
|
|
@@ -151,7 +151,7 @@ function applyStatement(schema, statement, context) {
|
|
|
151
151
|
// skip: DO block fully handled above
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
-
const createAs = statement.match(new RegExp(`^CREATE\\s+(?:UNLOGGED\\s+)?TABLE\\s+(IF NOT EXISTS\\s+)?${IDENT}
|
|
154
|
+
const createAs = statement.match(new RegExp(`^CREATE\\s+(?:UNLOGGED\\s+)?TABLE\\s+(IF NOT EXISTS\\s+)?${IDENT}(?:\\s*\\(([^)]*)\\)\\s*|\\s+)AS\\b([\\s\\S]*)$`, "i"));
|
|
155
155
|
if (createAs?.[2] !== undefined && createAs[4] !== undefined) {
|
|
156
156
|
if (!(createAs[1] !== undefined && schema.has(createAs[2]))) {
|
|
157
157
|
const explicitColumns = createAs[3] === undefined ? undefined : parseColumnNames(createAs[3]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const API_EXT_RULES: {
|
|
2
|
+
readonly unknownApi: {
|
|
3
|
+
readonly id: "api/unknown-api";
|
|
4
|
+
readonly summary: "Every r.usesApi name is exposed by some mounted feature.";
|
|
5
|
+
readonly fix: "Mount the feature that exposes the API or fix the API name.";
|
|
6
|
+
};
|
|
7
|
+
readonly providerNotRequired: {
|
|
8
|
+
readonly id: "api/provider-not-required";
|
|
9
|
+
readonly summary: "A feature using an API lists the exposing feature in requires or optionalRequires.";
|
|
10
|
+
readonly fix: "Add the exposing feature via r.requires({ features: [...] }).";
|
|
11
|
+
};
|
|
12
|
+
readonly providerNotMounted: {
|
|
13
|
+
readonly id: "api/provider-not-mounted";
|
|
14
|
+
readonly summary: "The feature exposing a used API is part of the app's feature list.";
|
|
15
|
+
readonly fix: "Register the provider feature in the app's feature list.";
|
|
16
|
+
};
|
|
17
|
+
readonly unknownExtension: {
|
|
18
|
+
readonly id: "extension/unknown-extension";
|
|
19
|
+
readonly summary: "Every extension a feature uses is defined by some mounted feature.";
|
|
20
|
+
readonly fix: "Register the feature that defines the extension or fix the extension name.";
|
|
21
|
+
};
|
|
22
|
+
readonly extensionProviderNotRequired: {
|
|
23
|
+
readonly id: "extension/provider-not-required";
|
|
24
|
+
readonly summary: "A feature using an extension lists the defining feature in requires or optionalRequires.";
|
|
25
|
+
readonly fix: "Add the defining feature via r.requires({ features: [...] }).";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const API_EXT_RULES = {
|
|
2
|
+
unknownApi: {
|
|
3
|
+
id: "api/unknown-api",
|
|
4
|
+
summary: "Every r.usesApi name is exposed by some mounted feature.",
|
|
5
|
+
fix: "Mount the feature that exposes the API or fix the API name.",
|
|
6
|
+
},
|
|
7
|
+
providerNotRequired: {
|
|
8
|
+
id: "api/provider-not-required",
|
|
9
|
+
summary: "A feature using an API lists the exposing feature in requires or optionalRequires.",
|
|
10
|
+
fix: "Add the exposing feature via r.requires({ features: [...] }).",
|
|
11
|
+
},
|
|
12
|
+
providerNotMounted: {
|
|
13
|
+
id: "api/provider-not-mounted",
|
|
14
|
+
summary: "The feature exposing a used API is part of the app's feature list.",
|
|
15
|
+
fix: "Register the provider feature in the app's feature list.",
|
|
16
|
+
},
|
|
17
|
+
unknownExtension: {
|
|
18
|
+
id: "extension/unknown-extension",
|
|
19
|
+
summary: "Every extension a feature uses is defined by some mounted feature.",
|
|
20
|
+
fix: "Register the feature that defines the extension or fix the extension name.",
|
|
21
|
+
},
|
|
22
|
+
extensionProviderNotRequired: {
|
|
23
|
+
id: "extension/provider-not-required",
|
|
24
|
+
summary: "A feature using an extension lists the defining feature in requires or optionalRequires.",
|
|
25
|
+
fix: "Add the defining feature via r.requires({ features: [...] }).",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
import { API_EXT_RULES } from "./api-ext-rules.js";
|
|
3
|
+
import { collectBootRuleViolations } from "./boot-report.js";
|
|
4
|
+
import { bootRuleError } from "./boot-rule.js";
|
|
2
5
|
const log = createFallbackLogger("kumiko:boot");
|
|
3
6
|
// --- Cross-feature API exposure / usage matching ---
|
|
4
7
|
//
|
|
@@ -13,42 +16,52 @@ const log = createFallbackLogger("kumiko:boot");
|
|
|
13
16
|
// apiNames must be globally unique across features; validateBoot() checks
|
|
14
17
|
// this before the per-feature walk.
|
|
15
18
|
export function validateApiExposureMatching(feature, allExposedApis, featureMap) {
|
|
16
|
-
|
|
19
|
+
collectBootRuleViolations(feature.usedApis, (apiName) => {
|
|
17
20
|
const providerFeature = allExposedApis.get(apiName);
|
|
18
21
|
if (!providerFeature) {
|
|
19
22
|
const known = [...allExposedApis.keys()].sort().join(", ") || "(none)";
|
|
20
|
-
throw
|
|
23
|
+
throw bootRuleError(API_EXT_RULES.unknownApi, {
|
|
24
|
+
message: `[Feature ${feature.name}] r.usesApi("${apiName}") but no feature exposes that API. Known exposed APIs: ${known}`,
|
|
25
|
+
});
|
|
21
26
|
}
|
|
22
27
|
if (providerFeature === feature.name) {
|
|
23
28
|
log.warn(`[Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
|
|
24
|
-
|
|
29
|
+
return;
|
|
25
30
|
}
|
|
26
31
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
27
32
|
if (!allDeps.includes(providerFeature)) {
|
|
28
|
-
throw
|
|
33
|
+
throw bootRuleError(API_EXT_RULES.providerNotRequired, {
|
|
34
|
+
message: `[Feature ${feature.name}] r.usesApi("${apiName}") is exposed by "${providerFeature}" but feature is not in requires/optionalRequires. Add r.requires({ features: ["${providerFeature}"] }).`,
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
37
|
// Sanity: the provider feature must exist in this app's feature set.
|
|
31
38
|
// This should always hold when allExposedApis was built from `features`,
|
|
32
39
|
// but remains defensive for unclear constructor paths.
|
|
33
40
|
if (!featureMap.has(providerFeature)) {
|
|
34
|
-
throw
|
|
41
|
+
throw bootRuleError(API_EXT_RULES.providerNotMounted, {
|
|
42
|
+
message: `[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map. Register the provider feature in the app's feature list.`,
|
|
43
|
+
});
|
|
35
44
|
}
|
|
36
|
-
}
|
|
45
|
+
});
|
|
37
46
|
}
|
|
38
47
|
// --- Extension usage validation ---
|
|
39
48
|
export function validateExtensionUsages(feature, extensionProviders) {
|
|
40
|
-
|
|
49
|
+
collectBootRuleViolations(feature.extensionUsages, (usage) => {
|
|
41
50
|
const providerFeature = extensionProviders.get(usage.extensionName);
|
|
42
51
|
if (!providerFeature) {
|
|
43
|
-
throw
|
|
52
|
+
throw bootRuleError(API_EXT_RULES.unknownExtension, {
|
|
53
|
+
message: `Feature "${feature.name}" uses extension "${usage.extensionName}" on entity "${usage.entityName}" but no feature defines that extension. Register the feature that defines it or fix the extension name.`,
|
|
54
|
+
});
|
|
44
55
|
}
|
|
45
|
-
// self-extension is legitimate: requires(self) would be circular.
|
|
46
56
|
if (providerFeature === feature.name) {
|
|
47
|
-
|
|
57
|
+
// skip: self-extension is legitimate, requires(self) would be circular
|
|
58
|
+
return;
|
|
48
59
|
}
|
|
49
60
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
50
61
|
if (!allDeps.includes(providerFeature)) {
|
|
51
|
-
throw
|
|
62
|
+
throw bootRuleError(API_EXT_RULES.extensionProviderNotRequired, {
|
|
63
|
+
message: `Feature "${feature.name}" uses extension "${usage.extensionName}" but missing requires("${providerFeature}"). Add r.requires({ features: ["${providerFeature}"] }).`,
|
|
64
|
+
});
|
|
52
65
|
}
|
|
53
|
-
}
|
|
66
|
+
});
|
|
54
67
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const BOOT_CHECK_RULES: {
|
|
2
|
+
readonly failed: {
|
|
3
|
+
readonly id: "boot-check/failed";
|
|
4
|
+
readonly summary: "A feature's r.bootCheck invariant holds for the mounted feature set.";
|
|
5
|
+
readonly fix: "Satisfy the invariant named in the message or unmount the feature that declares it.";
|
|
6
|
+
};
|
|
7
|
+
};
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { BOOT_CHECK_RULES } from "./boot-check-rules.js";
|
|
2
|
+
import { BootRuleFailures, BootRuleViolation, bootRuleError } from "./boot-rule.js";
|
|
1
3
|
// r.bootCheck(fn) lets a feature declare its own mount-invariant instead of
|
|
2
4
|
// relying on framework-internal knowledge (the gdpr-storage.ts guards are
|
|
3
5
|
// the framework-owned version of this same idea). Each registered fn gets
|
|
4
6
|
// the full mounted-feature set and throws to fail the boot; we wrap the
|
|
5
7
|
// message with the owning feature's name so the error text points back at
|
|
6
8
|
// the feature that declared the check.
|
|
9
|
+
function prefixFailure(failure, prefix) {
|
|
10
|
+
const rule = failure instanceof BootRuleViolation ? failure.rule : BOOT_CHECK_RULES.failed;
|
|
11
|
+
return new BootRuleViolation(rule, prefix + failure.message);
|
|
12
|
+
}
|
|
7
13
|
export function runFeatureBootCheck(feature, check, features) {
|
|
8
14
|
const ctx = { features };
|
|
15
|
+
const prefix = `[Feature ${feature.name}] r.bootCheck failed: `;
|
|
9
16
|
try {
|
|
10
17
|
check(ctx);
|
|
11
18
|
}
|
|
12
19
|
catch (err) {
|
|
20
|
+
if (err instanceof BootRuleViolation)
|
|
21
|
+
throw prefixFailure(err, prefix);
|
|
22
|
+
if (err instanceof BootRuleFailures) {
|
|
23
|
+
throw new BootRuleFailures(err.failures.map((failure) => prefixFailure(failure, prefix)));
|
|
24
|
+
}
|
|
13
25
|
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
-
throw
|
|
26
|
+
throw bootRuleError(BOOT_CHECK_RULES.failed, { message: prefix + message });
|
|
15
27
|
}
|
|
16
28
|
}
|
|
@@ -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
|
|
3
|
-
|
|
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;
|