@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.
Files changed (39) hide show
  1. package/AGENTS.md +4 -2
  2. package/agents/api/framework-engine.md +24 -0
  3. package/agents/api/types.md +1 -0
  4. package/agents/api.md +1 -1
  5. package/agents/boot-rules.md +98 -0
  6. package/dist/db/replay-migration-sql.js +1 -1
  7. package/dist/engine/boot-validator/api-ext-rules.d.ts +27 -0
  8. package/dist/engine/boot-validator/api-ext-rules.js +27 -0
  9. package/dist/engine/boot-validator/api-ext.js +25 -12
  10. package/dist/engine/boot-validator/boot-check-rules.d.ts +7 -0
  11. package/dist/engine/boot-validator/boot-check-rules.js +7 -0
  12. package/dist/engine/boot-validator/boot-check.js +13 -1
  13. package/dist/engine/boot-validator/boot-report.d.ts +1 -0
  14. package/dist/engine/boot-validator/boot-report.js +58 -7
  15. package/dist/engine/boot-validator/boot-rule.d.ts +20 -0
  16. package/dist/engine/boot-validator/boot-rule.js +33 -2
  17. package/dist/engine/boot-validator/boot-rules.d.ts +210 -0
  18. package/dist/engine/boot-validator/boot-rules.js +17 -0
  19. package/dist/engine/boot-validator/card-meta-overflow-rules.d.ts +7 -0
  20. package/dist/engine/boot-validator/card-meta-overflow-rules.js +7 -0
  21. package/dist/engine/boot-validator/card-meta-overflow.js +5 -1
  22. package/dist/engine/boot-validator/entity-list-screens-rules.d.ts +32 -0
  23. package/dist/engine/boot-validator/entity-list-screens-rules.js +32 -0
  24. package/dist/engine/boot-validator/entity-list-screens.js +74 -41
  25. package/dist/engine/boot-validator/i18n-keys-rules.d.ts +27 -0
  26. package/dist/engine/boot-validator/i18n-keys-rules.js +27 -0
  27. package/dist/engine/boot-validator/i18n-keys.js +27 -16
  28. package/dist/engine/boot-validator/pii-retention-rules.d.ts +107 -0
  29. package/dist/engine/boot-validator/pii-retention-rules.js +107 -0
  30. package/dist/engine/boot-validator/pii-retention.js +76 -33
  31. package/dist/engine/boot-validator/screens-actions-rules.d.ts +7 -0
  32. package/dist/engine/boot-validator/screens-actions-rules.js +7 -0
  33. package/dist/engine/boot-validator/screens-actions.js +7 -3
  34. package/dist/engine/index.d.ts +2 -0
  35. package/dist/engine/index.js +2 -0
  36. package/dist/i18n/required-surface-keys.js +3 -0
  37. package/dist/schema-cli/validate-command.js +4 -1
  38. package/package.json +3 -3
  39. 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;
@@ -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` | 412 |
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}\\s*(?:\\(([^)]*)\\)\\s*)?AS\\b([\\s\\S]*)$`, "i"));
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
- for (const apiName of feature.usedApis) {
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 new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") but no feature exposes that API. Known exposed APIs: ${known}`);
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
- continue;
29
+ return;
25
30
  }
26
31
  const allDeps = [...feature.requires, ...feature.optionalRequires];
