@owlmeans/viable-common 0.0.4
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/README.md +68 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/viable-common/SKILL.md +179 -0
- package/build/agent/index.d.ts +4 -0
- package/build/agent/index.d.ts.map +1 -0
- package/build/agent/index.js +3 -0
- package/build/agent/index.js.map +1 -0
- package/build/agent/memory.d.ts +12 -0
- package/build/agent/memory.d.ts.map +1 -0
- package/build/agent/memory.js +11 -0
- package/build/agent/memory.js.map +1 -0
- package/build/agent/spectator/consts.d.ts +9 -0
- package/build/agent/spectator/consts.d.ts.map +1 -0
- package/build/agent/spectator/consts.js +10 -0
- package/build/agent/spectator/consts.js.map +1 -0
- package/build/agent/spectator/index.d.ts +3 -0
- package/build/agent/spectator/index.d.ts.map +1 -0
- package/build/agent/spectator/index.js +2 -0
- package/build/agent/spectator/index.js.map +1 -0
- package/build/agent/spectator/types.d.ts +13 -0
- package/build/agent/spectator/types.d.ts.map +1 -0
- package/build/agent/spectator/types.js +2 -0
- package/build/agent/spectator/types.js.map +1 -0
- package/build/agent/types.d.ts +10 -0
- package/build/agent/types.d.ts.map +1 -0
- package/build/agent/types.js +2 -0
- package/build/agent/types.js.map +1 -0
- package/build/areas/consts.d.ts +88 -0
- package/build/areas/consts.d.ts.map +1 -0
- package/build/areas/consts.js +106 -0
- package/build/areas/consts.js.map +1 -0
- package/build/areas/index.d.ts +3 -0
- package/build/areas/index.d.ts.map +1 -0
- package/build/areas/index.js +2 -0
- package/build/areas/index.js.map +1 -0
- package/build/areas/types.d.ts +28 -0
- package/build/areas/types.d.ts.map +1 -0
- package/build/areas/types.js +2 -0
- package/build/areas/types.js.map +1 -0
- package/build/ba/consts.d.ts +38 -0
- package/build/ba/consts.d.ts.map +1 -0
- package/build/ba/consts.js +41 -0
- package/build/ba/consts.js.map +1 -0
- package/build/ba/helpers.d.ts +41 -0
- package/build/ba/helpers.d.ts.map +1 -0
- package/build/ba/helpers.js +79 -0
- package/build/ba/helpers.js.map +1 -0
- package/build/ba/index.d.ts +5 -0
- package/build/ba/index.d.ts.map +1 -0
- package/build/ba/index.js +4 -0
- package/build/ba/index.js.map +1 -0
- package/build/ba/schemas.d.ts +13 -0
- package/build/ba/schemas.d.ts.map +1 -0
- package/build/ba/schemas.js +181 -0
- package/build/ba/schemas.js.map +1 -0
- package/build/ba/types.d.ts +113 -0
- package/build/ba/types.d.ts.map +1 -0
- package/build/ba/types.js +2 -0
- package/build/ba/types.js.map +1 -0
- package/build/blueprint/case.d.ts +73 -0
- package/build/blueprint/case.d.ts.map +1 -0
- package/build/blueprint/case.js +244 -0
- package/build/blueprint/case.js.map +1 -0
- package/build/blueprint/consts.d.ts +11 -0
- package/build/blueprint/consts.d.ts.map +1 -0
- package/build/blueprint/consts.js +11 -0
- package/build/blueprint/consts.js.map +1 -0
- package/build/blueprint/index.d.ts +6 -0
- package/build/blueprint/index.d.ts.map +1 -0
- package/build/blueprint/index.js +6 -0
- package/build/blueprint/index.js.map +1 -0
- package/build/blueprint/resolve.d.ts +51 -0
- package/build/blueprint/resolve.d.ts.map +1 -0
- package/build/blueprint/resolve.js +108 -0
- package/build/blueprint/resolve.js.map +1 -0
- package/build/blueprint/schemas.d.ts +41 -0
- package/build/blueprint/schemas.d.ts.map +1 -0
- package/build/blueprint/schemas.js +27 -0
- package/build/blueprint/schemas.js.map +1 -0
- package/build/blueprint/types.d.ts +186 -0
- package/build/blueprint/types.d.ts.map +1 -0
- package/build/blueprint/types.js +32 -0
- package/build/blueprint/types.js.map +1 -0
- package/build/branding.d.ts +19 -0
- package/build/branding.d.ts.map +1 -0
- package/build/branding.js +32 -0
- package/build/branding.js.map +1 -0
- package/build/connect/consts.d.ts +378 -0
- package/build/connect/consts.d.ts.map +1 -0
- package/build/connect/consts.js +425 -0
- package/build/connect/consts.js.map +1 -0
- package/build/connect/entrypoints.d.ts +41 -0
- package/build/connect/entrypoints.d.ts.map +1 -0
- package/build/connect/entrypoints.js +141 -0
- package/build/connect/entrypoints.js.map +1 -0
- package/build/connect/errors.d.ts +50 -0
- package/build/connect/errors.d.ts.map +1 -0
- package/build/connect/errors.js +77 -0
- package/build/connect/errors.js.map +1 -0
- package/build/connect/index.d.ts +8 -0
- package/build/connect/index.d.ts.map +1 -0
- package/build/connect/index.js +6 -0
- package/build/connect/index.js.map +1 -0
- package/build/connect/ops.d.ts +192 -0
- package/build/connect/ops.d.ts.map +1 -0
- package/build/connect/ops.js +2 -0
- package/build/connect/ops.js.map +1 -0
- package/build/connect/references.d.ts +194 -0
- package/build/connect/references.d.ts.map +1 -0
- package/build/connect/references.js +62 -0
- package/build/connect/references.js.map +1 -0
- package/build/connect/schemas.d.ts +59 -0
- package/build/connect/schemas.d.ts.map +1 -0
- package/build/connect/schemas.js +346 -0
- package/build/connect/schemas.js.map +1 -0
- package/build/connect/types.d.ts +428 -0
- package/build/connect/types.d.ts.map +1 -0
- package/build/connect/types.js +2 -0
- package/build/connect/types.js.map +1 -0
- package/build/consts.d.ts +9 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +31 -0
- package/build/consts.js.map +1 -0
- package/build/convert/consts.d.ts +539 -0
- package/build/convert/consts.d.ts.map +1 -0
- package/build/convert/consts.js +630 -0
- package/build/convert/consts.js.map +1 -0
- package/build/convert/helpers.d.ts +91 -0
- package/build/convert/helpers.d.ts.map +1 -0
- package/build/convert/helpers.js +301 -0
- package/build/convert/helpers.js.map +1 -0
- package/build/convert/index.d.ts +11 -0
- package/build/convert/index.d.ts.map +1 -0
- package/build/convert/index.js +4 -0
- package/build/convert/index.js.map +1 -0
- package/build/convert/schemas.d.ts +62 -0
- package/build/convert/schemas.d.ts.map +1 -0
- package/build/convert/schemas.js +626 -0
- package/build/convert/schemas.js.map +1 -0
- package/build/convert/types.d.ts +683 -0
- package/build/convert/types.d.ts.map +1 -0
- package/build/convert/types.js +2 -0
- package/build/convert/types.js.map +1 -0
- package/build/design/consts.d.ts +25 -0
- package/build/design/consts.d.ts.map +1 -0
- package/build/design/consts.js +26 -0
- package/build/design/consts.js.map +1 -0
- package/build/design/helpers.d.ts +60 -0
- package/build/design/helpers.d.ts.map +1 -0
- package/build/design/helpers.js +221 -0
- package/build/design/helpers.js.map +1 -0
- package/build/design/index.d.ts +6 -0
- package/build/design/index.d.ts.map +1 -0
- package/build/design/index.js +5 -0
- package/build/design/index.js.map +1 -0
- package/build/design/runtime.d.ts +126 -0
- package/build/design/runtime.d.ts.map +1 -0
- package/build/design/runtime.js +65 -0
- package/build/design/runtime.js.map +1 -0
- package/build/design/schemas.d.ts +21 -0
- package/build/design/schemas.d.ts.map +1 -0
- package/build/design/schemas.js +369 -0
- package/build/design/schemas.js.map +1 -0
- package/build/design/types.d.ts +199 -0
- package/build/design/types.d.ts.map +1 -0
- package/build/design/types.js +2 -0
- package/build/design/types.js.map +1 -0
- package/build/dev/consts.d.ts +32 -0
- package/build/dev/consts.d.ts.map +1 -0
- package/build/dev/consts.js +33 -0
- package/build/dev/consts.js.map +1 -0
- package/build/dev/index.d.ts +4 -0
- package/build/dev/index.d.ts.map +1 -0
- package/build/dev/index.js +3 -0
- package/build/dev/index.js.map +1 -0
- package/build/dev/schemas.d.ts +15 -0
- package/build/dev/schemas.d.ts.map +1 -0
- package/build/dev/schemas.js +116 -0
- package/build/dev/schemas.js.map +1 -0
- package/build/dev/types.d.ts +42 -0
- package/build/dev/types.d.ts.map +1 -0
- package/build/dev/types.js +2 -0
- package/build/dev/types.js.map +1 -0
- package/build/execution/consts.d.ts +36 -0
- package/build/execution/consts.d.ts.map +1 -0
- package/build/execution/consts.js +37 -0
- package/build/execution/consts.js.map +1 -0
- package/build/execution/index.d.ts +3 -0
- package/build/execution/index.d.ts.map +1 -0
- package/build/execution/index.js +2 -0
- package/build/execution/index.js.map +1 -0
- package/build/execution/types.d.ts +50 -0
- package/build/execution/types.d.ts.map +1 -0
- package/build/execution/types.js +2 -0
- package/build/execution/types.js.map +1 -0
- package/build/index.d.ts +22 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +21 -0
- package/build/index.js.map +1 -0
- package/build/integrity/consts.d.ts +226 -0
- package/build/integrity/consts.d.ts.map +1 -0
- package/build/integrity/consts.js +401 -0
- package/build/integrity/consts.js.map +1 -0
- package/build/integrity/index.d.ts +4 -0
- package/build/integrity/index.d.ts.map +1 -0
- package/build/integrity/index.js +3 -0
- package/build/integrity/index.js.map +1 -0
- package/build/integrity/types.d.ts +35 -0
- package/build/integrity/types.d.ts.map +1 -0
- package/build/integrity/types.js +2 -0
- package/build/integrity/types.js.map +1 -0
- package/build/integrity/verify.d.ts +23 -0
- package/build/integrity/verify.d.ts.map +1 -0
- package/build/integrity/verify.js +198 -0
- package/build/integrity/verify.js.map +1 -0
- package/build/metadata/consts.d.ts +101 -0
- package/build/metadata/consts.d.ts.map +1 -0
- package/build/metadata/consts.js +104 -0
- package/build/metadata/consts.js.map +1 -0
- package/build/metadata/index.d.ts +3 -0
- package/build/metadata/index.d.ts.map +1 -0
- package/build/metadata/index.js +3 -0
- package/build/metadata/index.js.map +1 -0
- package/build/metadata/types.d.ts +286 -0
- package/build/metadata/types.d.ts.map +1 -0
- package/build/metadata/types.js +39 -0
- package/build/metadata/types.js.map +1 -0
- package/build/moderation/consts.d.ts +58 -0
- package/build/moderation/consts.d.ts.map +1 -0
- package/build/moderation/consts.js +60 -0
- package/build/moderation/consts.js.map +1 -0
- package/build/moderation/index.d.ts +5 -0
- package/build/moderation/index.d.ts.map +1 -0
- package/build/moderation/index.js +4 -0
- package/build/moderation/index.js.map +1 -0
- package/build/moderation/schemas.d.ts +19 -0
- package/build/moderation/schemas.d.ts.map +1 -0
- package/build/moderation/schemas.js +26 -0
- package/build/moderation/schemas.js.map +1 -0
- package/build/moderation/types.d.ts +64 -0
- package/build/moderation/types.d.ts.map +1 -0
- package/build/moderation/types.js +2 -0
- package/build/moderation/types.js.map +1 -0
- package/build/moderation/utils.d.ts +21 -0
- package/build/moderation/utils.d.ts.map +1 -0
- package/build/moderation/utils.js +31 -0
- package/build/moderation/utils.js.map +1 -0
- package/build/preview/consts.d.ts +10 -0
- package/build/preview/consts.d.ts.map +1 -0
- package/build/preview/consts.js +11 -0
- package/build/preview/consts.js.map +1 -0
- package/build/preview/index.d.ts +3 -0
- package/build/preview/index.d.ts.map +1 -0
- package/build/preview/index.js +2 -0
- package/build/preview/index.js.map +1 -0
- package/build/preview/types.d.ts +60 -0
- package/build/preview/types.d.ts.map +1 -0
- package/build/preview/types.js +2 -0
- package/build/preview/types.js.map +1 -0
- package/build/scaffold/consts.d.ts +25 -0
- package/build/scaffold/consts.d.ts.map +1 -0
- package/build/scaffold/consts.js +26 -0
- package/build/scaffold/consts.js.map +1 -0
- package/build/scaffold/helpers.d.ts +35 -0
- package/build/scaffold/helpers.d.ts.map +1 -0
- package/build/scaffold/helpers.js +54 -0
- package/build/scaffold/helpers.js.map +1 -0
- package/build/scaffold/index.d.ts +5 -0
- package/build/scaffold/index.d.ts.map +1 -0
- package/build/scaffold/index.js +4 -0
- package/build/scaffold/index.js.map +1 -0
- package/build/scaffold/schemas.d.ts +5 -0
- package/build/scaffold/schemas.d.ts.map +1 -0
- package/build/scaffold/schemas.js +273 -0
- package/build/scaffold/schemas.js.map +1 -0
- package/build/scaffold/types.d.ts +144 -0
- package/build/scaffold/types.d.ts.map +1 -0
- package/build/scaffold/types.js +2 -0
- package/build/scaffold/types.js.map +1 -0
- package/build/skills/catalogue.d.ts +47 -0
- package/build/skills/catalogue.d.ts.map +1 -0
- package/build/skills/catalogue.js +1566 -0
- package/build/skills/catalogue.js.map +1 -0
- package/build/skills/consts.d.ts +104 -0
- package/build/skills/consts.d.ts.map +1 -0
- package/build/skills/consts.js +170 -0
- package/build/skills/consts.js.map +1 -0
- package/build/skills/index.d.ts +4 -0
- package/build/skills/index.d.ts.map +1 -0
- package/build/skills/index.js +4 -0
- package/build/skills/index.js.map +1 -0
- package/build/skills/roles.d.ts +52 -0
- package/build/skills/roles.d.ts.map +1 -0
- package/build/skills/roles.js +256 -0
- package/build/skills/roles.js.map +1 -0
- package/build/slot/consts.d.ts +305 -0
- package/build/slot/consts.d.ts.map +1 -0
- package/build/slot/consts.js +376 -0
- package/build/slot/consts.js.map +1 -0
- package/build/slot/helpers.d.ts +25 -0
- package/build/slot/helpers.d.ts.map +1 -0
- package/build/slot/helpers.js +27 -0
- package/build/slot/helpers.js.map +1 -0
- package/build/slot/index.d.ts +6 -0
- package/build/slot/index.d.ts.map +1 -0
- package/build/slot/index.js +5 -0
- package/build/slot/index.js.map +1 -0
- package/build/slot/layout.d.ts +72 -0
- package/build/slot/layout.d.ts.map +1 -0
- package/build/slot/layout.js +82 -0
- package/build/slot/layout.js.map +1 -0
- package/build/slot/schemas.d.ts +12 -0
- package/build/slot/schemas.d.ts.map +1 -0
- package/build/slot/schemas.js +43 -0
- package/build/slot/schemas.js.map +1 -0
- package/build/slot/types.d.ts +147 -0
- package/build/slot/types.d.ts.map +1 -0
- package/build/slot/types.js +2 -0
- package/build/slot/types.js.map +1 -0
- package/build/topology/card.d.ts +23 -0
- package/build/topology/card.d.ts.map +1 -0
- package/build/topology/card.js +95 -0
- package/build/topology/card.js.map +1 -0
- package/build/topology/consts.d.ts +32 -0
- package/build/topology/consts.d.ts.map +1 -0
- package/build/topology/consts.js +55 -0
- package/build/topology/consts.js.map +1 -0
- package/build/topology/helpers.d.ts +44 -0
- package/build/topology/helpers.d.ts.map +1 -0
- package/build/topology/helpers.js +112 -0
- package/build/topology/helpers.js.map +1 -0
- package/build/topology/index.d.ts +5 -0
- package/build/topology/index.d.ts.map +1 -0
- package/build/topology/index.js +4 -0
- package/build/topology/index.js.map +1 -0
- package/build/topology/types.d.ts +83 -0
- package/build/topology/types.d.ts.map +1 -0
- package/build/topology/types.js +2 -0
- package/build/topology/types.js.map +1 -0
- package/build/types.d.ts +88 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/ux/consts.d.ts +5 -0
- package/build/ux/consts.d.ts.map +1 -0
- package/build/ux/consts.js +6 -0
- package/build/ux/consts.js.map +1 -0
- package/build/ux/index.d.ts +4 -0
- package/build/ux/index.d.ts.map +1 -0
- package/build/ux/index.js +3 -0
- package/build/ux/index.js.map +1 -0
- package/build/ux/schemas.d.ts +8 -0
- package/build/ux/schemas.d.ts.map +1 -0
- package/build/ux/schemas.js +91 -0
- package/build/ux/schemas.js.map +1 -0
- package/build/ux/types.d.ts +21 -0
- package/build/ux/types.d.ts.map +1 -0
- package/build/ux/types.js +2 -0
- package/build/ux/types.js.map +1 -0
- package/package.json +71 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/memory.ts +16 -0
- package/src/agent/spectator/consts.ts +10 -0
- package/src/agent/spectator/index.ts +3 -0
- package/src/agent/spectator/types.ts +14 -0
- package/src/agent/types.ts +10 -0
- package/src/areas/consts.ts +112 -0
- package/src/areas/index.ts +3 -0
- package/src/areas/types.ts +29 -0
- package/src/ba/consts.ts +40 -0
- package/src/ba/helpers.ts +93 -0
- package/src/ba/index.ts +5 -0
- package/src/ba/schemas.ts +196 -0
- package/src/ba/types.ts +125 -0
- package/src/blueprint/case.ts +272 -0
- package/src/blueprint/consts.ts +11 -0
- package/src/blueprint/index.ts +6 -0
- package/src/blueprint/resolve.ts +133 -0
- package/src/blueprint/schemas.ts +44 -0
- package/src/blueprint/types.ts +204 -0
- package/src/branding.ts +34 -0
- package/src/connect/consts.ts +441 -0
- package/src/connect/entrypoints.ts +291 -0
- package/src/connect/errors.ts +91 -0
- package/src/connect/index.ts +7 -0
- package/src/connect/ops.ts +208 -0
- package/src/connect/references.ts +89 -0
- package/src/connect/schemas.ts +384 -0
- package/src/connect/types.ts +456 -0
- package/src/consts.ts +34 -0
- package/src/convert/consts.ts +672 -0
- package/src/convert/helpers.ts +340 -0
- package/src/convert/index.ts +10 -0
- package/src/convert/schemas.ts +663 -0
- package/src/convert/types.ts +741 -0
- package/src/design/consts.ts +26 -0
- package/src/design/helpers.ts +255 -0
- package/src/design/index.ts +5 -0
- package/src/design/runtime.ts +142 -0
- package/src/design/schemas.ts +384 -0
- package/src/design/types.ts +191 -0
- package/src/dev/consts.ts +35 -0
- package/src/dev/index.ts +4 -0
- package/src/dev/schemas.ts +124 -0
- package/src/dev/types.ts +47 -0
- package/src/execution/consts.ts +37 -0
- package/src/execution/index.ts +3 -0
- package/src/execution/types.ts +55 -0
- package/src/index.ts +22 -0
- package/src/integrity/consts.ts +477 -0
- package/src/integrity/index.ts +3 -0
- package/src/integrity/types.ts +37 -0
- package/src/integrity/verify.ts +221 -0
- package/src/metadata/consts.ts +118 -0
- package/src/metadata/index.ts +3 -0
- package/src/metadata/types.ts +310 -0
- package/src/moderation/consts.ts +59 -0
- package/src/moderation/index.ts +4 -0
- package/src/moderation/schemas.ts +28 -0
- package/src/moderation/types.ts +68 -0
- package/src/moderation/utils.ts +35 -0
- package/src/preview/consts.ts +9 -0
- package/src/preview/index.ts +3 -0
- package/src/preview/types.ts +79 -0
- package/src/scaffold/consts.ts +26 -0
- package/src/scaffold/helpers.ts +84 -0
- package/src/scaffold/index.ts +4 -0
- package/src/scaffold/schemas.ts +283 -0
- package/src/scaffold/types.ts +118 -0
- package/src/skills/catalogue.ts +1617 -0
- package/src/skills/consts.ts +183 -0
- package/src/skills/index.ts +4 -0
- package/src/skills/roles.ts +299 -0
- package/src/slot/consts.ts +393 -0
- package/src/slot/helpers.ts +34 -0
- package/src/slot/index.ts +5 -0
- package/src/slot/layout.ts +141 -0
- package/src/slot/schemas.ts +46 -0
- package/src/slot/types.ts +164 -0
- package/src/topology/card.ts +106 -0
- package/src/topology/consts.ts +59 -0
- package/src/topology/helpers.ts +138 -0
- package/src/topology/index.ts +5 -0
- package/src/topology/types.ts +86 -0
- package/src/types.ts +104 -0
- package/src/ux/consts.ts +5 -0
- package/src/ux/index.ts +4 -0
- package/src/ux/schemas.ts +97 -0
- package/src/ux/types.ts +25 -0
- package/tests/connect-convert.spec.ts +161 -0
- package/tests/convert.spec.ts +318 -0
- package/tests/design.spec.ts +136 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
import { SKILL_ORDER, ViableSkill } from './consts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Every skill body is written for a WEAK model, which means three things throughout:
|
|
4
|
+
* state the rule as an imperative, show the wrong form next to the right one, and name
|
|
5
|
+
* the exact error the wrong form produces. A model that has seen the error text
|
|
6
|
+
* recognises its own output; one that has only seen an abstract rule does not.
|
|
7
|
+
*
|
|
8
|
+
* Bodies are pure constants — they land in the cached region of the system prompt, and
|
|
9
|
+
* one interpolated byte would invalidate the prefix for every call that shares them.
|
|
10
|
+
*/
|
|
11
|
+
const skill = (alias, title, body) => ({
|
|
12
|
+
alias, title, body: body.trim(), order: SKILL_ORDER[alias],
|
|
13
|
+
});
|
|
14
|
+
/** The one rendering of a library allow-list — an override must match it byte for byte. */
|
|
15
|
+
export const renderLibraries = (list) => `Use these libraries; do not introduce others:\n${list.map(l => `- ${l}`).join('\n')}`;
|
|
16
|
+
const libraries = (alias, title, list) => skill(alias, title, renderLibraries(list));
|
|
17
|
+
/**
|
|
18
|
+
* What a file in the shared package imports. Deliberately just `ajv`: this list is carried by
|
|
19
|
+
* `DomainTypesArchitect`/`AccessArchitect` too, and a domain model type file imports nothing
|
|
20
|
+
* else. `@owlmeans/entrypoint` and `@owlmeans/route` are named by `OwlMeansEntrypoints`
|
|
21
|
+
* instead — the personas that actually author `entrypoints.ts` carry that skill, and naming a
|
|
22
|
+
* package pulls its whole published documentation into the prompt.
|
|
23
|
+
*/
|
|
24
|
+
export const commonLibraryList = ['ajv'];
|
|
25
|
+
export const uiStateLibraryList = [
|
|
26
|
+
'react', '@owlmeans/client', '@owlmeans/state', '@owlmeans/web-client',
|
|
27
|
+
];
|
|
28
|
+
export const uiComponentLibraryList = [
|
|
29
|
+
'react', 'tailwindcss', 'shadcn', '@owlmeans/client', '@owlmeans/web-client', 'lucide-react',
|
|
30
|
+
// `toast` comes from here, and this list is the last thing a UI persona reads: a package
|
|
31
|
+
// missing from it reads as a package that is not installed.
|
|
32
|
+
'sonner',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Deliberately WITHOUT `drizzle-orm`. This list renders last of all the skills (order 90), so a
|
|
36
|
+
* package named here is the final instruction a backend persona reads — and it used to say
|
|
37
|
+
* "use drizzle" directly under `ResourceLayer` saying "never import it". `postgres-resource`
|
|
38
|
+
* owns drizzle internally (its own dependency; the peers are `pg` and `ajv`), and a target
|
|
39
|
+
* reaches the builder through the resource, never through an import. Do not re-add it.
|
|
40
|
+
*/
|
|
41
|
+
export const backendLibraryList = [
|
|
42
|
+
'@owlmeans/server-app', '@owlmeans/postgres-resource', 'ajv',
|
|
43
|
+
];
|
|
44
|
+
export const fullUiLibraryList = [...new Set([...uiStateLibraryList, ...uiComponentLibraryList])];
|
|
45
|
+
/**
|
|
46
|
+
* The backend list plus the two packages that make a model call possible.
|
|
47
|
+
*
|
|
48
|
+
* Naming them is what pulls their own published documentation into the prompt, so this list
|
|
49
|
+
* carries the API contract without this catalogue restating a line of it. It is reachable only
|
|
50
|
+
* through a blueprint case that also installs them — a coder told about a package the manifest
|
|
51
|
+
* does not declare writes `TS2307`.
|
|
52
|
+
*/
|
|
53
|
+
export const aiLibraryList = [
|
|
54
|
+
...backendLibraryList, '@owlmeans/llm', '@owlmeans/agent',
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* The UI list plus three.js. Scene code and interface code are the same package and the same
|
|
58
|
+
* bundle here; what separates them is the component, not the dependency list.
|
|
59
|
+
*/
|
|
60
|
+
export const gameLibraryList = [...new Set([...fullUiLibraryList, 'three'])];
|
|
61
|
+
/**
|
|
62
|
+
* How an entrypoint alias is spelled. Shared verbatim by the entrypoints skill and by every
|
|
63
|
+
* helper prompt that asks a model to invent one — two renderings would drift, and the pipeline
|
|
64
|
+
* splits screen from API access rules on the prefix alone.
|
|
65
|
+
*/
|
|
66
|
+
export const ALIAS_CONVENTION = `
|
|
67
|
+
- A screen alias is \`web:<screen-definition>\` — \`web:task-list\`, \`web:task-details\`.
|
|
68
|
+
- An AREA alias is \`web:area:<area>\`, and there are exactly four, all already declared:
|
|
69
|
+
\`web:area:guest\`, \`web:area:user\`, \`web:area:admin\`, \`web:area:operator\`, referenced as
|
|
70
|
+
\`app.web.area.<area>\`. Never invent a fifth, never respell one. There is no
|
|
71
|
+
\`web:layout:\` alias and no \`app.web.layout\` — writing either addresses nothing.
|
|
72
|
+
- An endpoint alias is \`api:<entity>:<action>\` — \`api:task:list\`, \`api:task:create\`. The
|
|
73
|
+
group alias that carries the shared path and guard is \`api:<entity>\` — \`api:task\`.
|
|
74
|
+
- A queued job alias is \`job:<entity>:<action>\` — \`job:report:build\`, \`job:contract:analyze\`.
|
|
75
|
+
A job is never given a \`web:\` or \`api:\` alias, and an endpoint is never given a \`job:\` one:
|
|
76
|
+
the prefix is what says which side serves it.
|
|
77
|
+
- Lowercase kebab-case in every segment. The prefix is NOT optional and NOT decorative: access
|
|
78
|
+
rules are applied per side by it.
|
|
79
|
+
`.trim();
|
|
80
|
+
export const VIABLE_SKILLS = [
|
|
81
|
+
skill(ViableSkill.ProjectLayout, 'Where files live', `
|
|
82
|
+
The generated project is a \`@owlmeans/create-app\` monorepo with five workspace packages under
|
|
83
|
+
\`sources/\`. Always prefix a relative path with the package it belongs to:
|
|
84
|
+
|
|
85
|
+
- \`sources/common\` — types, entrypoint declarations and the alias tree, shared by every side.
|
|
86
|
+
- \`sources/backend\` — the shared backend LIBRARY: the context factory, the config, the
|
|
87
|
+
resources, the services and the domain models. It builds with \`tsc -b\` and serves nothing.
|
|
88
|
+
- \`sources/api\` — the HTTP server. It builds its context from \`sources/backend\` and holds the
|
|
89
|
+
endpoint handlers.
|
|
90
|
+
- \`sources/web\` — the react application.
|
|
91
|
+
- \`sources/worker\` — the queue consumer. Same context, no HTTP routes, one processor per job.
|
|
92
|
+
|
|
93
|
+
These files are the CONTRACT between the packages. Every screen, endpoint and job passes through
|
|
94
|
+
them, and each has a sentinel comment marking where a new line goes:
|
|
95
|
+
|
|
96
|
+
- \`sources/common/src/consts.ts\` — the \`app\` alias tree.
|
|
97
|
+
- \`sources/common/src/entrypoints.ts\` — the shared \`entrypoints\` declaration list.
|
|
98
|
+
- \`sources/api/src/entrypoints.ts\` — server elevations (\`appEntrypoints\`).
|
|
99
|
+
- \`sources/web/src/entrypoints.ts\` — client elevations (\`appEntrypoints\`).
|
|
100
|
+
- \`sources/worker/src/entrypoints.ts\` — job elevations.
|
|
101
|
+
- \`sources/web/src/nav.ts\` — the navigation registry: one line per screen, which
|
|
102
|
+
is the only thing that puts it in the menus.
|
|
103
|
+
|
|
104
|
+
A file's KIND is its directory, never its name: \`models/task/task.ts\` and \`state/task.ts\` are
|
|
105
|
+
both plain \`.ts\` files and differ only in where they sit. The one exception is a view model,
|
|
106
|
+
\`<component>.vm.ts\`, because a view and its view model share a directory and a base name.
|
|
107
|
+
|
|
108
|
+
The web chrome is the FOUR area layouts in \`sources/web/src/layout/area.tsx\` —
|
|
109
|
+
the project's only layouts, already written, never added to. Screens are components under
|
|
110
|
+
\`sources/web/src/screens/\`.
|
|
111
|
+
|
|
112
|
+
An entity owns exactly ONE directory segment, shared by its shared types
|
|
113
|
+
(\`common/src/models/<entity>/\`), its resources (\`backend/src/resources/<entity>/\`), its
|
|
114
|
+
backend models (\`backend/src/models/<entity>/\`) and its endpoint handlers
|
|
115
|
+
(\`api/src/app/<entity>/\`). Spell it in lowercase kebab-case and spell it
|
|
116
|
+
the same way everywhere — the resource alias, and therefore the physical table name, is derived
|
|
117
|
+
from that directory. When the task carries a \`NAME REGISTRY\` block it is the authority on
|
|
118
|
+
every such name; use its paths verbatim rather than inventing a variant.
|
|
119
|
+
`),
|
|
120
|
+
skill(ViableSkill.AgentTooling, 'Working with the project', `
|
|
121
|
+
- Start by LISTING the files you need. Never assume the project structure — ask for it.
|
|
122
|
+
- Reading a source file and getting empty content means the file does not exist; the path
|
|
123
|
+
is almost certainly wrong. Do not create it blindly, look for the real one.
|
|
124
|
+
- Writing a file is a separate step. Generating code does not persist it — call the write
|
|
125
|
+
tool explicitly for every file you created or changed.
|
|
126
|
+
- Resolve the working directory before globbing: a source-list pattern has to be prefixed
|
|
127
|
+
with the project root.
|
|
128
|
+
- A clean validation is not a working application. \`validate\` type-checks and
|
|
129
|
+
\`validate_with_renderer\` builds — neither one loads the page, and the faults that matter most
|
|
130
|
+
here are the ones that only happen while a module is being evaluated. Never report a change as
|
|
131
|
+
verified because a build came back clean.
|
|
132
|
+
`),
|
|
133
|
+
skill(ViableSkill.OutputSourceOnly, 'Output format — source code only', `
|
|
134
|
+
Return working TypeScript source and nothing else. No prose before or after it, no
|
|
135
|
+
explanation, no markdown code fences.
|
|
136
|
+
`),
|
|
137
|
+
skill(ViableSkill.OutputTextOnly, 'Output format — text only', `
|
|
138
|
+
Return only the requested text. No commentary, no preamble, no explanation of what you
|
|
139
|
+
did, no markdown code fences.
|
|
140
|
+
`),
|
|
141
|
+
skill(ViableSkill.MainFlowFocus, 'Main flow focus', `
|
|
142
|
+
An application delivers value through ONE straight path, described at its natural length — long
|
|
143
|
+
enough to cover every step the flow really has, and no longer. That path is the main flow,
|
|
144
|
+
and it is the only thing you describe.
|
|
145
|
+
|
|
146
|
+
- The path has one BENEFICIARY — the kind of user who ends up holding the value — and however
|
|
147
|
+
many PARTICIPANTS the delivery cannot happen without. A social network needs a creator AND a
|
|
148
|
+
reader; a marketplace needs a seller AND a buyer; most applications need nobody besides the
|
|
149
|
+
beneficiary. Name each as a kind of user ("operator", "author", "reader"), never a market
|
|
150
|
+
segment or an ICP label, and never add a role the value can be delivered without.
|
|
151
|
+
- The main flow is 3 to 12 ordered steps, each performed by one named kind of user, running from
|
|
152
|
+
the enabling first action to the moment the beneficiary holds the value. The FIRST step is what
|
|
153
|
+
someone declares, creates or brings in before the flow can run at all.
|
|
154
|
+
- WRONG first step: "the user sees a dashboard of their results". RIGHT first step: "the user
|
|
155
|
+
declares the process their leads will pass through". A screen that shows, lists or reports on
|
|
156
|
+
something the flow has not produced yet is never the beginning of the flow.
|
|
157
|
+
- Cover the WHOLE path, not its opening. Every participant acts in at least one step, at most one
|
|
158
|
+
step is about setting things up, and the last step delivers the value. A description that stops
|
|
159
|
+
after the setup — a social network where someone publishes but nobody ever reads — has
|
|
160
|
+
described no value at all, however detailed its first steps are.
|
|
161
|
+
- Never describe branches, alternatives, optional steps, error paths, or anything about signing
|
|
162
|
+
up, signing in, roles, permissions, settings, configuration, onboarding, notifications, billing
|
|
163
|
+
or administration. They are not the flow. This bans them as STEPS — it does not ban thinking
|
|
164
|
+
about them: whether an actor needs an account is always judged, because it is what decides the
|
|
165
|
+
area their story belongs to.
|
|
166
|
+
- If a step can be dropped and the beneficiary still gets the value, drop it, and spend it on the
|
|
167
|
+
part of the path that is still missing. Breadth is one failure mode; a truncated path is the
|
|
168
|
+
other, and it is the more common one — a path that never shows the public half of the product,
|
|
169
|
+
or stops before anyone consumes what was produced, is truncated however tidy it looks.
|
|
170
|
+
|
|
171
|
+
Both bans above are about the FLOW ITSELF — about what may be one of its numbered steps. They are
|
|
172
|
+
not a ban on the screens that CONNECT the steps to each other: the list of records waiting for
|
|
173
|
+
someone, the directory that finds one of them, the page of a single record, the list of what an
|
|
174
|
+
actor submitted. Those are never steps, and you never volunteer one while you are describing the
|
|
175
|
+
flow. When a task asks for them IN SO MANY WORDS — handing you the flow steps as finished context
|
|
176
|
+
and asking what has to exist BETWEEN them — that task is not asking about the flow, and answering
|
|
177
|
+
it is not breadth. Describe them only then.
|
|
178
|
+
|
|
179
|
+
The application serves exactly FOUR audiences, called AREAS, and every actor in a story belongs
|
|
180
|
+
to one of them:
|
|
181
|
+
|
|
182
|
+
- \`guest\` — the actor is NOT signed in: public visitors, prospects, anonymous readers.
|
|
183
|
+
- \`user\` — a signed-in end user consuming the product's value; the front office.
|
|
184
|
+
- \`operator\` — staff running the product's business process from the inside; the back office.
|
|
185
|
+
- \`admin\` — the owner configuring or managing the application itself.
|
|
186
|
+
|
|
187
|
+
Decide an actor's area by asking, IN ORDER, and stopping at the first yes: can they do this step
|
|
188
|
+
with NO account (\`guest\`) — does the step manage the application itself (\`admin\`) — do they act
|
|
189
|
+
on the business process from the inside (\`operator\`) — otherwise \`user\`.
|
|
190
|
+
|
|
191
|
+
The authentication question comes first and it outranks who benefits: an actor who needs no
|
|
192
|
+
account is \`guest\` even when the product exists for them. A public product therefore normally has
|
|
193
|
+
both \`guest\` and \`user\` steps, and a flow whose stories all landed in one area is usually a
|
|
194
|
+
misread of that first question.
|
|
195
|
+
|
|
196
|
+
A product's own role names — a seller, an author, a reviewer, a reader — are not areas. The same
|
|
197
|
+
name can sit in different areas in different steps, so judge the STEP, never the title. An area is
|
|
198
|
+
never a step of its own: classifying an actor is not licence to add one, so an \`admin\` or a
|
|
199
|
+
moderator still appears only when the flow genuinely needs their action.
|
|
200
|
+
`),
|
|
201
|
+
skill(ViableSkill.ScopeDiscipline, 'Scope', `
|
|
202
|
+
Implement only what the task describes. Do not add features, files, abstractions, or
|
|
203
|
+
error handling for situations the task does not mention.
|
|
204
|
+
`),
|
|
205
|
+
skill(ViableSkill.NonTypescriptOutput, 'This file is not TypeScript', `
|
|
206
|
+
The file you are asked for is NOT TypeScript — it is CSS, JSON, Markdown or another
|
|
207
|
+
format. The TypeScript style and import rules above do not apply to it. Produce the file
|
|
208
|
+
in its own language, with no TypeScript syntax and no import statements it does not need.
|
|
209
|
+
`),
|
|
210
|
+
skill(ViableSkill.TsStyle, 'TypeScript style', `
|
|
211
|
+
- No semicolons at the end of statements.
|
|
212
|
+
- Named exports only — never \`export default\`.
|
|
213
|
+
- Declare every function before it is used.
|
|
214
|
+
- Add an explicit props type to every React component.
|
|
215
|
+
- When a value is a free-form dictionary (for example collected Zod errors), type it
|
|
216
|
+
explicitly as \`Record<string, any>\`.
|
|
217
|
+
- Never import a runtime value with \`import type\` or \`import { type X }\`. Those are for
|
|
218
|
+
types and interfaces ONLY. Importing a value that way compiles and then fails at
|
|
219
|
+
runtime with "X is not defined" or "does not provide an export named X", because the
|
|
220
|
+
import is erased.
|
|
221
|
+
- Keep comments short and only for what the code cannot show. Never restate parameter or
|
|
222
|
+
return types in a comment — TypeScript already carries them.
|
|
223
|
+
- View-model files end in \`.vm.ts\`, not \`.vm.tsx\`: they contain no JSX.
|
|
224
|
+
`),
|
|
225
|
+
skill(ViableSkill.TsImports, 'Imports and path aliases', `
|
|
226
|
+
- Import shared code as \`project-common/<path>\`, never as \`sources/common/src/<path>\`,
|
|
227
|
+
and never with a \`.js\` extension.
|
|
228
|
+
- Inside a BUNDLED package — \`api\`, \`web\`, \`worker\` — \`@/\` is an alias for that package's own
|
|
229
|
+
\`src\`; use it instead of chains of \`../\`.
|
|
230
|
+
- **The shared backend library has NO \`@/\` alias.** It is built by \`tsc -b\`, not bundled, and
|
|
231
|
+
declares no tsconfig \`paths\`. Inside \`sources/backend/\` every internal import is RELATIVE and
|
|
232
|
+
keeps its \`.js\` suffix:
|
|
233
|
+
\`import { taskResource } from '../../resources/task/task.js'\`.
|
|
234
|
+
\`@/resources/task/task.js\` there is \`TS2307: Cannot find module\`, and changing the suffix does
|
|
235
|
+
not help — the alias is what is missing, not the extension.
|
|
236
|
+
- \`@/\` is the ONLY alias that exists. If you find yourself writing any other \`@\`-prefixed
|
|
237
|
+
import that is not a real npm package, you are inventing a module that is not there;
|
|
238
|
+
use a relative path or an existing package instead.
|
|
239
|
+
`),
|
|
240
|
+
skill(ViableSkill.ReactComponents, 'React components', `
|
|
241
|
+
- Icons come from \`lucide-react\`. If a named icon does not exist in that package, do not
|
|
242
|
+
guess another name — inline a small SVG instead.
|
|
243
|
+
- Every component declares a props type.
|
|
244
|
+
- Do not fetch data directly in a component. A component reads its data and its handlers
|
|
245
|
+
from its view-model hook.
|
|
246
|
+
- Never use a dynamic import for a component or an icon — in particular never
|
|
247
|
+
\`lucide-react/dynamicIconImports\`. Import what you need statically by name.
|
|
248
|
+
`),
|
|
249
|
+
skill(ViableSkill.OwlMeansEntrypoints, 'Entrypoints — screens and endpoints', `
|
|
250
|
+
Everything this app addresses — a screen the browser renders, an endpoint the server answers
|
|
251
|
+
— is an OwlMeans **entrypoint**: declared with \`@owlmeans/entrypoint\` and \`@owlmeans/route\`
|
|
252
|
+
in the shared package, elevated with \`@owlmeans/web-client\` in the browser and
|
|
253
|
+
\`@owlmeans/server-app\` on the backend. \`@owlmeans/web-client\` builds the route table from the
|
|
254
|
+
entrypoints and renders it over the History API, so there is no router file, no \`<Routes>\` and
|
|
255
|
+
no \`app.get(...)\`. No third-party routing or HTTP-server package is installed — importing one
|
|
256
|
+
fails to resolve.
|
|
257
|
+
|
|
258
|
+
An entrypoint is DECLARED once in the shared package, then ELEVATED on each side that uses it.
|
|
259
|
+
|
|
260
|
+
**1. The alias.** Aliases live in ONE place, the \`app\` tree in
|
|
261
|
+
\`sources/common/src/consts.ts\`. NEVER write an alias string inline — always reference
|
|
262
|
+
\`app.web.*\` / \`app.api.*\`. An inline string compiles and then silently addresses nothing.
|
|
263
|
+
|
|
264
|
+
${ALIAS_CONVENTION}
|
|
265
|
+
|
|
266
|
+
**2. The declaration** — \`sources/common/src/entrypoints.ts\`, one line in the \`entrypoints\`
|
|
267
|
+
array:
|
|
268
|
+
|
|
269
|
+
import { entrypoint, guard, gate, filter, body } from '@owlmeans/entrypoint'
|
|
270
|
+
import { route, frontend, RouteMethod } from '@owlmeans/route'
|
|
271
|
+
|
|
272
|
+
entrypoint(route(app.api.task.list, '/list', { parent: app.api.task }), guard(DEFAULT_GUARD))
|
|
273
|
+
|
|
274
|
+
- \`route(alias, path)\` is a BACKEND route by default. \`route(alias, path, frontend())\` makes
|
|
275
|
+
it a screen; \`frontend({ default: true, parent: app.web.base })\` makes it the index screen.
|
|
276
|
+
- Nest with \`{ parent: <group alias> }\` — the child path is APPENDED to the parent's, so the
|
|
277
|
+
child path is the tail only (\`'/list'\`, not \`'/tasks/list'\`).
|
|
278
|
+
- \`{ method: RouteMethod.POST }\` (from \`@owlmeans/route\`) for anything that is not a GET.
|
|
279
|
+
- \`filter(body<T>(TSchema))\` whenever the body's type has a schema beside it in the shared
|
|
280
|
+
package — every generated type exports one, named after the type with a \`Schema\` suffix
|
|
281
|
+
(\`Task\` → \`TaskSchema\`). Import both from the shared package and use them; that is what makes
|
|
282
|
+
the framework reject a malformed request before your handler runs, so the handler never has to
|
|
283
|
+
check whether a field arrived. Still never INVENT a schema inline: if the type has none, leave
|
|
284
|
+
the endpoint unfiltered rather than writing a literal here that nothing else agrees with.
|
|
285
|
+
- Access is declarative: no \`guard()\` = public, \`guard(DEFAULT_GUARD)\` = any signed-in user,
|
|
286
|
+
\`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['<permission>']))\` = a permission is required.
|
|
287
|
+
\`<permission>\` is a SHAPE, never a value: build the real name from the domain being
|
|
288
|
+
implemented — the resource, TWO hyphens, the action, lowercase kebab-case, singular resource.
|
|
289
|
+
Do not write a bracketed word into an application, and do not copy a name out of this
|
|
290
|
+
document; both produce a gate nobody can pass.
|
|
291
|
+
Add \`@<routeParam>\` to scope the check to one record — the name after \`@\` MUST be a \`:\`
|
|
292
|
+
segment of that same entrypoint's own path, since one the path does not carry resolves to
|
|
293
|
+
nothing and the gate then refuses every request. Guards and gates are INHERITED by children and
|
|
294
|
+
enforced by the framework — a handler or a screen never re-checks them.
|
|
295
|
+
|
|
296
|
+
**3. The elevation.** The declaration alone renders and answers nothing.
|
|
297
|
+
|
|
298
|
+
- Server, \`sources/api/src/entrypoints.ts\`:
|
|
299
|
+
\`elevate(appEntrypoints, app.api.task.list, handleRequest(...))\`
|
|
300
|
+
- Client screen, \`sources/web/src/entrypoints.ts\`:
|
|
301
|
+
\`elevate(appEntrypoints, app.web.taskList, handler(TaskListScreen))\`
|
|
302
|
+
- Client CALL, same file: a BARE elevation, no component —
|
|
303
|
+
\`elevate(appEntrypoints, app.api.task.list)\`. This is what makes the alias callable from the
|
|
304
|
+
browser. Passing a component to a backend alias is a hard error, not a warning.
|
|
305
|
+
|
|
306
|
+
**A missing client elevation is invisible until the app runs.** The whole shared list is
|
|
307
|
+
registered on the browser context, so \`ctx.entrypoint(alias)\` finds the un-elevated declaration
|
|
308
|
+
and returns it; the call site casts it to \`ClientEntrypoint\`, so TypeScript sees a type that has
|
|
309
|
+
\`call\` and the build is clean. The only symptom is in the browser:
|
|
310
|
+
|
|
311
|
+
TypeError: entrypoint.call is not a function
|
|
312
|
+
|
|
313
|
+
That message means EXACTLY one thing — the alias in that \`ctx.entrypoint(...)\` has no
|
|
314
|
+
\`elevate(list, <alias>)\` line in \`sources/web/src/entrypoints.ts\`. Add the bare
|
|
315
|
+
elevation above the \`// owlmeans: add new backend elevations above this line\` sentinel. It is
|
|
316
|
+
never a broken component, never a bad import, and never a reason to rewrite the view model or to
|
|
317
|
+
replace the call with \`fetch\`. If the alias is not declared in
|
|
318
|
+
\`sources/common/src/entrypoints.ts\` either, declare it there FIRST: elevating an alias the
|
|
319
|
+
shared list does not carry throws \`Entrypoint with alias X not present\` while the module is
|
|
320
|
+
still loading, which blanks the whole app instead of failing one call.
|
|
321
|
+
|
|
322
|
+
**4. An AREA is the PARENT entrypoint.** The chrome of the app — header, navigation, footer —
|
|
323
|
+
is never imported by a screen. It comes from an entrypoint of its own, and the screens that use
|
|
324
|
+
it are declared as its CHILDREN. There are exactly FOUR areas, they already exist in the
|
|
325
|
+
project, and an area contributes chrome AND a URL prefix:
|
|
326
|
+
|
|
327
|
+
- \`app.web.area.guest\` at \`/\` — visitors who are not signed in.
|
|
328
|
+
- \`app.web.area.user\` at \`/frontoffice\` — signed-in end users; the front office.
|
|
329
|
+
- \`app.web.area.admin\` at \`/admin\` — the owner of the application.
|
|
330
|
+
- \`app.web.area.operator\` at \`/backoffice\` — staff running the business process; the back office.
|
|
331
|
+
|
|
332
|
+
Those four declarations and their elevations are FIXED. NEVER add an area, NEVER change one's
|
|
333
|
+
path, NEVER touch its \`guard()\`/\`gate()\`, NEVER re-declare one — put the screen in the area
|
|
334
|
+
whose access and audience it needs instead. A product's own roles are not areas: they are users
|
|
335
|
+
or operators holding different permissions.
|
|
336
|
+
|
|
337
|
+
// sources/common/src/entrypoints.ts — a screen: TAIL path, area as parent, no access
|
|
338
|
+
entrypoint(route(app.web.taskList, '/tasks', frontend({ parent: app.web.area.user }))),
|
|
339
|
+
|
|
340
|
+
// sources/web/src/entrypoints.ts
|
|
341
|
+
elevate(list, app.web.taskList, handler(TaskListScreen))
|
|
342
|
+
|
|
343
|
+
That screen answers at \`/frontoffice/tasks\`. The path you write is the TAIL ONLY — repeating the
|
|
344
|
+
area prefix (\`'/frontoffice/tasks'\`) publishes it at \`/frontoffice/frontoffice/tasks\`, which
|
|
345
|
+
nothing in the menu links to and no navigation call reaches.
|
|
346
|
+
|
|
347
|
+
The framework passes the matched child to the area as \`children\`, so an area layout is
|
|
348
|
+
\`FC<PropsWithChildren>\` and renders \`{children}\` in its content region. A screen therefore
|
|
349
|
+
renders ONLY its own content: importing a layout inside a screen renders the header, the menu
|
|
350
|
+
and the footer a second time, nested inside the first.
|
|
351
|
+
|
|
352
|
+
- \`app.web.base\` stays BARE — \`elevate(list, app.web.base)\`, no \`handler()\`. It is the
|
|
353
|
+
pass-through shell that hosts the four areas and contributes no chrome.
|
|
354
|
+
- Every area needs a child declared \`frontend({ default: true, parent: app.web.area.<area> })\`.
|
|
355
|
+
An entrypoint with children but no default child matches nothing and renders a BLANK PAGE.
|
|
356
|
+
Each area already ships one; never remove it and never add a second.
|
|
357
|
+
|
|
358
|
+
**Access is INHERITED from the area.** Guards and gates cascade to children, so choosing the area
|
|
359
|
+
IS the access decision and a screen declares none of its own:
|
|
360
|
+
|
|
361
|
+
- guest area — no guard at all; its screens add nothing.
|
|
362
|
+
- user area — \`guard(DEFAULT_GUARD)\`; its screens add nothing.
|
|
363
|
+
- admin area — \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['project--admin']))\`; its screens add
|
|
364
|
+
nothing, the marker is the whole rule.
|
|
365
|
+
- operator area — \`guard(DEFAULT_GUARD)\`, and EVERY screen under it additionally declares
|
|
366
|
+
\`gate(OIDC_GATE, ['<permission>'])\` naming the permission THAT screen needs, built from the
|
|
367
|
+
screen's own resource and action (add \`@<routeParam>\` when the screen shows one specific
|
|
368
|
+
record). This is the ONLY screen that declares access.
|
|
369
|
+
|
|
370
|
+
An ENDPOINT has no area to inherit from, so it states its level itself: no \`guard()\` for guest,
|
|
371
|
+
\`guard(DEFAULT_GUARD)\` for any signed-in user,
|
|
372
|
+
\`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['<permission>']))\` when a permission is required — built
|
|
373
|
+
from ITS OWN resource and action — and
|
|
374
|
+
\`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['project--admin']))\` for owner-only.
|
|
375
|
+
|
|
376
|
+
\`project--admin\` is the OWNER's marker — holding it passes EVERY gate, so the owner is not a
|
|
377
|
+
role inside the app. It is never one of the permissions this app declares for itself, and it is
|
|
378
|
+
never granted to an ordinary user. Never hand-write an admin bypass in a handler, never weaken an
|
|
379
|
+
existing guard to make something reachable, and never model a "limited admin" with this marker:
|
|
380
|
+
that is an ordinary user holding some of the app's own permissions.
|
|
381
|
+
|
|
382
|
+
Insert every new line ABOVE the matching \`// owlmeans: add new ... above this line\` sentinel,
|
|
383
|
+
one declaration or elevation per line. Adding an endpoint touches five files (alias,
|
|
384
|
+
declaration, handler, server elevation, client elevation); a screen touches five too (alias,
|
|
385
|
+
declaration, screen component, client elevation, navigation entry). Skipping one leaves it
|
|
386
|
+
unreachable.
|
|
387
|
+
|
|
388
|
+
**Calling an endpoint** from the frontend — \`call()\` resolves to the VALUE, and a non-2xx THROWS:
|
|
389
|
+
|
|
390
|
+
const tasks = await owlCtx.entrypoint<ClientEntrypoint<Task[]>>(app.api.task.list).call()
|
|
391
|
+
const task = await owlCtx.entrypoint<ClientEntrypoint<Task>>(app.api.task.create)
|
|
392
|
+
.call({ body: input })
|
|
393
|
+
const task = await owlCtx.entrypoint<ClientEntrypoint<Task>>(app.api.task.get)
|
|
394
|
+
.call({ params: { taskId } })
|
|
395
|
+
|
|
396
|
+
An entrypoint carries three verbs and each answers a different question:
|
|
397
|
+
|
|
398
|
+
- \`call(req?)\` — performs the call and resolves to the VALUE. This is the one nearly every
|
|
399
|
+
view model wants.
|
|
400
|
+
- \`invoke(req?)\` — the same round trip, resolving to \`{ value, outcome }\`, for the rare place
|
|
401
|
+
where the OUTCOME decides what happens next.
|
|
402
|
+
- \`url(req?, opts?)\` — builds the URL this entrypoint addresses; \`url(req, { absolute: true })\`
|
|
403
|
+
for a fully qualified one. A SCREEN entrypoint answers only this verb — calling \`call()\` or
|
|
404
|
+
\`invoke()\` on one THROWS.
|
|
405
|
+
|
|
406
|
+
\`ClientEntrypoint\` comes from \`@owlmeans/web-client\` and from nowhere else:
|
|
407
|
+
|
|
408
|
+
import type { ClientEntrypoint } from '@owlmeans/web-client'
|
|
409
|
+
|
|
410
|
+
\`@owlmeans/client\` does NOT export it — importing it from there fails the build with
|
|
411
|
+
\`error TS2305: Module '"@owlmeans/client"' has no exported member 'ClientEntrypoint'\`.
|
|
412
|
+
And the type argument goes on \`ClientEntrypoint\`, never on \`entrypoint\` itself:
|
|
413
|
+
\`owlCtx.entrypoint<Task>(alias)\` fails with \`error TS2739: Type 'Task' is missing the
|
|
414
|
+
following properties from type 'BasicEntrypoint'\`.
|
|
415
|
+
|
|
416
|
+
Inside a component or a hook — which is where nearly every call belongs — read the context with
|
|
417
|
+
\`useContext()\` from \`@owlmeans/web-client\`. \`owlCtx\` is for module-level code that has no
|
|
418
|
+
hook to run in: it is a MODULE EXPORT — \`import { owlCtx } from '@/owlmeans'\` — never a global;
|
|
419
|
+
\`window.owlCtx\` and \`(window as any).__owlCtx\` do not exist. NEVER set an
|
|
420
|
+
\`Authorization\` header and never read a token — the framework attaches it to every guarded
|
|
421
|
+
call. There is no \`fetchApi\` helper and no \`fetch\` call to the backend.
|
|
422
|
+
`),
|
|
423
|
+
skill(ViableSkill.OwlMeansNav, 'Navigating between screens', `
|
|
424
|
+
Navigation addresses an ALIAS, never a URL. The path an alias resolves to is owned by
|
|
425
|
+
\`sources/common/src/entrypoints.ts\`; a hard-coded \`/tasks/\${id}\` desynchronizes silently the
|
|
426
|
+
moment the declaration changes.
|
|
427
|
+
|
|
428
|
+
import { useNavigate } from '@owlmeans/client'
|
|
429
|
+
|
|
430
|
+
const nav = useNavigate()
|
|
431
|
+
nav.go(app.web.taskDetails, { params: { taskId } }) // navigate now
|
|
432
|
+
<button onClick={nav.press(app.web.taskList)}> // press() RETURNS the handler
|
|
433
|
+
nav.back()
|
|
434
|
+
|
|
435
|
+
- \`useNavigate\` comes from \`@owlmeans/client\`. It is NOT exported by
|
|
436
|
+
\`@owlmeans/web-client\` — importing it from there fails to resolve.
|
|
437
|
+
- \`nav.press(alias)\` returns a click handler. Write \`onClick={nav.press(alias)}\`, never
|
|
438
|
+
\`onClick={() => nav.press(alias)}\` — the second form navigates nowhere because it only
|
|
439
|
+
builds a handler and drops it.
|
|
440
|
+
- There is no \`<Link>\` and no \`<Navigate>\` component. Use an \`<a>\` (or a button) with
|
|
441
|
+
\`onClick\`.
|
|
442
|
+
|
|
443
|
+
Route params are PROPS, not a hook. The framework renders a screen with
|
|
444
|
+
\`{ context, params, alias, path }\`, so a screen that needs \`:taskId\` declares it:
|
|
445
|
+
|
|
446
|
+
interface TaskDetailsScreenProps { params: { taskId: string } }
|
|
447
|
+
|
|
448
|
+
export const TaskDetailsScreen: FC<TaskDetailsScreenProps> = ({ params }) => { ... }
|
|
449
|
+
|
|
450
|
+
Never import \`useParams\` — no package the app depends on exports it, and the import fails to
|
|
451
|
+
resolve.
|
|
452
|
+
|
|
453
|
+
**A screen reaches the MENUS only through the navigation registry** — one line per screen in
|
|
454
|
+
\`sources/web/src/nav.ts\`, above the
|
|
455
|
+
\`// owlmeans: add new navigation entries above this line\` sentinel:
|
|
456
|
+
|
|
457
|
+
{ area: 'user', section: 'Tasks', alias: app.web.taskList, label: 'All tasks' },
|
|
458
|
+
|
|
459
|
+
- \`area\` is \`'guest' | 'user' | 'admin' | 'operator'\` and must be the SAME area the screen is
|
|
460
|
+
declared under in \`sources/common/src/entrypoints.ts\`.
|
|
461
|
+
- \`section\` groups screens into the TOP menu (the first level). The screens of the active
|
|
462
|
+
section are the SIDE menu (the second level), and a section holding a single screen shows no
|
|
463
|
+
side menu at all.
|
|
464
|
+
- A section is a LABEL, never a URL segment. Regrouping screens changes the menus and never an
|
|
465
|
+
address.
|
|
466
|
+
- NEVER hand-write a menu, a \`<nav>\`, a sidebar, a breadcrumb or a list of links in a screen or
|
|
467
|
+
a layout. The chrome comes from \`NavLayout\` (\`@owlmeans/web-panel\`), which the four area
|
|
468
|
+
layouts already render off this registry.
|
|
469
|
+
|
|
470
|
+
A screen with no line here compiles, elevates and renders — it is simply reachable by direct URL
|
|
471
|
+
only, and nothing reports it.
|
|
472
|
+
`),
|
|
473
|
+
skill(ViableSkill.PermissionModel, 'Permissions — the name, the gate and the grant', `
|
|
474
|
+
\`enquiry\` below belongs to a DIFFERENT application, used here only to show the shape. Every
|
|
475
|
+
permission you write is built from the resource and action of the app you are working on; a name
|
|
476
|
+
copied out of this document is a gate nobody can ever pass.
|
|
477
|
+
|
|
478
|
+
A permission is ONE string that has to line up in three places:
|
|
479
|
+
|
|
480
|
+
1. the GATE on the entrypoint — \`gate(OIDC_GATE, ['enquiry--view@enquiryId'])\`
|
|
481
|
+
2. the permission DEFINITION registered for the project — name \`enquiry--view\`,
|
|
482
|
+
resource \`enquiry\`, action \`view\`, resource-scoped \`true\`
|
|
483
|
+
3. the GRANT an administrator makes, which addresses that definition by name.
|
|
484
|
+
|
|
485
|
+
**The \`@\` suffix belongs to the GATE ONLY. It is never part of a permission's name.**
|
|
486
|
+
|
|
487
|
+
// right — the gate scopes the check; the definition and the grant use the bare name
|
|
488
|
+
entrypoint(
|
|
489
|
+
route(app.api.enquiry.get, '/:enquiryId', { parent: app.api.enquiry }),
|
|
490
|
+
guard(DEFAULT_GUARD, gate(OIDC_GATE, ['enquiry--view@enquiryId']))
|
|
491
|
+
)
|
|
492
|
+
// definition: enquiry--view grant: enquiry--view
|
|
493
|
+
|
|
494
|
+
// wrong — the suffix carried into the registered or granted NAME
|
|
495
|
+
// definition: enquiry--view@enquiryId
|
|
496
|
+
// grant: enquiry--view@enquiryId
|
|
497
|
+
|
|
498
|
+
The gate splits its parameter at the FIRST \`@\`: what comes before is the permission it looks up,
|
|
499
|
+
what comes after says where to read the resource id. So a definition or a grant whose name
|
|
500
|
+
contains an \`@\` is a key nothing ever looks up. The symptom is a user who HAS been granted the
|
|
501
|
+
permission and still gets 403, with nothing logged. The cause is the \`@\` in the stored name —
|
|
502
|
+
never the gate line, which is usually already correct. Repair the definition and the grant; do
|
|
503
|
+
NOT delete the \`@\` from the gate to make the two strings match.
|
|
504
|
+
|
|
505
|
+
**Repair order matters.** Normalize FIRST, then judge what is left. Normalizing renames a
|
|
506
|
+
malformed definition and carries its grants across in the same write; deleting first throws those
|
|
507
|
+
grants away, and nothing can reconstruct who held them.
|
|
508
|
+
|
|
509
|
+
**The name.** \`<resource>--<action>\`:
|
|
510
|
+
- Both halves are names from THIS application's own domain. \`<resource>\`, \`<action>\` and
|
|
511
|
+
\`<permission>\` are placeholders describing the shape — writing one of them, or any other
|
|
512
|
+
bracketed word, produces a permission no screen can grant and no user can ever hold.
|
|
513
|
+
- \`--\` is TWO hyphens. One hyphen is not a separator: \`enquiry-view\` is a resource named
|
|
514
|
+
\`enquiry-view\` with no action at all.
|
|
515
|
+
- lowercase kebab-case in both segments, singular resource.
|
|
516
|
+
- Actions are collapsed: create, update, edit and delete are all \`modify\`; reading one record is
|
|
517
|
+
\`view\`; reading many is \`list\`.
|
|
518
|
+
- \`project--admin\` is the OWNER's marker. No story ever registers it, and holding it passes every
|
|
519
|
+
gate. It is enforced by a decoration wrapped around the gate in \`sources/backend/src/context.ts\`,
|
|
520
|
+
NOT by any gate parameter — so its absence from the entrypoint declarations is not evidence that
|
|
521
|
+
it is unused, and it must never be deleted as "a definition nothing names".
|
|
522
|
+
|
|
523
|
+
**The \`@\` selector.** \`@<routeParam>\`: the name after \`@\` must be a \`:\` segment of THAT
|
|
524
|
+
entrypoint's own declared path. \`'enquiry--view@enquiryId'\` on a route declared \`'/enquiries/:id'\`
|
|
525
|
+
resolves to nothing, so the gate refuses every request and no error explains why. A permission used
|
|
526
|
+
with a selector is resource-scoped: a grant may carry the specific ids it covers, and a grant
|
|
527
|
+
carrying no ids at all covers every resource.
|
|
528
|
+
|
|
529
|
+
**Every way this goes wrong, and what each one costs.** The application is a clinic in these
|
|
530
|
+
examples; yours is not, so read the right-hand column as a shape and build the name from your own
|
|
531
|
+
domain.
|
|
532
|
+
|
|
533
|
+
// WRONG — a bracketed word from an instruction, written as if it were a name
|
|
534
|
+
gate(OIDC_GATE, ['<permission>'])
|
|
535
|
+
gate(OIDC_GATE, ['<resource>--<action>'])
|
|
536
|
+
// RIGHT — this application's own resource and action
|
|
537
|
+
gate(OIDC_GATE, ['appointment--modify'])
|
|
538
|
+
// Costs: nothing registers a bracketed name, so the gate refuses every request forever.
|
|
539
|
+
|
|
540
|
+
// WRONG — a name copied out of a skill, a comment or another app's example
|
|
541
|
+
gate(OIDC_GATE, ['article--modify']) // in an application that has no articles
|
|
542
|
+
// RIGHT — a name that exists in THIS domain
|
|
543
|
+
gate(OIDC_GATE, ['appointment--modify'])
|
|
544
|
+
// Costs: the permission is asserted but never declared, so no administrator can grant it.
|
|
545
|
+
|
|
546
|
+
// WRONG — the selector carried into the stored name
|
|
547
|
+
ensurePermission(entity, client, 'appointment', { permission: 'modify@appointmentId' })
|
|
548
|
+
// RIGHT — the selector stays in the gate; the stored name is bare
|
|
549
|
+
ensurePermission(entity, client, 'appointment', { permission: 'modify' })
|
|
550
|
+
// Costs: a grant lands on a key no gate reads — the screen says granted, the request says 403.
|
|
551
|
+
|
|
552
|
+
// WRONG — a selector naming a param this route does not declare
|
|
553
|
+
route(app.api.appointment.get, '/:appointmentId')
|
|
554
|
+
gate(OIDC_GATE, ['appointment--view@id'])
|
|
555
|
+
// RIGHT — the name after @ is a ":" segment of this very route
|
|
556
|
+
gate(OIDC_GATE, ['appointment--view@appointmentId'])
|
|
557
|
+
// Costs: the id resolves to nothing, so the endpoint refuses every request with nothing logged.
|
|
558
|
+
|
|
559
|
+
// WRONG — one hyphen
|
|
560
|
+
gate(OIDC_GATE, ['appointment-modify'])
|
|
561
|
+
// RIGHT — two
|
|
562
|
+
gate(OIDC_GATE, ['appointment--modify'])
|
|
563
|
+
// Costs: it registers as a resource with no action, and never lines up with the real one.
|
|
564
|
+
|
|
565
|
+
**Never spell one permission two ways.** Reuse the exact string an existing declaration already
|
|
566
|
+
carries. Introducing a second spelling orphans every grant made against the first.
|
|
567
|
+
`),
|
|
568
|
+
skill(ViableSkill.ShadcnUi, 'shadcn/ui and Tailwind', `
|
|
569
|
+
- The \`cn\` helper is a NAMED export of \`@/lib/utils\` — not \`@/lib/utils/cn\`, which is not a
|
|
570
|
+
file and fails to resolve.
|
|
571
|
+
- The shadcn primitives are VENDORED into this project at \`@/components/ui/*\`, and
|
|
572
|
+
\`@owlmeans/web-panel\`'s own components resolve theirs through that same path — against these
|
|
573
|
+
files, not a copy of their own. They are already there: import them, never delete, rename or
|
|
574
|
+
move one, and never re-add a primitive the directory already carries.
|
|
575
|
+
- Styling is Tailwind utility classes. There are no CSS modules and no styled-components.
|
|
576
|
+
- Colour comes in PAIRS. Every surface token in this theme has a foreground partner —
|
|
577
|
+
\`--background\`/\`--foreground\`, \`--card\`/\`--card-foreground\`, \`--primary\`/\`--primary-foreground\`,
|
|
578
|
+
and the same for \`secondary\`, \`muted\`, \`accent\`, \`popover\`, \`destructive\` and \`sidebar\`. Painting a
|
|
579
|
+
surface is therefore never a single decision: give the element its partner in the SAME
|
|
580
|
+
\`className\`, or the text keeps the colour meant for the surface underneath and vanishes wherever
|
|
581
|
+
the design made that surface dark.
|
|
582
|
+
|
|
583
|
+
<section className="bg-primary p-6">Join us</section> // WRONG
|
|
584
|
+
<section className="bg-primary text-primary-foreground p-6">Join us</section> // right
|
|
585
|
+
|
|
586
|
+
Nothing catches the wrong form — it compiles and it renders. It is unreadable only on the
|
|
587
|
+
surfaces the design happened to make dark, which is why it survives to the finished screen.
|
|
588
|
+
- Never paint a surface with a raw or palette colour — \`bg-slate-900\`, \`bg-[#101820]\`,
|
|
589
|
+
\`style={{ background: '#101820' }}\`. Those have no foreground partner, so nothing keeps the text
|
|
590
|
+
on them legible, and they ignore the theme. Every surface is one of the tokens above.
|
|
591
|
+
- Depth and emphasis come from the theme's DECORATION VOCABULARY, and using it is expected rather
|
|
592
|
+
than exceptional — a screen built only from flat cards is an unfinished screen. That vocabulary
|
|
593
|
+
is: the component classes \`gradient-heading\` (a page or section headline), \`glass-card\` (a
|
|
594
|
+
panel with depth), \`eyebrow\` (the small chip above a headline), \`cta-btn\` (the primary call to
|
|
595
|
+
action, on an \`<a>\` or a bare \`<button>\`, never on a shadcn \`Button\` whose own \`bg-\`
|
|
596
|
+
utility would win), \`glow-orb\` (a soft colour field behind a section, always
|
|
597
|
+
\`pointer-events-none\` and behind the content); the elevation utilities \`shadow-soft\`,
|
|
598
|
+
\`shadow-raised\`, \`shadow-floating\`, \`shadow-glow\`; the idle-motion classes \`reveal\`,
|
|
599
|
+
\`float-slow\`, \`drift-slow\`, \`pulse-soft\`, \`spin-slow\`; and gradients whose stops are THEME
|
|
600
|
+
tokens with opacity. None of these is a raw colour.
|
|
601
|
+
|
|
602
|
+
<div className="bg-[radial-gradient(#818cf8,transparent)] blur-[120px]" /> // WRONG
|
|
603
|
+
<span className="glow-orb h-80 w-96 -z-10" aria-hidden /> // right
|
|
604
|
+
<div className="bg-gradient-to-br from-primary/20 to-transparent p-6" /> // right
|
|
605
|
+
|
|
606
|
+
Keep a gradient under a text element light — \`/20\` or less — unless the element also carries a
|
|
607
|
+
\`-foreground\` partner class. These class names are defined in the project's stylesheet; do not
|
|
608
|
+
invent others in the same family.
|
|
609
|
+
- Text that is NOT on a coloured surface takes NO colour class: it already inherits the readable
|
|
610
|
+
one. Never add \`text-white\`, \`text-black\` or a \`-foreground\` class "to be safe" — on an ordinary
|
|
611
|
+
panel that is the same fault inverted. \`text-muted-foreground\` for secondary text is the
|
|
612
|
+
exception, and it is the partner of the standard background.
|
|
613
|
+
- Tailwind is version 4: every styling configuration — theme variables, custom utilities,
|
|
614
|
+
layers — lives in the css files. There is NO \`tailwind.config.js\` in the project and it is
|
|
615
|
+
never required: do not create it, do not look for it, do not reference it.
|
|
616
|
+
- A \`SelectItem\` value must never be the empty string. Radix throws
|
|
617
|
+
"A <Select.Item /> must have a value prop that is not an empty string" at render time,
|
|
618
|
+
because the empty string is reserved for clearing the selection. Model an
|
|
619
|
+
empty/unset/"all" option with a sentinel value of \`"__empty__"\` and translate it back to
|
|
620
|
+
an empty value in the change handler:
|
|
621
|
+
|
|
622
|
+
<SelectItem value="__empty__">Any</SelectItem>
|
|
623
|
+
...
|
|
624
|
+
onValueChange={value => onChange(value === '__empty__' ? undefined : value)}
|
|
625
|
+
`),
|
|
626
|
+
skill(ViableSkill.FormFeedback, 'Telling the user what happened', `
|
|
627
|
+
Every action a user takes says what happened, and a successful one leaves them somewhere sensible.
|
|
628
|
+
An action that silently succeeds and an action that silently fails look identical from the other
|
|
629
|
+
side of the screen, and the user's only recourse is to do it again.
|
|
630
|
+
|
|
631
|
+
import { toast } from 'sonner'
|
|
632
|
+
|
|
633
|
+
toast.success('Request submitted')
|
|
634
|
+
toast.error('Could not submit the request — please try again')
|
|
635
|
+
|
|
636
|
+
- The \`<Toaster />\` is ALREADY MOUNTED by the area layouts. Never import it, never render one in
|
|
637
|
+
a screen or a component: a second one renders every message twice.
|
|
638
|
+
- ON SUCCESS of a submit, a create, an update or a delete: raise \`toast.success\` with what
|
|
639
|
+
happened in a few words, then navigate. Where to is decided in this order:
|
|
640
|
+
1. the destination the user story itself names;
|
|
641
|
+
2. otherwise the SECTION DASHBOARD of the section this screen belongs to — its alias is the
|
|
642
|
+
first entry of that section in \`sources/web/src/nav.ts\`.
|
|
643
|
+
Leaving the user on a form they have just submitted, with the fields still filled in, reads as
|
|
644
|
+
though nothing happened.
|
|
645
|
+
- ON FAILURE: raise \`toast.error\` with a message that says what to do about it, and STAY on the
|
|
646
|
+
screen with the entered values intact. Never navigate away from a failure.
|
|
647
|
+
- The toast belongs in the VIEW MODEL's action handler, beside the call that succeeded or threw —
|
|
648
|
+
never in JSX, and never inside a render.
|
|
649
|
+
- Never \`alert()\`, never \`window.confirm()\`, never a hand-built floating \`<div>\` announcing an
|
|
650
|
+
outcome, and never a bare \`console.error\` as the user-facing report.
|
|
651
|
+
|
|
652
|
+
const submit = async (values: RequestInput) => { // right
|
|
653
|
+
try {
|
|
654
|
+
await model.create(values)
|
|
655
|
+
toast.success('Request submitted')
|
|
656
|
+
nav.go(app.web.requestsDashboard)
|
|
657
|
+
} catch (e) {
|
|
658
|
+
toast.error(e instanceof Error ? e.message : 'Could not submit the request')
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
await model.create(values); alert('Saved'); location.href = '/requests' // WRONG
|
|
663
|
+
`),
|
|
664
|
+
skill(ViableSkill.OwlmeansState, 'Client state', `
|
|
665
|
+
There is NO redux in this project. \`@reduxjs/toolkit\` and \`react-redux\` are NOT installed,
|
|
666
|
+
there is no \`store.ts\`, and \`createSlice\`, \`createAsyncThunk\`, \`configureStore\`,
|
|
667
|
+
\`useSelector\`, \`useDispatch\`, \`PayloadAction\`, \`RootState\` and \`AppDispatch\` do not exist.
|
|
668
|
+
Importing any of them fails to resolve.
|
|
669
|
+
|
|
670
|
+
State lives in a state RESOURCE on the OwlMeans context — the same container the entrypoints and
|
|
671
|
+
services live in.
|
|
672
|
+
|
|
673
|
+
**1. One state module per domain type**, at \`sources/web/src/state/<entity>/<type>.ts\`.
|
|
674
|
+
It declares the alias and the hooks that READ it, and nothing else:
|
|
675
|
+
|
|
676
|
+
import { useStoreList, useStoreModel } from '@owlmeans/client'
|
|
677
|
+
import { useContext } from '@owlmeans/web-client'
|
|
678
|
+
import type { Criteria } from '@owlmeans/resource'
|
|
679
|
+
import type { Task } from 'project-common/models/task/task.type.js'
|
|
680
|
+
|
|
681
|
+
export const TASK_STATE = 'task-state'
|
|
682
|
+
|
|
683
|
+
export const useTaskResource = () => useContext().getStateResource<Task>(TASK_STATE)
|
|
684
|
+
export const useTask = (id?: string) => useStoreModel<Task>(id, TASK_STATE)
|
|
685
|
+
export const useTaskList = (query: Criteria<Task> = {}) =>
|
|
686
|
+
useStoreList<Task>({ query, resource: TASK_STATE })
|
|
687
|
+
|
|
688
|
+
\`useStoreModel\` / \`useStoreList\` come from \`@owlmeans/client\` — \`@owlmeans/web-client\` does
|
|
689
|
+
NOT re-export them. \`useContext\` in a STATE module comes from \`@owlmeans/web-client\`, never
|
|
690
|
+
from \`@/context.js\`: that module imports this one to register the alias, so importing it back
|
|
691
|
+
is a cycle.
|
|
692
|
+
|
|
693
|
+
**2. The alias is registered on the context**, one line in \`sources/web/src/context.ts\`
|
|
694
|
+
above its sentinel:
|
|
695
|
+
|
|
696
|
+
import { TASK_STATE } from '@/state/task/task.js'
|
|
697
|
+
...
|
|
698
|
+
appendStateResource<C, T>(context, TASK_STATE)
|
|
699
|
+
// owlmeans: add new state resources above this line
|
|
700
|
+
|
|
701
|
+
Without that line every hook for the alias throws \`Resource task-state not found\` at runtime,
|
|
702
|
+
and no type check can see it.
|
|
703
|
+
|
|
704
|
+
**3. Reading.** \`useStoreList({ query })\` is a LIVE list: it re-renders whenever a write changes
|
|
705
|
+
which records match, so a screen never recomputes ids and never re-subscribes.
|
|
706
|
+
|
|
707
|
+
const open = useTaskList({ status: 'open' }) // StateModel<Task>[]
|
|
708
|
+
open.map(model => model.record.title)
|
|
709
|
+
|
|
710
|
+
\`useStoreModel(id)\` is one record — the model always exists so a screen has something to bind
|
|
711
|
+
to, but nothing is invented to fill it. \`model.empty\` is what "not loaded yet" looks like:
|
|
712
|
+
|
|
713
|
+
const task = useTask(id)
|
|
714
|
+
if (task.empty) return <Spinner /> // no record for that id (yet)
|
|
715
|
+
|
|
716
|
+
**4. Writing.** A \`StateModel.record\` is a COPY. Assigning to it changes nothing:
|
|
717
|
+
|
|
718
|
+
model.record.title = 'renamed' // WRONG — silent no-op, nothing re-renders
|
|
719
|
+
model.update({ title: 'renamed' }) // RIGHT — merges and commits
|
|
720
|
+
model.clear() // deletes the record
|
|
721
|
+
|
|
722
|
+
Through the resource: \`await tasks.save(record)\` creates-or-replaces, \`tasks.delete(id)\`
|
|
723
|
+
removes, \`tasks.list(criteria)\` returns \`{ items, total }\` and \`tasks.count(criteria)\` the
|
|
724
|
+
number alone.
|
|
725
|
+
|
|
726
|
+
When an endpoint answers with a WHOLE set — every task of a project, the session's items — install
|
|
727
|
+
it in one step:
|
|
728
|
+
|
|
729
|
+
await tasks.replace(fromServer) // the store now holds exactly these
|
|
730
|
+
|
|
731
|
+
\`replace\` is what makes the store agree with the server: a record deleted elsewhere leaves in the
|
|
732
|
+
same write, and the subscribers wake once instead of once per record. A loop of \`save\` calls only
|
|
733
|
+
ever ADDS, so anything stale stays visible forever.
|
|
734
|
+
|
|
735
|
+
**5. Criteria** are the same language the backend resources take: a bare value is equality, a
|
|
736
|
+
bare ARRAY means "any of these", plus \`$eq $ne $gt $gte $lt $lte $in $nin $exists $null $like
|
|
737
|
+
$ilike $regex $startsWith $endsWith $between\` and \`$and\` / \`$or\` / \`$not\`. A dotted key
|
|
738
|
+
reaches into the record. A value of \`undefined\` is skipped, so an untouched filter does not
|
|
739
|
+
empty the list.
|
|
740
|
+
`),
|
|
741
|
+
skill(ViableSkill.StoreAccess, 'Reading and writing the store', `
|
|
742
|
+
- The store is NOT granular: it holds whole records. There is no method per field, and adding
|
|
743
|
+
one is not how a single property is changed — \`model.update({ field })\` merges.
|
|
744
|
+
- A subscribed record is reached through \`model.record\`, never as the model itself:
|
|
745
|
+
\`task.record.title\`, not \`task.title\`.
|
|
746
|
+
- A record may not have arrived yet, and nothing invents one to stand in. \`model.empty\` says so;
|
|
747
|
+
guard on it before rendering, and guard before reading THROUGH the record —
|
|
748
|
+
\`model.record?.items?.[id]?.name\` — rather than assuming the shape is populated.
|
|
749
|
+
- A list read is \`model.list()\` on the resource, returning \`{ items, total }\` — take the count
|
|
750
|
+
from \`total\`, and \`count(criteria)\` when the number is all that is wanted.
|
|
751
|
+
- The store carries RECORDS, not request status. Loading flags and error messages are ordinary
|
|
752
|
+
\`useState\` in the view model; a field named \`isLoading\` or \`error\` does not belong in a
|
|
753
|
+
stored record unless the domain type genuinely has one.
|
|
754
|
+
`),
|
|
755
|
+
skill(ViableSkill.ViewModelNaming, 'View-model naming convention', `
|
|
756
|
+
A component's view logic lives in its sibling \`.vm.ts\` file. Derive its public
|
|
757
|
+
symbols from the component name (PascalCase of the component definition — for example
|
|
758
|
+
"main-task-list" becomes "MainTaskList"):
|
|
759
|
+
|
|
760
|
+
- The hook is \`use<ComponentName>ViewModel\` (\`useMainTaskListViewModel\`).
|
|
761
|
+
- Its options/props type, when it takes arguments, is \`<ComponentName>ViewModelOptions\`.
|
|
762
|
+
- The primary loader the hook returns is \`load<ComponentName>\`; a refresh action is
|
|
763
|
+
\`reload<ComponentName>\`.
|
|
764
|
+
- Action handlers are verb-first camelCase (\`createTask\`, \`removeTask\`).
|
|
765
|
+
|
|
766
|
+
Both sides must use these exact names. This is a DEFAULT: when a "View-model contract"
|
|
767
|
+
block is present in the task, it lists the symbols that actually exist and overrides this
|
|
768
|
+
convention entirely.
|
|
769
|
+
`),
|
|
770
|
+
skill(ViableSkill.ResourceLayer, 'OwlMeans resources own the schema', `
|
|
771
|
+
A "resource" is ONE file — \`src/resources/{entity}/{type}.ts\` — carrying all data access
|
|
772
|
+
for one domain entity: its AJV schema, its \`makePostgresResource()\` maker, and an accessor
|
|
773
|
+
that returns the instance already registered on the OwlMeans context.
|
|
774
|
+
|
|
775
|
+
\`src/resources\` sits directly under \`src\`, not under \`models\`.
|
|
776
|
+
|
|
777
|
+
THE RESOURCE OWNS THE DDL. You never define a table. The AJV schema you write on the
|
|
778
|
+
resource IS the table: the OwlMeans layer creates it, and on every start reconciles an
|
|
779
|
+
existing table against the schema — adding, retyping and dropping columns and reconciling
|
|
780
|
+
indexes. A resource whose schema changed simply converges the next time the backend starts.
|
|
781
|
+
|
|
782
|
+
Never do any of the following — each one is the removed Drizzle-DDL layout. Some of these
|
|
783
|
+
imports still RESOLVE, because the framework uses the driver internally, so a clean build
|
|
784
|
+
proves nothing: the table you define becomes a SECOND owner of one the resource layer
|
|
785
|
+
already owns, and the next start reconciles yours away.
|
|
786
|
+
|
|
787
|
+
- \`import ... from 'drizzle-orm'\` or \`'drizzle-orm/pg-core'\`. \`drizzle-orm\` is NOT a
|
|
788
|
+
dependency of this project. If you ever truly need the builder, the resource hands it to
|
|
789
|
+
you — see the resource-method rules below.
|
|
790
|
+
- \`import ... from 'postgres'\`, \`'pg'\`, \`'@/lib/db.js'\` or \`'@/db/schema/...'\`. \`pg\` is
|
|
791
|
+
installed only because the framework's driver requires it; it is never yours to import.
|
|
792
|
+
- calling \`drizzle()\`, \`pgSchema()\` or \`pgTable()\` yourself.
|
|
793
|
+
- writing a table definition, a migration file, a \`drizzle.config.ts\`, or a \`src/db\`
|
|
794
|
+
directory. There is no \`db:generate\` and no \`db:migrate\`.
|
|
795
|
+
|
|
796
|
+
A foreign key is declared by the referenced resource's ALIAS, never by importing another
|
|
797
|
+
file:
|
|
798
|
+
|
|
799
|
+
pg: { references: { resource: 'other-alias' } }
|
|
800
|
+
|
|
801
|
+
If the referenced resource does not exist, use a plain string property instead.
|
|
802
|
+
`),
|
|
803
|
+
skill(ViableSkill.ResourceResults, 'How resource methods are called, and what they return', `
|
|
804
|
+
Reach data through the accessor from \`@/resources/{entity}/{type}.js\` and call the
|
|
805
|
+
resource's own methods. Never write raw SQL in a route handler, never import a database
|
|
806
|
+
connection, and never construct a resource yourself.
|
|
807
|
+
|
|
808
|
+
A WRITE takes ONE argument — the record — and the id travels INSIDE it. There is no
|
|
809
|
+
\`(id, changes)\` overload on any of them:
|
|
810
|
+
|
|
811
|
+
await resource.create(payload) // refuses a caller-supplied id
|
|
812
|
+
await resource.update({ ...current, ...changes }) // replaces the WHOLE record
|
|
813
|
+
await resource.patch({ id: current.id, progress }) // merges only the listed fields
|
|
814
|
+
await resource.save(record) // create-or-replace
|
|
815
|
+
|
|
816
|
+
THE DATABASE ASSIGNS THE ID. \`id\` is a generated column — the table is created with a
|
|
817
|
+
\`gen_random_uuid()\` default — so a NEW record does not have one yet and it is not yours to
|
|
818
|
+
invent. NEVER generate an id in application code: no \`randomUUID()\`, no \`crypto.randomUUID()\`,
|
|
819
|
+
no \`uuid()\`, no counter, no \`Date.now()\` string, and never \`import { randomUUID } from
|
|
820
|
+
'crypto'\` for this. Build the record WITHOUT an \`id\` key at all and read the id off the record
|
|
821
|
+
\`create\` HANDS BACK:
|
|
822
|
+
|
|
823
|
+
const created = await resource.create({ title, ownerId }) // no id in the payload
|
|
824
|
+
return created.id // the id the database assigned
|
|
825
|
+
|
|
826
|
+
\`create\` REJECTS a record that carries an \`id\` — including one you just generated — by
|
|
827
|
+
throwing \`resource:record-exists:id-present\`. Despite its name that error does NOT mean a
|
|
828
|
+
duplicate record was found and it is NOT a race, a double submit or a uniqueness collision: it
|
|
829
|
+
means the payload had an id in it. The only fix is to stop putting one there. Do not "repair" it
|
|
830
|
+
with a retry, an in-flight guard, an idempotency check, or a \`load(id)\`-then-return-existing
|
|
831
|
+
branch — none of those touch the cause, and looking up an id you just minted can only ever miss.
|
|
832
|
+
|
|
833
|
+
When you genuinely have an id already — the record exists and you are writing it back — that is
|
|
834
|
+
\`update\`/\`patch\`/\`save\`, never \`create\`. A record type declaring \`id: string\` as required
|
|
835
|
+
describes a STORED record; the create payload is that type without its id (\`Omit<T, 'id'>\`).
|
|
836
|
+
|
|
837
|
+
Passing an id as a WRITE's first argument fails to compile with \`error TS2559: Type 'string'
|
|
838
|
+
has no properties in common with type 'Partial<...>'\` — a bare id is a READ's argument, never
|
|
839
|
+
a write's. Load the record first when you only have its id, then write the merged object back
|
|
840
|
+
— or use \`patch\`.
|
|
841
|
+
|
|
842
|
+
A READ takes either an ID or a CRITERIA OBJECT as its first argument. There is no
|
|
843
|
+
"value plus field name" overload — the field is a key of the object:
|
|
844
|
+
|
|
845
|
+
await resource.load(id) // the record, or null
|
|
846
|
+
await resource.load({ projectId }) // by any other field, or several at once
|
|
847
|
+
await resource.get(id) // throws UnknownRecordError instead of null
|
|
848
|
+
await resource.get({ ownerId, slug })
|
|
849
|
+
await resource.list({ status: 'open' }) // { items, total, page?, size? }
|
|
850
|
+
await resource.count({ status: 'open' }) // number
|
|
851
|
+
|
|
852
|
+
Writing \`load(projectId, 'projectId')\` fails to compile — the second parameter is
|
|
853
|
+
\`{ sort }\`, not a field name. Fetching ONE record is \`load\`/\`get\`, never a \`list\` whose
|
|
854
|
+
first element you take: \`const { items: [x] } = await resource.list({ a, b })\` is
|
|
855
|
+
\`const x = await resource.load({ a, b })\`.
|
|
856
|
+
|
|
857
|
+
Inside the criteria object a bare value means equality, a bare ARRAY means "any of these"
|
|
858
|
+
(\`{ status: ['open', 'held'] }\`), \`null\` asks for IS NULL, and \`undefined\` is SKIPPED —
|
|
859
|
+
so an untouched filter cannot empty a list. A field may instead carry an operator object —
|
|
860
|
+
\`$eq $ne $gt $gte $lt $lte $in $nin $exists $null $like $ilike $regex $startsWith $endsWith
|
|
861
|
+
$between $contains $contained $overlaps\` — and \`$and\` / \`$or\` / \`$not\` combine whole
|
|
862
|
+
criteria:
|
|
863
|
+
|
|
864
|
+
await resource.list({ createdAt: { $gte: since }, $or: [{ ownerId }, { shared: true }] })
|
|
865
|
+
|
|
866
|
+
\`delete(id)\` returns the removed record or \`null\`. \`take(id)\` DELETES the record it hands
|
|
867
|
+
back and throws \`UnknownRecordError\` when there is none — it is never a way to read one; use
|
|
868
|
+
\`load\`/\`get\` for that. \`purge(where)\` deletes every match and returns how many went, so
|
|
869
|
+
clearing a set is one call and never a page loop around \`delete\`; it refuses an empty \`{}\`
|
|
870
|
+
rather than emptying the table.
|
|
871
|
+
|
|
872
|
+
These methods return the RECORDS THEMSELVES, never a driver result object. There is NO
|
|
873
|
+
\`rowsAffected\`, \`affectedRows\`, \`changes\` or \`rowCount\` anywhere — reading one
|
|
874
|
+
yields \`undefined\`, which then silently compares as false. Use \`count(where)\` for a
|
|
875
|
+
number and \`await resource.load(id) != null\` for an existence check.
|
|
876
|
+
|
|
877
|
+
PAGING AND SORT ARE FLAT IN THE SECOND ARGUMENT of \`list\`. There is no \`limit\`, no
|
|
878
|
+
\`offset\`, and no \`pager\` or \`criteria\` wrapper — those two names do not exist:
|
|
879
|
+
|
|
880
|
+
resource.list({ status: 'open' }, { page: 0, size: 20, sort: ['createdAt'] })
|
|
881
|
+
|
|
882
|
+
- \`sort\` is an array of field names (ascending) or \`{ field, order: 'desc' }\` objects.
|
|
883
|
+
- Writing \`list({}, { page, limit: 10 })\` fails with \`'limit' does not exist in type
|
|
884
|
+
'ListOptions<...>'\`, and nesting the same pair under \`pager\` only moves the error to
|
|
885
|
+
\`'pager' does not exist in type 'ListOptions<...>'\`. The declared name is \`size\`.
|
|
886
|
+
- The backend pages by itself: Postgres returns at most \`DEFAULT_PAGE_SIZE\` (100) rows when
|
|
887
|
+
\`size\` is absent, so a listing that genuinely needs every match asks for
|
|
888
|
+
\`{ size: 0 }\` — explicitly, once, where a reader can see it.
|
|
889
|
+
- The result is \`{ items, total, page?, size? }\`. \`total\` is the full count of matching
|
|
890
|
+
records, never \`items.length\` — build pagination UI from it, and never recompute it.
|
|
891
|
+
|
|
892
|
+
For a join or aggregate the CRUD surface cannot express, use \`resource.select()\`/\`query()\`
|
|
893
|
+
with \`{{}}\` for this resource's table and \`{{other-alias}}\` for another registered
|
|
894
|
+
resource's.
|
|
895
|
+
|
|
896
|
+
A Drizzle handle exists as a LAST RESORT, and only through the resource:
|
|
897
|
+
\`(await resource.db()).drizzle\` over \`resource.entity\`, reached with NO import. Nothing on
|
|
898
|
+
that path is typed — \`resource.entity\` is \`Record<string, any>\` — and the moment the query
|
|
899
|
+
needs \`eq\`, \`and\` or \`sql\` it needs an import you may not write: that query goes in
|
|
900
|
+
\`resource.query()\` instead. Read \`resource.entity\` inside a function, never at module
|
|
901
|
+
scope; it is built during \`init()\` and is \`undefined\` before then.
|
|
902
|
+
`),
|
|
903
|
+
skill(ViableSkill.ResourceMigrations, 'Data migrations', `
|
|
904
|
+
Schema shape needs no migration — reconciliation handles it. Only a DATA change
|
|
905
|
+
reconciliation cannot make on its own (a backfill, a value rewrite) needs one, and it is
|
|
906
|
+
registered on the resource as code, inside the maker, beside \`resource.index(...)\`. The
|
|
907
|
+
signature is \`resource.migration(name, fn, stage)\`:
|
|
908
|
+
|
|
909
|
+
resource.migration(name, async tx => { ... }, stage)
|
|
910
|
+
|
|
911
|
+
Registering is all there is to do: the resource runs its pending migrations itself during
|
|
912
|
+
\`init()\`, so nothing ever calls one. The call returns the resource, so several chain, and
|
|
913
|
+
re-registering the same name with the same body is a no-op — which is what makes it safe in a
|
|
914
|
+
maker that runs more than once.
|
|
915
|
+
|
|
916
|
+
Each one is applied once and recorded under its name and a checksum of its body. Editing a
|
|
917
|
+
migration that has already run throws \`resource:migration-conflict\` at the next boot; a new
|
|
918
|
+
fact needs a NEW migration under a new name. A migration that throws aborts the boot with
|
|
919
|
+
\`resource:migration-failed\` rather than leaving the app on a half-shaped database.
|
|
920
|
+
`),
|
|
921
|
+
skill(ViableSkill.OwlmeansContext, 'The OwlMeans context', `
|
|
922
|
+
The context is the application's container. It holds exactly two kinds of thing: RESOURCES (data)
|
|
923
|
+
and SERVICES. Nothing else lives on it.
|
|
924
|
+
|
|
925
|
+
**The context is built by a LIBRARY and there is no module-level \`owlCtx\` on the server side.**
|
|
926
|
+
\`makeBackendContext\` lives in \`sources/backend/src/context.ts\`, and the api and the worker each
|
|
927
|
+
build their own from it — so a module in \`sources/backend\` that imported a container singleton
|
|
928
|
+
would bind whichever process happened to load it first. The browser is the exception: the web app
|
|
929
|
+
IS one process, and \`sources/web/src/owlmeans.ts\` exports \`owlCtx\` for module-level code.
|
|
930
|
+
|
|
931
|
+
**Data comes from the resource accessor, and the accessor takes the context.** Every handler and
|
|
932
|
+
every job processor is handed its own; pass it down.
|
|
933
|
+
|
|
934
|
+
import { taskResource } from 'project-backend/resources/task/task'
|
|
935
|
+
const { items } = await taskResource(ctx).list({ status: 'open' })
|
|
936
|
+
|
|
937
|
+
**Business logic in \`src/models/**\` is PLAIN EXPORTED FUNCTIONS. It is never a service.**
|
|
938
|
+
Never write this:
|
|
939
|
+
|
|
940
|
+
// WRONG — this object is not a service, nothing registers it, and the lookup throws
|
|
941
|
+
export const DASHBOARD_SERVICE = 'dashboard-activity'
|
|
942
|
+
export const makeDashboardService = (ctx) => ({
|
|
943
|
+
alias: DASHBOARD_SERVICE, registerContext: ..., assertCtx: ..., initialized: true,
|
|
944
|
+
getSummary: async () => ...
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
Write this instead, and let the route handler import the function directly:
|
|
948
|
+
|
|
949
|
+
// RIGHT — sources/backend/src/models/dashboard/activity.ts
|
|
950
|
+
export const getSummary = async (ctx: BasicContext<BasicConfig>, guestId: string) => { ... }
|
|
951
|
+
|
|
952
|
+
// RIGHT — sources/api/src/app/dashboard/activity.ts
|
|
953
|
+
import { getSummary } from 'project-backend/models/dashboard/activity.js'
|
|
954
|
+
const summary = await getSummary(ctx, guestId)
|
|
955
|
+
|
|
956
|
+
Do not create a service just to hold functions.
|
|
957
|
+
|
|
958
|
+
**A resource is NEVER reached through \`service()\`.** Resources and services are two different
|
|
959
|
+
registries. Asking for a resource alias on the service registry throws even though the resource
|
|
960
|
+
exists and is registered:
|
|
961
|
+
|
|
962
|
+
// WRONG — 'reservation' is a RESOURCE; this throws
|
|
963
|
+
// SyntaxError: Service reservation not found
|
|
964
|
+
const reservations = ctx.service<PostgresResource<any>>('reservation')
|
|
965
|
+
|
|
966
|
+
// RIGHT — import the accessor the resource file exports, and hand it the context
|
|
967
|
+
import { reservationResource } from 'project-backend/resources/reservation/reservation'
|
|
968
|
+
const { items } = await reservationResource(ctx).list({ status: 'open' })
|
|
969
|
+
|
|
970
|
+
A function in \`src/models/**\` takes the context first and its own domain arguments after.
|
|
971
|
+
|
|
972
|
+
**\`ctx.service(alias)\` THROWS when the alias is unknown** — it does NOT return
|
|
973
|
+
\`undefined\`:
|
|
974
|
+
|
|
975
|
+
SyntaxError: Service dashboard-activity not found
|
|
976
|
+
|
|
977
|
+
So a null check around it is dead code that never runs:
|
|
978
|
+
|
|
979
|
+
// WRONG — the throw already happened on the line above
|
|
980
|
+
const svc = ctx.service(ALIAS)
|
|
981
|
+
if (!svc) { throw { status: 503 } }
|
|
982
|
+
|
|
983
|
+
\`ctx.resource(alias)\` and \`ctx.entrypoint(alias)\` throw the same way
|
|
984
|
+
(\`Resource X not found\`, \`Entrypoint X not found\`).
|
|
985
|
+
|
|
986
|
+
**Only when a real lifecycle-owning singleton is needed** — a client holding a connection, a
|
|
987
|
+
cache, a poller. Business logic never qualifies, and neither does "the handler needs to call
|
|
988
|
+
this": export a function. When it genuinely does qualify, put it in
|
|
989
|
+
\`sources/backend/src/services/{name}.ts\`, one service per file,
|
|
990
|
+
flat in that directory, built with \`createService\` and never as a hand-written object:
|
|
991
|
+
|
|
992
|
+
import { createService } from '@owlmeans/context'
|
|
993
|
+
import type { InitializedService } from '@owlmeans/context'
|
|
994
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
995
|
+
|
|
996
|
+
export const SERVICE_ALIAS = 'dashboard-activity'
|
|
997
|
+
|
|
998
|
+
export interface DashboardActivityService extends InitializedService {
|
|
999
|
+
getSummary: (guestId: string) => Promise<Summary | null>
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// The maker name is fixed — the generated service registry imports exactly this symbol.
|
|
1003
|
+
export const makeService = (): DashboardActivityService =>
|
|
1004
|
+
createService<DashboardActivityService>(SERVICE_ALIAS, {
|
|
1005
|
+
getSummary: async guestId => { ... }
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
/** The registered instance — import this from API handlers and job processors. */
|
|
1009
|
+
export const dashboardActivityService = (ctx: BasicContext<BasicConfig>): DashboardActivityService =>
|
|
1010
|
+
ctx.service<DashboardActivityService>(SERVICE_ALIAS)
|
|
1011
|
+
|
|
1012
|
+
Never call \`registerService()\` yourself and never edit \`src/services/index.ts\`: that registry
|
|
1013
|
+
is generated from the directory, and the context factory registers everything in it before
|
|
1014
|
+
\`init()\` runs.
|
|
1015
|
+
|
|
1016
|
+
**Never resolve anything at module scope.** Module bodies run before the context is initialized,
|
|
1017
|
+
so a top-level \`const svc = ctx.service(X)\` throws while the file is being imported and kills
|
|
1018
|
+
the process before it can listen. Resolve inside the function that uses it.
|
|
1019
|
+
`),
|
|
1020
|
+
skill(ViableSkill.OwlMeansServices, 'Services vs entrypoints — two registries', `
|
|
1021
|
+
This project has TWO registries of names. They share nothing, and confusing them is the single
|
|
1022
|
+
most expensive mistake that still compiles.
|
|
1023
|
+
|
|
1024
|
+
**SERVICES** are the two sides of the application as NETWORK ENDPOINTS — a host, a port, a base
|
|
1025
|
+
path. They live in \`cfg.services\`, and there are exactly two:
|
|
1026
|
+
|
|
1027
|
+
- \`APP_WEB\` — the frontend
|
|
1028
|
+
- \`APP_API\` — the backend
|
|
1029
|
+
|
|
1030
|
+
Both are declared in \`sources/common/src/consts.ts\` and registered by a \`service({ … })\` /
|
|
1031
|
+
\`sservice({ … })\` call in \`sources/web/src/config.ts\` and
|
|
1032
|
+
\`sources/api/src/owlmeans.ts\`. **\`serviceRoute()\` reads this registry and nothing else.**
|
|
1033
|
+
|
|
1034
|
+
**ENTRYPOINTS** are the \`app.web.*\` and \`app.api.*\` tree — screens and endpoints, i.e. ROUTES.
|
|
1035
|
+
\`ctx.entrypoint()\` reads that one.
|
|
1036
|
+
|
|
1037
|
+
// RIGHT — sources/web/src/owlmeans.ts, exactly as the project ships
|
|
1038
|
+
import { APP_API, APP_WEB } from 'project-common/consts'
|
|
1039
|
+
owlCtx.serviceRoute(APP_WEB, true)
|
|
1040
|
+
owlCtx.serviceRoute(APP_API, true)
|
|
1041
|
+
|
|
1042
|
+
// WRONG — every one of these type-checks, builds clean, and blanks the whole application
|
|
1043
|
+
owlCtx.serviceRoute(app.web.base, true)
|
|
1044
|
+
owlCtx.serviceRoute(app.web.area.guest, true)
|
|
1045
|
+
owlCtx.serviceRoute(app.api.post.base, true)
|
|
1046
|
+
|
|
1047
|
+
Both sides are strings, so nothing in TypeScript can tell them apart. The failure appears only
|
|
1048
|
+
in the browser, while the module is still being imported:
|
|
1049
|
+
|
|
1050
|
+
Uncaught SyntaxError: Service not found web:base
|
|
1051
|
+
|
|
1052
|
+
The alias in that message is not a key of \`cfg.services\`. Pass one of the SERVICE constants
|
|
1053
|
+
above. Do NOT substitute a different \`app.*\` value: the error simply returns wearing the new
|
|
1054
|
+
name, which is how one project spent twenty repair attempts on one line — every one of them
|
|
1055
|
+
reporting success.
|
|
1056
|
+
|
|
1057
|
+
**\`config.ts\` and \`owlmeans.ts\` on both sides are framework wiring this project was generated
|
|
1058
|
+
with.** They carry no sentinel and no application code, nothing in the pipeline authors them, and
|
|
1059
|
+
the write tools refuse them. If one of them is the cause, call \`restore_wiring_file\` to put back
|
|
1060
|
+
the version the project was generated from — never re-derive it, and never "fix" it by changing
|
|
1061
|
+
which name it uses. A service alias is renamed in \`sources/common/src/consts.ts\` and nowhere
|
|
1062
|
+
else, and both sides must keep using the same constant or the frontend calls a backend that
|
|
1063
|
+
publishes itself under another name.
|
|
1064
|
+
`),
|
|
1065
|
+
skill(ViableSkill.OwlMeansServer, 'Writing an endpoint handler', `
|
|
1066
|
+
There is NO express here. \`express\`, \`cors\` and \`@types/express\` are not installed, there is
|
|
1067
|
+
no \`app.get(...)\`, no \`req\`/\`res\` of a web server, no \`next\`, and no middleware. Anything
|
|
1068
|
+
written for express fails to resolve.
|
|
1069
|
+
|
|
1070
|
+
A handler is a plain async function that RETURNS its result, wrapped in one of three helpers
|
|
1071
|
+
from \`@owlmeans/server-app\`. Pick by what the handler reads:
|
|
1072
|
+
|
|
1073
|
+
import { handleBody, handleParams, handleRequest } from '@owlmeans/server-app'
|
|
1074
|
+
|
|
1075
|
+
// the request BODY — the payload type is the type argument
|
|
1076
|
+
export const createTask = handleBody<TaskInput>(async (payload, ctx) => {
|
|
1077
|
+
const tasks = getTaskResource(ctx)
|
|
1078
|
+
return await tasks.create(payload)
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
// the route PARAMS — names match the ':' segments of the declared path
|
|
1082
|
+
export const getTask = handleParams<{ taskId: string }>(async ({ taskId }, ctx) => {
|
|
1083
|
+
return await tasks(ctx).load(taskId)
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
// anything else (query, headers, nothing at all) — the whole request
|
|
1087
|
+
export const listTasks = handleRequest(async (req, ctx) => {
|
|
1088
|
+
const query = req.query as { search?: string }
|
|
1089
|
+
return await tasks(ctx).list(query.search != null ? { search: query.search } : {})
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
- The SECOND argument is the OwlMeans context. Reach every resource through it — never
|
|
1093
|
+
import a database connection and never write raw SQL in a handler.
|
|
1094
|
+
- RETURN the value. \`res.json(...)\`, \`res.status(...)\`, \`res.send(...)\` do not exist; a
|
|
1095
|
+
returned value IS the 200 response body.
|
|
1096
|
+
- Do not wrap the body in try/catch to convert an error into a response. A thrown
|
|
1097
|
+
\`ResilientError\` subclass is mapped to its status by the framework; catching it produces a
|
|
1098
|
+
200 carrying an error object instead.
|
|
1099
|
+
- Never read a token, never check a role, never look at an \`Authorization\` header. Access is
|
|
1100
|
+
declared on the entrypoint (\`guard()\` / \`gate()\`) and enforced before the handler runs.
|
|
1101
|
+
- A handler is inert until an \`elevate(appEntrypoints, alias, handler)\` line in
|
|
1102
|
+
\`sources/api/src/entrypoints.ts\` binds it to its alias. Without that line the endpoint
|
|
1103
|
+
answers 404 and nothing reports an error.
|
|
1104
|
+
- Handlers are ENTITY-SCOPED: \`sources/api/src/app/<entity>/<action>.ts\`, named exports only.
|
|
1105
|
+
The directory is what keeps two entities' \`list\` apart — the file name carries no marker.
|
|
1106
|
+
`),
|
|
1107
|
+
skill(ViableSkill.FixerHeuristics, 'Reading a build error', `
|
|
1108
|
+
Change as little as the reported error requires, and read the error for what it actually
|
|
1109
|
+
says before rewriting anything.
|
|
1110
|
+
|
|
1111
|
+
- When the task carries a \`NAME REGISTRY\` block, it is AUTHORITATIVE for every directory,
|
|
1112
|
+
path, alias and exported symbol it lists. NEVER rename an identifier that appears in it. An
|
|
1113
|
+
error saying one "does not exist" means the thing is MISSING and must be DECLARED or CREATED
|
|
1114
|
+
where the registry says it lives — add the alias line above the sentinel, add the import,
|
|
1115
|
+
write the file at that path. Renaming the reference to a different guess makes the error move
|
|
1116
|
+
rather than go away: the next build reports it again under the new spelling, and the loop
|
|
1117
|
+
never converges. "Change as little as required" is about the SIZE of the edit, never a licence
|
|
1118
|
+
to rename instead of declare.
|
|
1119
|
+
- Two files disagreeing about where something lives is not two problems. Take the registry's
|
|
1120
|
+
spelling in both, and never invent a third.
|
|
1121
|
+
|
|
1122
|
+
- **"Cannot find module '@/…'" in a file under \`sources/backend/\` is the ALIAS, not the
|
|
1123
|
+
suffix.** That package is the shared library: it is built by \`tsc -b\`, declares no tsconfig
|
|
1124
|
+
\`paths\`, and \`@/\` resolves to nothing in it. Rewrite the specifier as a RELATIVE path keeping
|
|
1125
|
+
its \`.js\` suffix — \`'../../resources/task/task.js'\`. Adding, removing or changing the
|
|
1126
|
+
extension on the alias fixes nothing and the same error comes back under the other spelling;
|
|
1127
|
+
two attempts that differ only in the suffix mean you are in this case.
|
|
1128
|
+
- "Cannot find module" for \`postgres\`, \`@/lib/db.js\` or \`@/db/schema/...\` inside a
|
|
1129
|
+
\`resources/**\` file means the file was written against the REMOVED Drizzle-DDL layout.
|
|
1130
|
+
Rewrite it as an OwlMeans resource: an AJV schema on \`resource.schema\`, built with
|
|
1131
|
+
\`makePostgresResource\`, with no table definition at all. Do not create the missing module.
|
|
1132
|
+
- An \`import\` of \`drizzle-orm\`, \`drizzle-orm/pg-core\` or \`pg\` reports NO error — the
|
|
1133
|
+
driver is installed for the framework's own use — and is still wrong. It is never the cause
|
|
1134
|
+
of the error you were given, so do not go hunting for one; but when the reported error puts
|
|
1135
|
+
you in that file anyway, drop the import as part of the fix. The builder is
|
|
1136
|
+
\`(await resource.db()).drizzle\`, and anything that needed \`eq\`/\`and\`/\`sql\` becomes
|
|
1137
|
+
\`resource.query()\` with \`{{}}\` and \`$1\` parameters.
|
|
1138
|
+
- A missing foreign-key target means the referenced resource does not exist. Declare the
|
|
1139
|
+
reference by alias (\`pg: { references: { resource: '<alias>' } }\`) or fall back to a
|
|
1140
|
+
plain string property — never import another resource file. The alias must be one the
|
|
1141
|
+
registry (or the listed existing resources) actually names: an invented one compiles and then
|
|
1142
|
+
kills the backend at startup with \`fk-unknown-resource\`, which no type check can catch.
|
|
1143
|
+
- An error about \`rowsAffected\`/\`rowCount\`/\`changes\` not existing is the resource result
|
|
1144
|
+
shape — see the result rules above. \`result.pager\` is the same fault: a listing returns
|
|
1145
|
+
\`{ items, total, page?, size? }\`, so \`pager?.total\` becomes \`total\`.
|
|
1146
|
+
- \`'limit' does not exist in type 'ListOptions<...>'\` (or the same for \`offset\` or
|
|
1147
|
+
\`pager\`) is one call written against a shape that has none of them. Paging is FLAT in
|
|
1148
|
+
\`list\`'s second argument: \`list(where, { page, size, sort })\`. \`size\` is the declared
|
|
1149
|
+
name — moving the pair under a \`pager\` key only changes which of the two names the next
|
|
1150
|
+
build rejects, and that swap is the loop that never converges.
|
|
1151
|
+
- \`Property 'pick' does not exist\` on a resource means \`take(id)\` — delete-and-return, which
|
|
1152
|
+
throws \`UnknownRecordError\` instead of returning null.
|
|
1153
|
+
- \`Argument of type 'string' is not assignable to parameter of type 'Criteria<...>'\` on a
|
|
1154
|
+
\`load\`/\`get\` call is the removed \`(value, 'field')\` overload. The field is a key of the
|
|
1155
|
+
criteria object: \`load({ projectId })\`. The second argument now carries only \`sort\`.
|
|
1156
|
+
- \`delete\` takes an id and nothing else. A \`delete(criteria)\` call is \`purge(where)\`, which
|
|
1157
|
+
returns the number of records removed rather than a record.
|
|
1158
|
+
- "Cannot find module" for a third-party router or HTTP-server package (\`express\`, \`cors\`),
|
|
1159
|
+
\`@reduxjs/toolkit\`, \`react-redux\`, \`@/state/store\` or \`@/lib/fetch\` means the file was
|
|
1160
|
+
written against the REMOVED stack. None of those packages are installed and none will be.
|
|
1161
|
+
Rewrite the file against entrypoints: \`useNavigate\` from \`@owlmeans/client\` for navigation,
|
|
1162
|
+
\`ctx.entrypoint(alias).call(...)\` for a backend call, a
|
|
1163
|
+
\`handleRequest\`/\`handleBody\`/\`handleParams\` function for an endpoint, and the state hooks
|
|
1164
|
+
(\`useStoreModel\`/\`useStoreList\` over a state resource) for client state. Do NOT install the
|
|
1165
|
+
package and do NOT create the missing module.
|
|
1166
|
+
- A missing export from a \`*.ts\` module — a selector, an action creator, a thunk, a
|
|
1167
|
+
reducer — is the same removed stack in a different disguise. That module exports an ALIAS
|
|
1168
|
+
constant and read hooks only. Replace a selector with the matching hook, a dispatched action
|
|
1169
|
+
with \`resource.save(record)\` / \`model.update({ ... })\`, and a thunk with an
|
|
1170
|
+
\`entrypoint(alias).call(...)\` in the VIEW MODEL followed by a \`save\`. Do not add the missing
|
|
1171
|
+
export to the state module.
|
|
1172
|
+
- A runtime \`Resource <alias> not found\` for a state alias means the state module is written but
|
|
1173
|
+
nothing registered it. Add the import and ONE
|
|
1174
|
+
\`appendStateResource<C, T>(context, ALIAS)\` line above the sentinel in
|
|
1175
|
+
\`sources/web/src/context.ts\` — never a second alias string, and never a new store file.
|
|
1176
|
+
- \`DEFAULT_ID\` has no export from \`@owlmeans/state\`. A record is either held or it is not, and
|
|
1177
|
+
\`model.empty\` is the test — \`record.id === DEFAULT_ID\` was a placeholder that no longer gets
|
|
1178
|
+
invented. Never reintroduce the constant, and never compare against \`'_default'\` in its place.
|
|
1179
|
+
- \`Property 'erase' does not exist\` on a state resource is \`clear()\`; \`all()\` and
|
|
1180
|
+
\`match(criteria)\` are both \`list(criteria)\`, which returns \`{ items, total }\` rather than a
|
|
1181
|
+
bare array. An \`erase()\` followed by a \`save\` per record is \`replace(records)\` — one write,
|
|
1182
|
+
and the store ends up holding exactly what the server sent.
|
|
1183
|
+
- A list screen that recomputes ids to keep itself in step is fighting the store. One
|
|
1184
|
+
\`useStoreList({ query, resource })\` subscribes to the CRITERIA and re-renders on every write
|
|
1185
|
+
that changes which records match, so the \`useValue(() => resource.list())\` that fed it a list
|
|
1186
|
+
of ids goes away entirely rather than being repaired.
|
|
1187
|
+
- A runtime \`TypeError: entrypoint.call is not a function\` (or \`<name>.call is not a function\`
|
|
1188
|
+
on the result of \`ctx.entrypoint(...)\`) is a MISSING CLIENT ELEVATION and nothing else. The
|
|
1189
|
+
alias resolves — the browser context carries every shared declaration — but only an elevated
|
|
1190
|
+
one has \`call\`. Add the bare \`elevate(list, <the alias in that call>)\` above the
|
|
1191
|
+
\`// owlmeans: add new backend elevations above this line\` sentinel in
|
|
1192
|
+
\`sources/web/src/entrypoints.ts\`. Do NOT rewrite the view model, do NOT replace the
|
|
1193
|
+
call with \`fetch\`, and do NOT add a component to a backend alias. The build was clean
|
|
1194
|
+
because the call site casts to \`ClientEntrypoint\`, so \`tsc\` will not confirm the fix —
|
|
1195
|
+
the elevation line is the fix.
|
|
1196
|
+
- A screen that renders blank is the same class of fault on the other side: the screen alias has
|
|
1197
|
+
no \`elevate(list, alias, handler(Screen))\` line. Adding a declaration without its elevation
|
|
1198
|
+
is the usual cause of both.
|
|
1199
|
+
- A header, menu or footer rendered TWICE is a screen importing its own layout. The AREA is the
|
|
1200
|
+
screen's parent entrypoint and the framework already wraps it — delete the import and the
|
|
1201
|
+
wrapper element from the screen, never the area from the entrypoint tree.
|
|
1202
|
+
- An error naming an AREA — \`app.web.area.<area>\`, its \`route(...)\`, its \`guard()\`/\`gate()\`,
|
|
1203
|
+
or one of the four layouts in \`sources/web/src/layout/area.tsx\` — means generated code
|
|
1204
|
+
EDITED something that ships with the project. RESTORE the shipped form: four areas under
|
|
1205
|
+
\`app.web.base\` at \`/\`, \`/frontoffice\`, \`/admin\` and \`/backoffice\`, each with its own guard
|
|
1206
|
+
and one \`default: true\` child. Never re-declare an area to satisfy the error, never add a
|
|
1207
|
+
fifth, and never move a screen's access up onto the area — a screen that needs different
|
|
1208
|
+
access belongs in a different area.
|
|
1209
|
+
- A user who WAS granted a permission and still gets 403 is almost always an \`@\` in the stored
|
|
1210
|
+
permission NAME. The gate splits its parameter at the first \`@\`: \`'enquiry--view@enquiryId'\`
|
|
1211
|
+
in \`gate(OIDC_GATE, [...])\` is CORRECT and means "look up \`enquiry--view\`, read the resource
|
|
1212
|
+
id from the \`:enquiryId\` route param". The same string registered or granted as a permission
|
|
1213
|
+
NAME is a key nothing ever looks up, so every grant against it is a silent no-op. Fix the
|
|
1214
|
+
registration and the grant, never the gate line — and never delete the \`@\` from the gate to
|
|
1215
|
+
"make the names match". A gate whose \`@name\` is not a \`:\` segment of that entrypoint's own
|
|
1216
|
+
declared path is the other half of the same fault: it refuses every request.
|
|
1217
|
+
- A PERMISSION that no definition backs is a gate nobody can pass, and there are two ways generated
|
|
1218
|
+
code gets one. A bracketed word — \`gate(OIDC_GATE, ['<permission>'])\` — is an instruction's
|
|
1219
|
+
placeholder written out as if it were a name. A foreign name — \`article--modify\` in an
|
|
1220
|
+
application that has no articles — is an example copied from a comment or a skill. Both look
|
|
1221
|
+
perfectly valid to the compiler and to the boot check, so nothing reports them; the app simply
|
|
1222
|
+
refuses those requests forever. Replace the string with a permission built from THIS project's own
|
|
1223
|
+
resource and action, spelling it exactly as the project's other declarations spell that capability
|
|
1224
|
+
— never delete the gate to make the error go away, and never invent a definition to match a name
|
|
1225
|
+
that should not have been written.
|
|
1226
|
+
- A whole area rendering BLANK is its missing \`default: true\` child: an entrypoint with
|
|
1227
|
+
children and no default child matches nothing. Restore the default child; do not give the area
|
|
1228
|
+
a component of its own.
|
|
1229
|
+
- \`'X' does not exist in type 'Y'\` means the SHAPE you wrote is wrong, not that the
|
|
1230
|
+
feature is impossible. When a TYPE_DECLARATIONS block is present in the task, it is the
|
|
1231
|
+
ground truth — rewrite the object or call to match it exactly, instead of renaming the
|
|
1232
|
+
property to another guess or deleting the code.
|
|
1233
|
+
- If the same error survives a fix, the fix was wrong. Re-read the message rather than
|
|
1234
|
+
applying a larger version of the same change.
|
|
1235
|
+
- NEVER make a handler or a function stop doing its job to make the build pass. Returning
|
|
1236
|
+
\`{ items: [], total: 0 }\`, an empty object, a hard-coded literal or \`res.sendStatus(204)\`
|
|
1237
|
+
in place of the real call is not a fix — it turns a loud build error into a silent wrong
|
|
1238
|
+
answer nobody will notice. If a call does not type-check, correct the ARGUMENTS to match
|
|
1239
|
+
the function's real signature; the imports and the call must survive the fix.
|
|
1240
|
+
- A runtime \`SyntaxError: Service not found <alias>\` from \`context.serviceRoute\` is a
|
|
1241
|
+
DIFFERENT error from the one below, and the difference decides the repair. It means an
|
|
1242
|
+
ENTRYPOINT alias (\`app.web.*\`, \`app.api.*\`) was passed where a SERVICE alias is required.
|
|
1243
|
+
The registered ones are \`APP_WEB\` and \`APP_API\` from \`sources/common/src/consts.ts\`, and
|
|
1244
|
+
the REGISTERED SERVICE ALIASES block in this task lists what this project actually declares.
|
|
1245
|
+
**Substituting another \`app.*\` value is what makes this loop circle** — it has already cost
|
|
1246
|
+
one project twenty attempts, each one reporting success and changing only which name the error
|
|
1247
|
+
prints. The two files that can produce it, \`config.ts\` and \`owlmeans.ts\`, are framework
|
|
1248
|
+
wiring: they are refused to the write tools, and \`restore_wiring_file\` is the repair.
|
|
1249
|
+
- A runtime \`Service <alias> not found\` means generated code called
|
|
1250
|
+
\`owlCtx.service('<alias>')\` for something nothing registers. Two causes, both common:
|
|
1251
|
+
a hand-written service object in \`src/models/**\`, or a RESOURCE alias asked for on the
|
|
1252
|
+
service registry (\`ctx.service('reservation')\` — use the resource accessor instead,
|
|
1253
|
+
\`reservationResource()\` from \`@/resources/reservation/reservation.js\`, and drop the
|
|
1254
|
+
\`ctx\` parameter the function only needed for the lookup). **Do NOT create a service to fix it, and do NOT add a
|
|
1255
|
+
file under \`src/services/\`.** Delete the fake service object and the lookup, EXPORT the
|
|
1256
|
+
functions it wrapped from the same model file (dropping the \`ctx\` parameter they never
|
|
1257
|
+
needed), and import those functions directly in the handler. TypeScript cannot see the
|
|
1258
|
+
original error — the code compiles — but it does see the follow-ups, so expect
|
|
1259
|
+
\`declares 'X' locally, but it is not exported\` next and fix it by exporting, never by
|
|
1260
|
+
reintroducing a service.
|
|
1261
|
+
- A runtime \`Resource <alias> not found\` means the resource module is missing from the
|
|
1262
|
+
generated \`src/resources/index.ts\`. Report it; never hand-register a resource and never
|
|
1263
|
+
edit a generated registry.
|
|
1264
|
+
`),
|
|
1265
|
+
skill(ViableSkill.LayoutDefinition, 'What "layout" means here', `
|
|
1266
|
+
A layout is one of the FOUR AREA shells this project already ships, all in one file —
|
|
1267
|
+
\`sources/web/src/layout/area.tsx\`: \`GuestLayout\`, \`UserLayout\`, \`AdminLayout\`,
|
|
1268
|
+
\`OperatorLayout\`. They carry the chrome of the application — header, section menu, side menu,
|
|
1269
|
+
content region, footer — and each is a thin wrapper over \`NavLayout\` from
|
|
1270
|
+
\`@owlmeans/web-panel\`, which builds both menu levels from \`sources/web/src/nav.ts\`.
|
|
1271
|
+
|
|
1272
|
+
A layout is mounted as the PARENT entrypoint of its area, never imported by a screen. The
|
|
1273
|
+
framework renders the matched screen INTO it as \`children\`, so a layout is
|
|
1274
|
+
\`FC<PropsWithChildren>\` and puts \`{children}\` where the screen belongs. It knows nothing about
|
|
1275
|
+
which screen that is.
|
|
1276
|
+
|
|
1277
|
+
A generated project never creates a layout. There are four and there will only ever be four:
|
|
1278
|
+
customise one by RESTYLING it — its title, its classes, its header actions — never by writing a
|
|
1279
|
+
fifth shell and never by rebuilding the navigation inside it by hand.
|
|
1280
|
+
`),
|
|
1281
|
+
skill(ViableSkill.ScreenDefinition, 'What "screen" means here', `
|
|
1282
|
+
A screen is the React component that arranges other components and their layout inside
|
|
1283
|
+
the page. It may also switch between the components it contains. It never contains
|
|
1284
|
+
general chrome — no header, no footer, no navigation, no menu, no sidebar.
|
|
1285
|
+
|
|
1286
|
+
A screen NEVER imports and NEVER renders a layout component. Its layout is the AREA it is
|
|
1287
|
+
declared under — \`frontend({ parent: app.web.area.<area> })\` — and the framework wraps the
|
|
1288
|
+
screen in it. A screen that wraps itself renders the whole chrome twice, nested.
|
|
1289
|
+
|
|
1290
|
+
A screen's declared path is the TAIL ONLY: \`'/tasks'\`, never \`'/frontoffice/tasks'\`. The area
|
|
1291
|
+
contributes the prefix, so repeating it publishes the screen at a doubled URL nothing links to.
|
|
1292
|
+
`),
|
|
1293
|
+
skill(ViableSkill.QueueDiscipline, 'When work belongs off the request path', `
|
|
1294
|
+
Almost never. Start from "this is a request handler and a table" and stay there unless one of
|
|
1295
|
+
the reasons below is TRUE of this feature. A queue turns one request into two processes and a
|
|
1296
|
+
message that can be delivered twice; a worker is a second thing that can be down; an LLM agent
|
|
1297
|
+
turns a click into a bill. None of that is free, and none of it is undone easily.
|
|
1298
|
+
|
|
1299
|
+
## Reasons that are real
|
|
1300
|
+
- The work calls an LLM, or any third-party API that is slow or rate-limited.
|
|
1301
|
+
- The work walks an unbounded set — every row a user owns, every file in an upload.
|
|
1302
|
+
- The work must survive the user closing the tab: a long import, a generated report.
|
|
1303
|
+
- The work is scheduled or repeated rather than requested.
|
|
1304
|
+
- The work must be retried on failure without the user doing anything.
|
|
1305
|
+
|
|
1306
|
+
## Reasons that are NOT real
|
|
1307
|
+
- "It might be slow one day." Measure first; moving it later is a small change.
|
|
1308
|
+
- "It writes to several tables." That is a transaction, not a job.
|
|
1309
|
+
- "It sends one email." One outbound call in a request is fine.
|
|
1310
|
+
- "It feels like background work." Feelings are not a reason; name the property.
|
|
1311
|
+
- "It is complicated." Complexity belongs in a function, not in another process.
|
|
1312
|
+
|
|
1313
|
+
## If the answer is yes
|
|
1314
|
+
Say WHY in one sentence naming the property above, and say how the processor is safe to run
|
|
1315
|
+
twice — a worker can die mid-job, the lock expires and the step re-runs. There is no way to
|
|
1316
|
+
make that automatic. "Skip rows already marked done" and "delete what a previous attempt
|
|
1317
|
+
created before recreating it" are answers; "it should be fine" is not.
|
|
1318
|
+
|
|
1319
|
+
## And say who watches it
|
|
1320
|
+
A queued job has no screen and no session. Name the story whose screen shows its progress, or
|
|
1321
|
+
the work is invisible and the user is left pressing a button that appears to do nothing.
|
|
1322
|
+
`),
|
|
1323
|
+
skill(ViableSkill.WorkerJobs, 'Queues, jobs and processors', `
|
|
1324
|
+
Three files have to agree, and a job that exists in two of the three is worse than one that
|
|
1325
|
+
exists in none — a declared name nothing processes is a message that piles up, and a processor
|
|
1326
|
+
with no declaration is dead code the barrel still imports.
|
|
1327
|
+
|
|
1328
|
+
## 1. The queue — \`sources/backend/src/jobs/index.ts\`
|
|
1329
|
+
A queue is an ADDRESS: it says what exists and which job names it accepts. Both the api (which
|
|
1330
|
+
enqueues) and the worker (which consumes) read this one list, and a job name the queue does not
|
|
1331
|
+
declare is refused at enqueue time.
|
|
1332
|
+
|
|
1333
|
+
\`\`\`ts
|
|
1334
|
+
export const queues: QueueDeclaration[] = [
|
|
1335
|
+
{ name: APP_QUEUE, jobs: [app.job.test, app.job.<name>],
|
|
1336
|
+
worker: { concurrency: 4, lockDuration: 60_000 } },
|
|
1337
|
+
]
|
|
1338
|
+
\`\`\`
|
|
1339
|
+
|
|
1340
|
+
## 2. The alias and the entrypoint — \`sources/common/src\`
|
|
1341
|
+
The job's name IS its entrypoint alias. Declare \`app.job.<name>\` in \`consts.ts\` above the
|
|
1342
|
+
sentinel, and the entrypoint in \`entrypoints.ts\` with \`job()\` from \`@owlmeans/route\`.
|
|
1343
|
+
|
|
1344
|
+
## 3. The processor — \`sources/worker/src/jobs/<name>.ts\`
|
|
1345
|
+
A plain async function wrapped in \`handleRequest\` / \`handleBody<T>\` / \`handleParams<T>\`,
|
|
1346
|
+
exactly like an endpoint handler. It RETURNS its result; throwing a \`ResilientError\` subclass
|
|
1347
|
+
is how a refusal is reported, and the class survives the broker.
|
|
1348
|
+
|
|
1349
|
+
Two rules with no equivalent on the HTTP side:
|
|
1350
|
+
- **Call \`job.touch()\` inside every long loop.** The broker judges liveness by the lock, and
|
|
1351
|
+
silence for longer than \`lockDuration\` is indistinguishable from a dead worker — the job is
|
|
1352
|
+
handed to somebody else and the work runs twice.
|
|
1353
|
+
- **A processor must be safe to run twice.** Skip what a previous attempt recorded, or delete
|
|
1354
|
+
what it created, and say in a comment which of the two this one does.
|
|
1355
|
+
|
|
1356
|
+
Elevate it in \`sources/worker/src/entrypoints.ts\` above the sentinel. Enqueue from an endpoint
|
|
1357
|
+
with \`context.jobs().create({ name: app.job.<name>, data })\`.
|
|
1358
|
+
`),
|
|
1359
|
+
skill(ViableSkill.TargetAgents, 'LLM agents inside the application', `
|
|
1360
|
+
An agent generated into the application is an \`@owlmeans/agent\` model, built in the shared
|
|
1361
|
+
backend package and RUN FROM A JOB — never on the request path. A model call takes seconds to
|
|
1362
|
+
minutes and costs money per attempt; holding a request open for it gives the user a timeout and
|
|
1363
|
+
the operator a bill with no result attached.
|
|
1364
|
+
|
|
1365
|
+
- \`makeAgentModel({ exec, tools, ... })\` for a tool loop that converses.
|
|
1366
|
+
- \`makePipeline(spec, steps)\` for ordered, resumable steps whose position must survive a crash.
|
|
1367
|
+
Pipeline state holds KEYS, never artifacts — a step writes its output somewhere and puts the
|
|
1368
|
+
id in the state.
|
|
1369
|
+
|
|
1370
|
+
The agent module goes in \`sources/backend/src/agents/<alias>.ts\` and is registered in that
|
|
1371
|
+
directory's generated barrel. The endpoint the user presses enqueues the job; the job invokes
|
|
1372
|
+
the agent; the screen watches the job. Never import an agent from \`sources/api\`.
|
|
1373
|
+
`),
|
|
1374
|
+
skill(ViableSkill.AgenticChoice, 'A call, a pipeline, or an agent', `
|
|
1375
|
+
Three shapes can perform work with a model, and they are not interchangeable. Pick the
|
|
1376
|
+
SIMPLEST one that does the job, and increase complexity only when the simpler shape provably
|
|
1377
|
+
cannot. Every step up costs latency, money and a failure mode.
|
|
1378
|
+
|
|
1379
|
+
**A call** — one prompt, one answer. The default, and the right answer far more often than it
|
|
1380
|
+
is chosen. Summarise, classify, extract, rewrite, draft, translate, answer a question about
|
|
1381
|
+
text you already have. If you can write down the prompt, it is a call.
|
|
1382
|
+
|
|
1383
|
+
**A pipeline** — fixed, ordered steps, decided by you and not by the model. Choose it when the
|
|
1384
|
+
work decomposes cleanly into subtasks you can NAME IN ADVANCE, and each step's output is the
|
|
1385
|
+
next step's input: transcribe then summarise then file; extract then validate then store. You
|
|
1386
|
+
are trading latency for accuracy, and you know the number of steps before you start.
|
|
1387
|
+
|
|
1388
|
+
**An agent** — the model chooses its own path through tools, and you cannot say in advance how
|
|
1389
|
+
many steps it will take. Choose it ONLY when all of these are true:
|
|
1390
|
+
- the number of steps genuinely cannot be predicted;
|
|
1391
|
+
- which tool to use next is a judgement, not a rule you could write down;
|
|
1392
|
+
- the input is unstructured and the decision is contextual;
|
|
1393
|
+
- and the work runs in a place where an unpredictable number of tool calls is acceptable.
|
|
1394
|
+
|
|
1395
|
+
Not reasons to choose an agent: the task is "complex"; it uses more than one piece of data; it
|
|
1396
|
+
sounds impressive; a tool exists. A rules engine with a model in it is a pipeline. An app that
|
|
1397
|
+
calls a model but does not let the model direct the work is not an agent at all.
|
|
1398
|
+
|
|
1399
|
+
If you are unsure between two of them, take the simpler one. A call that turns out to need a
|
|
1400
|
+
second step becomes a pipeline with one edit; an agent that never needed to be one is a bill
|
|
1401
|
+
nobody can explain.
|
|
1402
|
+
|
|
1403
|
+
Whichever shape it is, it runs off the request path — a model call is seconds to minutes and
|
|
1404
|
+
costs money per attempt, and holding a request open for it gives the user a timeout and the
|
|
1405
|
+
operator a bill with no result attached. So it is ALWAYS reached through something a person does:
|
|
1406
|
+
somebody starts it, and somebody watches it finish. Work with no human half is work nobody can
|
|
1407
|
+
see, cancel or be told about, and it reaches the user as a button that appears to do nothing.
|
|
1408
|
+
|
|
1409
|
+
Describe that work INSIDE the story of the person who starts it — "I request a write-up and come
|
|
1410
|
+
back to read it" — rather than as a story of its own told in the machine's voice. A machine has
|
|
1411
|
+
no account to sign in with, no screen of its own and no permission that can be granted to it, so
|
|
1412
|
+
"As an AI assistant, I want to open the queue…" is a story whose actor can never use what gets
|
|
1413
|
+
built for it.
|
|
1414
|
+
`),
|
|
1415
|
+
skill(ViableSkill.TargetLlm, 'Calling a model from the application', `
|
|
1416
|
+
Model calls happen in \`sources/backend\` and are invoked from a job in \`sources/worker\`.
|
|
1417
|
+
NEVER from \`sources/web\` — a browser bundle cannot hold a provider key — and never inline in
|
|
1418
|
+
a request handler in \`sources/api\`.
|
|
1419
|
+
|
|
1420
|
+
The key is the user's, not the platform's, and it may be absent. Follow the shape
|
|
1421
|
+
\`sources/backend/src/config.ts\` already uses for the database and the queue:
|
|
1422
|
+
|
|
1423
|
+
\`\`\`ts
|
|
1424
|
+
export const anthropicKey = process.env.ANTHROPIC_API_KEY ?? ''
|
|
1425
|
+
export const llmConfigured = anthropicKey !== ''
|
|
1426
|
+
\`\`\`
|
|
1427
|
+
|
|
1428
|
+
- Never throw at module scope when it is missing. The application must still boot, exactly as
|
|
1429
|
+
it does without \`VALKEY_URL\`.
|
|
1430
|
+
- The endpoint that would use it answers a refusal explaining that a model API key has not been
|
|
1431
|
+
configured yet — not a 500, and not silence.
|
|
1432
|
+
- Declare the variable so the platform can ask the owner for it; do not invent a default.
|
|
1433
|
+
|
|
1434
|
+
Give every call a bounded output and treat a refusal or an empty answer as an outcome the code
|
|
1435
|
+
handles, never as an exception that reaches the user.
|
|
1436
|
+
|
|
1437
|
+
Where the answer has a SHAPE, describe it with the same AJV schema style the rest of this
|
|
1438
|
+
application uses — the one already exported beside each type. Do not reach for \`zod\`: it is not
|
|
1439
|
+
a dependency of this project, and importing it fails the build with \`TS2307\`.
|
|
1440
|
+
`),
|
|
1441
|
+
skill(ViableSkill.TargetAgentTools, 'Tools and skills for the application’s agent', `
|
|
1442
|
+
An agent is only as good as the tools it is given, and a bloated tool set is the most common
|
|
1443
|
+
way to make one worse.
|
|
1444
|
+
|
|
1445
|
+
- Give it the FEWEST tools that can complete the work. If you cannot say which of two tools the
|
|
1446
|
+
agent should reach for in a given situation, neither can it.
|
|
1447
|
+
- Name each tool for what it accomplishes, not for the endpoint behind it, and describe it in
|
|
1448
|
+
one sentence that says WHEN to use it.
|
|
1449
|
+
- Return human-readable results. An agent reasons better over a name than over a row id, and a
|
|
1450
|
+
large result should be filtered or truncated by the tool rather than by the model.
|
|
1451
|
+
- A tool must never reject. Catch inside it and return the failure as text the agent can act
|
|
1452
|
+
on; a thrown tool call aborts the whole turn.
|
|
1453
|
+
|
|
1454
|
+
The application's own \`.agents/skills/\` directory is loadable into its agent's prompt. Where
|
|
1455
|
+
guidance is long or situational, write it as a skill and let the agent read it when it needs
|
|
1456
|
+
it, rather than pasting it into the system prompt where it is paid for on every call.
|
|
1457
|
+
`),
|
|
1458
|
+
skill(ViableSkill.GameDesign, 'What a game brief is made of', `
|
|
1459
|
+
A game is not a business flow, and analysing it as one produces a menu with nothing behind it.
|
|
1460
|
+
|
|
1461
|
+
Describe, in this order:
|
|
1462
|
+
1. **The core loop** — the one thing the player does over and over, in a sentence. "Steer, dodge,
|
|
1463
|
+
collect." "Place a tile, score the line."
|
|
1464
|
+
2. **The win and lose conditions** — how a session ends, both ways. A game with no end is a toy.
|
|
1465
|
+
3. **The controls** — what input does what. Keyboard, pointer, touch.
|
|
1466
|
+
4. **Progression** — what changes between the first minute and the tenth: speed, levels, score,
|
|
1467
|
+
unlocks. One axis is enough.
|
|
1468
|
+
5. **What is persisted** — usually a score, a run history, a player profile. Almost never the
|
|
1469
|
+
frame-by-frame state of a session.
|
|
1470
|
+
|
|
1471
|
+
Steps that are NOT part of a game brief: sign-up, settings, billing, admin. They exist, they are
|
|
1472
|
+
ordinary screens, and they are not the product.
|
|
1473
|
+
|
|
1474
|
+
Keep it to one game. A brief that describes a platform of several games describes none of them.
|
|
1475
|
+
`),
|
|
1476
|
+
skill(ViableSkill.GameScene, 'The three.js scene component', `
|
|
1477
|
+
Exactly ONE component owns the 3D scene. It creates the renderer, the camera and the scene,
|
|
1478
|
+
runs the animation loop, and tears all of it down again.
|
|
1479
|
+
|
|
1480
|
+
\`\`\`tsx
|
|
1481
|
+
// right — the loop and the renderer live in one effect, and it cleans up after itself
|
|
1482
|
+
useEffect(() => {
|
|
1483
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
|
1484
|
+
mount.current!.appendChild(renderer.domElement)
|
|
1485
|
+
let frame = 0
|
|
1486
|
+
const tick = () => { frame = requestAnimationFrame(tick); renderer.render(scene, camera) }
|
|
1487
|
+
tick()
|
|
1488
|
+
return () => {
|
|
1489
|
+
cancelAnimationFrame(frame)
|
|
1490
|
+
renderer.dispose()
|
|
1491
|
+
renderer.domElement.remove()
|
|
1492
|
+
}
|
|
1493
|
+
}, [])
|
|
1494
|
+
\`\`\`
|
|
1495
|
+
|
|
1496
|
+
Rules that are not optional:
|
|
1497
|
+
- **Never drive the animation loop from React state.** A \`setState\` per frame re-renders the
|
|
1498
|
+
tree sixty times a second and the game stutters. Mutate the object3D directly in the loop and
|
|
1499
|
+
publish to React only what the interface shows — a score, a life count — and only when it
|
|
1500
|
+
changes.
|
|
1501
|
+
- **Dispose what you create.** Geometries, materials and the renderer all hold GPU memory that
|
|
1502
|
+
unmounting does not release. A scene mounted and unmounted a few times without disposal
|
|
1503
|
+
exhausts the context and the canvas goes black.
|
|
1504
|
+
- **Resize is an event, not a render.** Update \`camera.aspect\`, call
|
|
1505
|
+
\`camera.updateProjectionMatrix()\` and \`renderer.setSize(...)\` from a resize listener.
|
|
1506
|
+
- **Geometry is generated, never loaded.** Compose it from the built-in geometries and simple
|
|
1507
|
+
materials — box, sphere, cylinder, plane, lathe, extrude. No textures, no image files, no
|
|
1508
|
+
external model formats. Colour, light and shape carry the whole look.
|
|
1509
|
+
`),
|
|
1510
|
+
skill(ViableSkill.GameUi, 'The interface is React, above the canvas', `
|
|
1511
|
+
Everything a player reads or presses — score, menus, dialogs, buttons, settings — is an
|
|
1512
|
+
ordinary React component rendered ABOVE the canvas, using the same shadcn primitives as the rest
|
|
1513
|
+
of the application. Nothing is drawn as text inside the 3D scene.
|
|
1514
|
+
|
|
1515
|
+
\`\`\`tsx
|
|
1516
|
+
// right — the canvas fills the frame, the interface floats over it
|
|
1517
|
+
<div className="relative h-full w-full">
|
|
1518
|
+
<div ref={mount} className="absolute inset-0" />
|
|
1519
|
+
<div className="pointer-events-none absolute inset-0 p-4">
|
|
1520
|
+
<ScoreBadge value={score} />
|
|
1521
|
+
<div className="pointer-events-auto"><Button onClick={pause}>Pause</Button></div>
|
|
1522
|
+
</div>
|
|
1523
|
+
</div>
|
|
1524
|
+
\`\`\`
|
|
1525
|
+
|
|
1526
|
+
- The overlay container carries \`pointer-events-none\` so clicks reach the canvas; each
|
|
1527
|
+
interactive control turns them back on with \`pointer-events-auto\`. Forgetting this makes the
|
|
1528
|
+
game unplayable — every drag lands on an invisible div.
|
|
1529
|
+
- Text rendered into the scene cannot be selected, translated, scaled by the browser or read by
|
|
1530
|
+
a screen reader. Use it for nothing that matters.
|
|
1531
|
+
- SVG is the second half of the art: icons, badges, backgrounds and 2D games are inline SVG
|
|
1532
|
+
written by hand. No raster images anywhere.
|
|
1533
|
+
`),
|
|
1534
|
+
skill(ViableSkill.GameNetworking, 'Who owns the state', `
|
|
1535
|
+
Three kinds of game, and the difference is entirely about where the state lives.
|
|
1536
|
+
|
|
1537
|
+
**Casual** — one player, one browser. The session runs entirely on the client; the server sees
|
|
1538
|
+
only what is worth keeping: a final score, a run record, a profile. No queue, no worker, no
|
|
1539
|
+
socket. This is most games, and it is the right answer unless the brief asks otherwise.
|
|
1540
|
+
|
|
1541
|
+
**Online, turn-based** — several players acting one after another, minutes or days apart. The
|
|
1542
|
+
match is an ordinary record and every move is an ordinary endpoint that validates it and writes
|
|
1543
|
+
the next state. Anything slow that follows a move — scoring a finished match, notifying the next
|
|
1544
|
+
player, rebuilding a leaderboard — is a job. No realtime anything.
|
|
1545
|
+
|
|
1546
|
+
**Online, live** — several players acting at once, and the server simulates.
|
|
1547
|
+
- The client sends INTENT ("move forward", "fire"), never an outcome ("I am at x=12", "I hit
|
|
1548
|
+
them"). A client that reports outcomes is a client that decides them, and one player's browser
|
|
1549
|
+
then decides everybody's game.
|
|
1550
|
+
- The server holds the authoritative state, advances it on a fixed tick, and broadcasts it.
|
|
1551
|
+
- The client may predict its own movement locally so it feels immediate, and corrects when the
|
|
1552
|
+
server's answer disagrees. Other players' entities are interpolated between the last two
|
|
1553
|
+
updates rather than snapped.
|
|
1554
|
+
- Anything the outcome depends on — damage, scoring, currency, who won — is decided on the
|
|
1555
|
+
server and nowhere else.
|
|
1556
|
+
`),
|
|
1557
|
+
libraries(ViableSkill.LibrariesCommon, 'Libraries — shared code', commonLibraryList),
|
|
1558
|
+
libraries(ViableSkill.LibrariesUiState, 'Libraries — UI state', uiStateLibraryList),
|
|
1559
|
+
libraries(ViableSkill.LibrariesUi, 'Libraries — UI', fullUiLibraryList),
|
|
1560
|
+
libraries(ViableSkill.LibrariesBackend, 'Libraries — backend', backendLibraryList),
|
|
1561
|
+
libraries(ViableSkill.LibrariesAi, 'Libraries — model calls and agents', aiLibraryList),
|
|
1562
|
+
libraries(ViableSkill.LibrariesGame, 'Libraries — game', gameLibraryList),
|
|
1563
|
+
];
|
|
1564
|
+
/** Look one up by alias — for the places that still splice a rule into a task prompt. */
|
|
1565
|
+
export const viableSkill = (alias) => VIABLE_SKILLS.find(entry => entry.alias === alias);
|
|
1566
|
+
//# sourceMappingURL=catalogue.js.map
|