27
32
  if (!allDeps.includes(providerFeature)) {
28
- throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") is exposed by "${providerFeature}" but feature is not in requires/optionalRequires. Add r.requires({ features: ["${providerFeature}"] }).`);
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 new Error(`[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.`);
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
- for (const usage of feature.extensionUsages) {
49
+ collectBootRuleViolations(feature.extensionUsages, (usage) => {
41
50
  const providerFeature = extensionProviders.get(usage.extensionName);
42
51
  if (!providerFeature) {
43
- throw new Error(`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.`);
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
- continue;
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 new Error(`Feature "${feature.name}" uses extension "${usage.extensionName}" but missing requires("${providerFeature}"). Add r.requires({ features: ["${providerFeature}"] }).`);
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
+ };
@@ -0,0 +1,7 @@
1
+ export const BOOT_CHECK_RULES = {
2
+ failed: {
3
+ id: "boot-check/failed",
4
+ summary: "A feature's r.bootCheck invariant holds for the mounted feature set.",
5
+ 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 new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}`);
26
+ throw bootRuleError(BOOT_CHECK_RULES.failed, { message: prefix + message });
15
27
  }
16
28
  }
@@ -4,3 +4,4 @@ export type BootReport = {
4
4
  finish(): void;
5
5
  };
6
6
  export declare function createBootReport(): BootReport;
7
+ export declare function collectBootRuleViolations<T>(items: Iterable<T>, run: (item: T) => void): void;
@@ -1,4 +1,4 @@
1
- import { BootValidationError } from "./boot-rule.js";
1
+ import { BootRuleFailures, BootRuleViolation, BootValidationError, } from "./boot-rule.js";
2
2
  // A runtime crash is a bug in a validator (or in the input it assumed), not a
3
3
  // rule violation: it must surface as itself instead of masquerading as a finding.
4
4
  function isRuntimeCrash(thrown) {
@@ -10,6 +10,33 @@ function isRuntimeCrash(thrown) {
10
10
  thrown instanceof EvalError ||
11
11
  thrown instanceof URIError);
12
12
  }
13
+ function violationsOf(failure, checkName, feature) {
14
+ if (failure instanceof BootRuleFailures) {
15
+ return failure.failures.flatMap((nested) => violationsOf(nested, checkName, feature));
16
+ }
17
+ if (failure instanceof BootValidationError)
18
+ return failure.violations;
19
+ const featurePart = feature === undefined ? {} : { feature };
20
+ if (failure instanceof BootRuleViolation) {
21
+ return [
22
+ {
23
+ ruleId: failure.rule.id,
24
+ check: checkName,
25
+ ...featurePart,
26
+ message: failure.message,
27
+ fix: failure.rule.fix,
28
+ },
29
+ ];
30
+ }
31
+ return [
32
+ {
33
+ ruleId: `unclassified/${checkName}`,
34
+ check: checkName,
35
+ ...featurePart,
36
+ message: failure.message,
37
+ },
38
+ ];
39
+ }
13
40
  export function createBootReport() {
14
41
  const violations = [];
15
42
  let firstCrash;
@@ -31,12 +58,7 @@ export function createBootReport() {
31
58
  firstCrash ??= { thrown };
32
59
  }
33
60
  else if (thrown instanceof Error) {
34
- violations.push({
35
- ruleId: `unclassified/${checkName}`,
36
- check: checkName,
37
- ...(feature === undefined ? {} : { feature }),
38
- message: thrown.message,
39
- });
61
+ violations.push(...violationsOf(thrown, checkName, feature));
40
62
  }
41
63
  return false;
42
64
  }
@@ -45,3 +67,32 @@ export function createBootReport() {
45
67
  finish: endPhase,
46
68
  };
47
69
  }
70
+ // Boundary per item: every item runs, so one boot reports all independent violations
71
+ // instead of only the first. Crashes behave as in endPhase: dropped next to failures.
72
+ export function collectBootRuleViolations(items, run) {
73
+ const failures = [];
74
+ let firstCrash;
75
+ for (const item of items) {
76
+ try {
77
+ run(item);
78
+ }
79
+ catch (thrown) {
80
+ if (isRuntimeCrash(thrown)) {
81
+ firstCrash ??= { thrown };
82
+ }
83
+ else if (thrown instanceof BootRuleFailures) {
84
+ failures.push(...thrown.failures);
85
+ }
86
+ else if (thrown instanceof Error) {
87
+ failures.push(thrown);
88
+ }
89
+ }
90
+ }
91
+ const [onlyFailure] = failures;
92
+ if (onlyFailure !== undefined && failures.length === 1)
93
+ throw onlyFailure;
94
+ if (failures.length > 1)
95
+ throw new BootRuleFailures(failures);
96
+ if (firstCrash !== undefined)
97
+ throw firstCrash.thrown;
98
+ }
@@ -5,6 +5,26 @@ export type BootViolation = {
5
5
  readonly message: string;
6
6
  readonly fix?: string;
7
7
  };
8
+ export type BootRule = {
9
+ readonly id: string;
10
+ readonly summary: string;
11
+ readonly fix: string;
12
+ };
13
+ export type BootRuleTable = Readonly<Record<string, BootRule>>;
14
+ export declare class BootRuleViolation extends Error {
15
+ readonly rule: BootRule;
16
+ readonly name = "BootRuleViolation";
17
+ constructor(rule: BootRule, message: string);
18
+ }
19
+ export declare function bootRuleError(rule: BootRule, details: {
20
+ readonly message: string;
21
+ }): BootRuleViolation;
22
+ export declare class BootRuleFailures extends Error {
23
+ readonly failures: readonly Error[];
24
+ readonly name = "BootRuleFailures";
25
+ constructor(failures: readonly Error[]);
26
+ }
27
+ export declare function bootRuleDocLink(ruleId: string): string | undefined;
8
28
  export declare class BootValidationError extends Error {
9
29
  readonly violations: readonly BootViolation[];
10
30
  readonly name = "BootValidationError";
@@ -1,6 +1,37 @@
1
+ export class BootRuleViolation extends Error {
2
+ rule;
3
+ name = "BootRuleViolation";
4
+ constructor(rule, message) {
5
+ super(message);
6
+ this.rule = rule;
7
+ }
8
+ }
9
+ export function bootRuleError(rule, details) {
10
+ return new BootRuleViolation(rule, details.message);
11
+ }
12
+ export class BootRuleFailures extends Error {
13
+ failures;
14
+ name = "BootRuleFailures";
15
+ constructor(failures) {
16
+ super(failures.map((failure) => failure.message).join("\n"));
17
+ this.failures = failures;
18
+ }
19
+ }
20
+ const UNCLASSIFIED_RULE_PREFIX = "unclassified/";
21
+ // Must match anchorOf in packages/guards/src/_lib/agent-docs-boot-rules.ts.
22
+ export function bootRuleDocLink(ruleId) {
23
+ if (ruleId.startsWith(UNCLASSIFIED_RULE_PREFIX))
24
+ return undefined;
25
+ return `agents/boot-rules.md#${ruleId.replaceAll("/", "-")}`;
26
+ }
1
27
  function describeViolation(violation) {
2
- const line = `- [${violation.ruleId}] ${violation.message}`;
3
- return violation.fix === undefined ? line : `${line}\n Fix: ${violation.fix}`;
28
+ const lines = [`- [${violation.ruleId}] ${violation.message}`];
29
+ if (violation.fix !== undefined)
30
+ lines.push(` Fix: ${violation.fix}`);
31
+ const docLink = bootRuleDocLink(violation.ruleId);
32
+ if (docLink !== undefined)
33
+ lines.push(` Rule: ${docLink}`);
34
+ return lines.join("\n");
4
35
  }
5
36
  export class BootValidationError extends Error {
6
37
  violations;