@owlmeans/create-app 0.1.18-rc.7 → 0.1.18
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 +49 -5
- package/THIRD-PARTY-NOTICES.txt +24 -0
- package/build/args/types.d.ts +9 -0
- package/build/args/types.d.ts.map +1 -0
- package/build/args/types.js +2 -0
- package/build/args/types.js.map +1 -0
- package/build/args.d.ts +3 -20
- package/build/args.d.ts.map +1 -1
- package/build/args.js +121 -88
- package/build/args.js.map +1 -1
- package/build/bin.js +3 -3
- package/build/bin.js.map +1 -1
- package/build/consts.d.ts +6 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +6 -0
- package/build/consts.js.map +1 -0
- package/build/consts.local.d.ts +12 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +49 -0
- package/build/consts.local.js.map +1 -0
- package/build/index.d.ts +11 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -2
- package/build/index.js.map +1 -1
- package/build/naming/types.d.ts +14 -0
- package/build/naming/types.d.ts.map +1 -0
- package/build/naming/types.js +2 -0
- package/build/naming/types.js.map +1 -0
- package/build/naming.d.ts +4 -0
- package/build/naming.d.ts.map +1 -0
- package/build/naming.js +18 -0
- package/build/naming.js.map +1 -0
- package/build/run.d.ts +1 -1
- package/build/run.d.ts.map +1 -1
- package/build/run.js +23 -17
- package/build/run.js.map +1 -1
- package/build/scaffold.d.ts +8 -0
- package/build/scaffold.d.ts.map +1 -0
- package/build/scaffold.js +19 -0
- package/build/scaffold.js.map +1 -0
- package/build/template/types.d.ts +16 -0
- package/build/template/types.d.ts.map +1 -0
- package/build/template/types.js +2 -0
- package/build/template/types.js.map +1 -0
- package/build/template.d.ts +3 -16
- package/build/template.d.ts.map +1 -1
- package/build/template.js +69 -52
- package/build/template.js.map +1 -1
- package/build/types.d.ts +61 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +8 -6
- package/template/AGENTS.md +61 -2
- package/template/README.bare.md +70 -0
- package/template/README.md +9 -60
- package/template/_agents/scripts/link-skills.sh +327 -15
- package/template/_agents/skills/agent-memory/SKILL.md +12 -11
- package/template/_agents/skills/getting-started/SKILL.md +38 -79
- package/template/_agents/skills/logging/SKILL.md +41 -0
- package/template/_agents/skills/memory-promotion/SKILL.md +10 -4
- package/template/_agents/skills/memory-recompact/SKILL.md +15 -14
- package/template/_agents/skills/owlmeans-code-structure/SKILL.md +188 -0
- package/template/_agents/skills/owlmeans-code-structure/parts/code-structure-reference.md +125 -0
- package/template/_agents/skills/reuse-code/SKILL.md +56 -10
- package/template/_agents/skills/self-education/SKILL.md +25 -5
- package/template/_agents/skills/skill-authoring/SKILL.md +132 -14
- package/template/_bare.json +24 -0
- package/template/_gitignore +8 -0
- package/template/bunfig.toml +2 -0
- package/template/package.json +3 -2
- package/template/sources/api/package.json +5 -4
- package/template/sources/api/src/app/session/add.ts +6 -17
- package/template/sources/api/src/app/session/list.ts +6 -12
- package/template/sources/api/src/app/session/remove.ts +6 -15
- package/template/sources/api/src/config.ts +7 -0
- package/template/sources/api/src/context.bare.ts +11 -0
- package/template/sources/api/src/context.ts +0 -4
- package/template/sources/api/src/entrypoints.bare.ts +4 -0
- package/template/sources/api/src/entrypoints.ts +13 -0
- package/template/sources/api/src/index.ts +8 -2
- package/template/sources/api/src/models/session/types.ts +14 -0
- package/template/sources/api/src/models/session.ts +43 -0
- package/template/sources/api/src/types.bare.ts +5 -0
- package/template/sources/common/package.json +8 -8
- package/template/sources/common/src/config.ts +2 -1
- package/template/sources/common/src/consts.bare.ts +8 -0
- package/template/sources/common/src/consts.ts +0 -13
- package/template/sources/common/src/entrypoints.bare.ts +14 -0
- package/template/sources/common/src/entrypoints.ts +60 -0
- package/template/sources/common/src/index.bare.ts +3 -0
- package/template/sources/common/src/index.ts +1 -1
- package/template/sources/common/src/schemas.ts +7 -7
- package/template/sources/web/THIRD-PARTY-NOTICES.txt +24 -0
- package/template/sources/web/index.html +3 -1
- package/template/sources/web/package.json +33 -31
- package/template/sources/web/src/components/ui/dropdown-menu.tsx +257 -0
- package/template/sources/web/src/components/ui/navigation-menu.tsx +169 -0
- package/template/sources/web/src/config.ts +8 -0
- package/template/sources/web/src/consts.ts +15 -0
- package/template/sources/web/src/context.bare.ts +13 -0
- package/template/sources/web/src/context.ts +1 -7
- package/template/sources/web/src/entrypoints.bare.ts +13 -0
- package/template/sources/web/src/entrypoints.ts +16 -0
- package/template/sources/web/src/index.css +7 -0
- package/template/sources/web/src/index.tsx +2 -2
- package/template/sources/web/src/layout/main.tsx +17 -19
- package/template/sources/web/src/nav.bare.ts +24 -0
- package/template/sources/web/src/nav.ts +23 -0
- package/template/sources/web/src/screens/about.tsx +19 -0
- package/template/sources/web/src/screens/home.bare.tsx +17 -0
- package/template/sources/web/src/screens/session.tsx +14 -16
- package/template/sources/web/src/vite-env.d.ts +8 -0
- package/template/sources/web/vite.config.ts +4 -2
- package/template/CLAUDE.md +0 -13
- package/template/sources/api/src/modules.ts +0 -11
- package/template/sources/common/src/modules.ts +0 -26
- package/template/sources/web/src/components/nav/main.tsx +0 -15
- package/template/sources/web/src/modules.ts +0 -20
|
@@ -1,105 +1,64 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: getting-started
|
|
3
|
-
description:
|
|
4
|
-
user-invocable: true
|
|
3
|
+
description: Start a protocol-first OwlMeans application with shared contracts, server entrypoints and browser entrypoints.
|
|
5
4
|
metadata:
|
|
6
5
|
scope: general
|
|
7
6
|
---
|
|
8
7
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
8
|
|
|
10
|
-
#
|
|
9
|
+
# Protocol-first application shape
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
An OwlMeans application owns one shared protocol declaration and creates local entrypoints for each
|
|
12
|
+
runtime. The declaration is never modified by a server or browser.
|
|
13
13
|
|
|
14
|
+
```ts
|
|
15
|
+
// common/src/entrypoints.ts
|
|
16
|
+
export const sessionProtocols = {
|
|
17
|
+
list: protocol(
|
|
18
|
+
route(session.list, '/session', backend()),
|
|
19
|
+
contract.request({ query: typed<SessionQuery>(SessionQuerySchema) }, typed<Session[]>())
|
|
20
|
+
),
|
|
21
|
+
}
|
|
14
22
|
```
|
|
15
|
-
sources/
|
|
16
|
-
├── common/ # shared entrypoints (routes), AJV schemas, types, config — the single source of truth
|
|
17
|
-
├── api/ # @owlmeans/server-app backend; handlers attached to the shared entrypoints
|
|
18
|
-
└── web/ # @owlmeans/web-panel + shadcn UI; screens attached to the same entrypoints
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The full, runnable walkthrough (scaffolded **and** manual) lives in
|
|
22
|
-
[`docs/getting-started.md`](../../../docs/getting-started.md). To generate this exact project, use
|
|
23
|
-
[[scaffolding]] (`npm create @owlmeans/app`). This skill is the mental model.
|
|
24
|
-
|
|
25
|
-
## The core idea: one contract, two sides
|
|
26
|
-
|
|
27
|
-
Declare each route once in `common` as an **entrypoint**, then `elevate()` it on each side:
|
|
28
23
|
|
|
29
24
|
```ts
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
entrypoint(route(session.add, '/:sid/items', { parent: session.base, method: RouteMethod.POST }),
|
|
36
|
-
filter(params<SessionParams>(SessionParamsSchema, body<AddItemPayload>(AddItemSchema)))),
|
|
25
|
+
// api/src/entrypoints.ts
|
|
26
|
+
const api = handlers<Context>()
|
|
27
|
+
export const serverBindings = [
|
|
28
|
+
...frameworkEntrypoints,
|
|
29
|
+
bind(sessionProtocols.list, api.request(sessionProtocols.list, listSessions)),
|
|
37
30
|
]
|
|
38
31
|
```
|
|
39
32
|
|
|
40
33
|
```ts
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
// web/src/entrypoints.ts
|
|
35
|
+
export const clientBindings = [
|
|
36
|
+
...frameworkEntrypoints,
|
|
37
|
+
...bindAll(sessionProtocols),
|
|
38
|
+
]
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
// web/modules.ts — attach screen components, plus call-only elevation for backend routes
|
|
48
|
-
elevate(modules, session.list) // callable from the client
|
|
49
|
-
modules.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
|
|
40
|
+
const sessions = await context.entrypoint(sessionProtocols.list).call({ query: { sid } })
|
|
50
41
|
```
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
## Shared config (where services live)
|
|
56
|
-
|
|
57
|
-
`common/config.ts` registers both services so the web knows where the API is. `base: 'api'`
|
|
58
|
-
prefixes API routes with `/api`:
|
|
43
|
+
Use `schema<T>(...)` or `typed<T>(...)` at the contract boundary. Bind all route parents with their
|
|
44
|
+
children. Keep organization entity values on the wire as `entitySlug`; database relations use
|
|
45
|
+
`entityId` only.
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
const cfg = service({ type: AppType.Frontend, service: APP_WEB, host: 'localhost', port: 3001 })
|
|
62
|
-
service({ type: AppType.Backend, service: APP_API, host: 'localhost', port: 3000, base: 'api' }, cfg)
|
|
63
|
-
cfg.debug = { all: true }
|
|
64
|
-
export const commonConfig = cfg
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
api: `config(APP_API, commonConfig)` (+ `cfg.port`). web: `config(APP_WEB, commonConfig)`. See [[config]].
|
|
47
|
+
## Configuration per runtime
|
|
68
48
|
|
|
69
|
-
|
|
49
|
+
The shared config in `common` (services, alias, security) is imported by both runtimes, so it
|
|
50
|
+
never reads `process.env` or `import.meta.env`, and it never sets `cfg.debug = { all: true }`.
|
|
51
|
+
Each runtime's own `config.ts` adds what depends on its environment — the log level above all
|
|
52
|
+
(`logging`, `log`), behind a type-only `import type {} from '@owlmeans/log'`:
|
|
70
53
|
|
|
71
54
|
```ts
|
|
72
|
-
// api/
|
|
73
|
-
|
|
74
|
-
appendStaticResource(context, SESSION_ITEMS) // @owlmeans/static-resource — in-memory, no DB
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Handlers use `handleRequest` / `handleBody` / `handleParams` (validated payload, then context, then
|
|
78
|
-
req). Read/write `ctx.getStaticResource<T>(alias)` — full CRUD (`get/load/list/create/save/delete`).
|
|
79
|
-
**`static-resource.list()` takes no criteria** — list all and filter in JS (e.g. by `sessionId`).
|
|
80
|
-
`main(context, appModules)` starts the server. See [[static-resource]], [[server-app]], [[resource]].
|
|
55
|
+
// api/src/config.ts — Bun runtime env
|
|
56
|
+
cfg.log = { level: process.env.LOG_LEVEL || 'info', debug: process.env.LOG_DEBUG ?? '' }
|
|
81
57
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
## Web bootstrap (shadcn)
|
|
86
|
-
|
|
87
|
-
`@owlmeans/web-panel`'s `PanelApp` is shadcn/Tailwind v4 (no MUI). The **app provides** the shadcn
|
|
88
|
-
primitives at the `@` alias — `web-panel` references `@/lib/utils` and
|
|
89
|
-
`@/components/ui/{alert,button,card,input,label,progress}`; copy those into `src/`. Render with
|
|
90
|
-
`provide` from `@owlmeans/web-client`:
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
basicRender(<PanelApp context={context} provide={provide} />)
|
|
58
|
+
// web/src/config.ts — Vite build-time env, typed in vite-env.d.ts
|
|
59
|
+
const env = import.meta.env
|
|
60
|
+
cfg.log = { level: env.VITE_LOG_LEVEL || (env.PROD ? 'info' : 'debug'), debug: env.VITE_LOG_DEBUG ?? '' }
|
|
94
61
|
```
|
|
95
62
|
|
|
96
|
-
`
|
|
97
|
-
`
|
|
98
|
-
A parent `BASE` route renders the layout via `handler(LayoutComponent)`; `HOME` is its default child.
|
|
99
|
-
Screens call the backend with `context.entrypoint(alias).call({ params, body })` → `[data, outcome]`.
|
|
100
|
-
See [[web-panel]], [[web-client]], [[shadcn-web]], [[client-entrypoint]].
|
|
101
|
-
|
|
102
|
-
## Authentication
|
|
103
|
-
|
|
104
|
-
This shape is intentionally **auth-free**. To add it: `@owlmeans/server-auth` + `@owlmeans/client-auth`
|
|
105
|
-
and `guard(...)` on entrypoints. See [[auth-protocol]], [[server-auth]], [[client-auth]].
|
|
63
|
+
The server and client contexts apply `cfg.log` themselves; application code logs through
|
|
64
|
+
`logger('<scope>')` from `@owlmeans/log`, never `console.*`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logging
|
|
3
|
+
description: Logging policy for any OwlMeans application — no bare console calls; every line through @owlmeans/log with a scope and a deliberate level; what belongs at info vs debug; never log secrets or personal content; debug off in production, with the level set per environment (a server's config file, a browser's build-time env); analytics events as a call option routed to plugins. Use when adding any log line or a catch block, debugging with prints, deciding what an operator should see in production, setting a log level for an environment, or reviewing code that calls console.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
scope: general
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# Logging — the policy
|
|
11
|
+
|
|
12
|
+
The mechanics are the `log` skill (`@owlmeans/log`). This is what to do in application code.
|
|
13
|
+
|
|
14
|
+
1. **No `console.*`.** `import { logger } from '@owlmeans/log'`, `const log = logger('<scope>')` once per
|
|
15
|
+
module, then `log.debug|info|warn|error(message, data?, options?)`. A scope is lowercase and
|
|
16
|
+
colon-separated (`orders`, `orders:import`).
|
|
17
|
+
2. **Choose the level on purpose.** `debug` is for diagnosis (per request, progress, dumps) — it is OFF
|
|
18
|
+
in production. `info` is what an operator wants to read in production: a job started or stopped, a
|
|
19
|
+
subscription or payment event, a sign-in, a server listening. `warn`: it degraded and went on.
|
|
20
|
+
`error`: the work failed. A line that fires every few seconds is `debug` or `logThrottle`d.
|
|
21
|
+
3. **Stable message, variable data.** `log.info('Order shipped', { orderId, carrier })` — not a
|
|
22
|
+
sentence assembled from values. Give a significant event a dotted name: `{ event: 'order.shipped' }`.
|
|
23
|
+
4. **Pass the `Error`.** `log.error('Import failed', error)` — never `error.message` alone, never
|
|
24
|
+
`JSON.stringify(error)`. In a browser the `Error` object must reach `console.error`; the logger does
|
|
25
|
+
that, and an error reporter hooked there keeps working.
|
|
26
|
+
5. **Never log secrets or personal content**: tokens, keys, passwords, authorization or cookie headers,
|
|
27
|
+
request or response bodies, query strings, user text (a prompt, a message, a note, a model's
|
|
28
|
+
rationale quoting it), e-mail text, file contents, environment values. Log ids, names, sizes,
|
|
29
|
+
counts, lengths — `noteLength`, not the note. A refused request is its method, path (no query),
|
|
30
|
+
status and error code/message, at debug. A catch that only logs has not told the user anything —
|
|
31
|
+
the user-facing report is a separate step (a message, a state), and `log.error` is the operator's
|
|
32
|
+
record.
|
|
33
|
+
6. **Analytics are a parameter, not a second API.** `log.info('…', data, { analytics: 'event_name' })`
|
|
34
|
+
hands the call to the registered analytics plugins (`@owlmeans/web-log` for a tag manager); add
|
|
35
|
+
`console: false` to keep it out of the log. Only for events something should count.
|
|
36
|
+
7. **The level is set per environment, never in code.** A server declares `cfg.log` leaves as config
|
|
37
|
+
file paths (a ConfigMap or a `LOG_LEVEL` environment variable); a browser takes a build-time
|
|
38
|
+
constant (`VITE_LOG_LEVEL`). Development defaults to `debug`, a production build to `info`. Do not
|
|
39
|
+
hard-code `cfg.debug = { all: true }` — `debug.all` does not control logging and must not be on in
|
|
40
|
+
production.
|
|
41
|
+
8. **Tests read the log with `memoryPlugin()`**; they do not spy on `console`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-promotion
|
|
3
|
-
description: Transform procedure-shaped or repeatedly-used memory into skills
|
|
3
|
+
description: Transform procedure-shaped or repeatedly-used memory into skills — the procedure-shape test, the mandatory distillation rewrite, promote? repeated-touch flags, over-cap trigger, update-vs-create decision, and the post-promotion pointer state. Use when memory content reads as "how to", when a promote? flag is encountered again, when writing memory-derived content into a skill, or during recompaction.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata:
|
|
6
6
|
scope: general
|
|
@@ -9,7 +9,7 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
# Memory promotion
|
|
11
11
|
|
|
12
|
-
Memory holds **facts**; **procedures** belong in skills
|
|
12
|
+
Memory holds **facts**; **procedures** belong in skills, where they auto-invoke and
|
|
13
13
|
stop consuming memory-read cycles. Promotion is how the store stays compact and the harness
|
|
14
14
|
teaches itself.
|
|
15
15
|
|
|
@@ -79,8 +79,8 @@ Evaluable by reading the file alone — no tooling required.
|
|
|
79
79
|
|
|
80
80
|
## Update vs create
|
|
81
81
|
|
|
82
|
-
**Default is update** — extend the existing skill
|
|
83
|
-
|
|
82
|
+
**Default is update** — extend the existing skill whose scope covers the activity, even
|
|
83
|
+
partially. Create a NEW skill only when:
|
|
84
84
|
|
|
85
85
|
- (a) a new subsystem or technology entered the repo;
|
|
86
86
|
- (b) an activity with no covering skill needed memory read/write more than once (a
|
|
@@ -90,6 +90,12 @@ even partially; keep both twins in sync. Create a NEW pair only when:
|
|
|
90
90
|
New skills multiply lookup cost — compactness applies to the skill population *and* to each
|
|
91
91
|
skill's body.
|
|
92
92
|
|
|
93
|
+
The covering skill may be one this project does not own — a `.agents/linked-skills/<name>` entry
|
|
94
|
+
symlinked into an upstream repo or installed package, or an installer-placed copy carrying the
|
|
95
|
+
`AUTO-GENERATED` banner. Never write the promotion into either: put the distilled rule in a local
|
|
96
|
+
skill and name it after the activity, because a local skill named after the upstream one shadows
|
|
97
|
+
that skill for the whole project (`self-education` → Skills this project does not own).
|
|
98
|
+
|
|
93
99
|
## Procedure
|
|
94
100
|
|
|
95
101
|
1. Collect the flagged / procedure-shaped memory lines.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-recompact
|
|
3
|
-
description: Recompact
|
|
3
|
+
description: Recompact a whole .agents/memory/ store — rebuild the node map from project structure, merge event-shaped records into subsystem nodes, deduplicate, enforce caps, regenerate the MEMORY.md index, and fold in memory records kept anywhere else. Use when a store degrades into event logs, when indexes bloat or conflict, or when scattered records have to become one store.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
6
|
scope: general
|
|
@@ -9,9 +9,9 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
# Memory recompaction
|
|
11
11
|
|
|
12
|
-
Whole-store maintenance for `.agents/memory/` (protocol: `agent-memory`)
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Whole-store maintenance for `.agents/memory/` (protocol: `agent-memory`), and the procedure for
|
|
13
|
+
folding records kept anywhere else into it. Store-wide rewrite — propose it when triggers appear;
|
|
14
|
+
the operator invokes it.
|
|
15
15
|
|
|
16
16
|
## When
|
|
17
17
|
|
|
@@ -20,7 +20,8 @@ it when triggers appear; the operator invokes it.
|
|
|
20
20
|
- The same fact stated in two or more nodes.
|
|
21
21
|
- More than ~20% of a node is stale `Status` content.
|
|
22
22
|
- A node's `updated:` is months behind commits touching its scope.
|
|
23
|
-
-
|
|
23
|
+
- Memory records live outside `.agents/memory/` — a second store, a per-agent directory, a stray
|
|
24
|
+
notes file → fold them in with the merge pass below.
|
|
24
25
|
|
|
25
26
|
## Build the target node map first
|
|
26
27
|
|
|
@@ -43,14 +44,14 @@ For each old file or section:
|
|
|
43
44
|
4. Procedure-shaped survivors do not enter nodes — route them to `memory-promotion`. Routing
|
|
44
45
|
means distilling them into general rules, never handing the text over verbatim.
|
|
45
46
|
|
|
46
|
-
##
|
|
47
|
+
## Folding in an outside store
|
|
47
48
|
|
|
48
|
-
1. Union
|
|
49
|
-
|
|
49
|
+
1. Union every source. Two same-named files are two drifted sources of ONE node — merge both;
|
|
50
|
+
the code-consistent version wins.
|
|
50
51
|
2. Index-only entries with no backing file: extract the fact into its node, or drop if stale.
|
|
51
|
-
3.
|
|
52
|
-
|
|
53
|
-
4. When the new store verifies (below), delete
|
|
52
|
+
3. `## Skills` / "Key Files" index sections are dropped — skills self-describe; harness layout belongs to `AGENTS.md`; a genuinely non-obvious dispatch hint goes into the
|
|
53
|
+
owning skill's `description:`, never into `AGENTS.md`.
|
|
54
|
+
4. When the new store verifies (below), delete every merged source entirely.
|
|
54
55
|
|
|
55
56
|
## Regenerate the index
|
|
56
57
|
|
|
@@ -62,9 +63,9 @@ index incrementally.
|
|
|
62
63
|
- Every node file is listed in the index; every listed node exists; every wiki-link resolves.
|
|
63
64
|
- All caps met (index ≤ 50 lines; nodes ≤ 120; entries ≤ 3 lines; Status ≤ 5 dated lines).
|
|
64
65
|
- No dates outside `Status` and `updated:`; no event-keyed filenames.
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
66
|
+
- The root `AGENTS.md` Memory section points at `.agents/memory/`.
|
|
67
|
+
- `.agents/memory/` is the only memory store left: every merged source directory or file is
|
|
68
|
+
deleted, and nothing in the harness still points at one.
|
|
68
69
|
|
|
69
70
|
## Report
|
|
70
71
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: owlmeans-code-structure
|
|
3
|
+
description: "Mandatory object and layout rules for all OwlMeans TypeScript: the functions of one domain are ONE object built by a factory; its interface is declared FIRST (interfaces, never ReturnType<typeof …>); private parts live inside the factory; the context or collaborator is bound into the factory; types, consts and code live in separate files; an object with its own types or sub-helpers gets a same-named folder. Use whenever you write or change any .ts/.tsx in an @owlmeans package, an app built on OwlMeans or a generated target: adding a function, helper, util, service, model, resource, type or constant, creating or splitting a file, or reviewing a diff."
|
|
4
|
+
metadata:
|
|
5
|
+
scope: general
|
|
6
|
+
---
|
|
7
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
8
|
+
|
|
9
|
+
# OwlMeans code structure
|
|
10
|
+
|
|
11
|
+
Every OwlMeans repo — the libraries, the platform and every generated application — holds to the
|
|
12
|
+
same shape. No tool checks it: this skill is the rule, every diff you write or review is held to it,
|
|
13
|
+
and a violation is a defect, not a style note.
|
|
14
|
+
|
|
15
|
+
## The rules
|
|
16
|
+
|
|
17
|
+
1. **One domain, one object.** The functions a file offers to other files form ONE object, built by
|
|
18
|
+
ONE factory. Two domains in a file are two files. A file that exports a single function is not a
|
|
19
|
+
bundle and may stay a plain export.
|
|
20
|
+
2. **Type first.** Declare the object's `interface` before writing the factory, in the object's
|
|
21
|
+
types file, and make the factory return it: `export const createXxxHelper = (…): XxxHelper => {…}`.
|
|
22
|
+
A type derived from the implementation — `ReturnType<typeof createXxx>`, `Parameters<typeof fn>`,
|
|
23
|
+
`typeof someObject` — never names a helper, util, service, model or resource. The contract's JSDoc
|
|
24
|
+
lives on the interface members.
|
|
25
|
+
3. **Encapsulate.** The factory's closure holds the implementation: what the interface does not name
|
|
26
|
+
is a private local. Only pure, stateless primitives may stay module-private above the factory.
|
|
27
|
+
A factory that merely collects module-level functions — `createX = () => ({ a, b })` — is the
|
|
28
|
+
thing this rule forbids. No `this`: members are arrow functions, so they can be destructured and
|
|
29
|
+
passed on.
|
|
30
|
+
4. **Bind what is shared.** When every member would take the same first argument — a context, a
|
|
31
|
+
request, a collaborator, a record — that argument goes to the factory, not to each call.
|
|
32
|
+
5. **Interfaces over types — always where possible.**
|
|
33
|
+
- `type X = { … }` → `interface X { … }`; `A & B & { … }` → `interface X extends A, B { … }`.
|
|
34
|
+
- `type X = Pick<A, 'a'>` (also `Omit`, `Partial`, `Required`, `Readonly`) → `interface X extends Pick<A, 'a'> {}`.
|
|
35
|
+
- `type Fn = (a: A) => R` → `interface Fn { (a: A): R }`.
|
|
36
|
+
- `type` stays only for what an interface cannot say: unions, mapped, conditional and
|
|
37
|
+
template-literal types, tuples, primitives, `keyof`/indexed access, and protocol trees inferred
|
|
38
|
+
from a DSL (with a one-line reason).
|
|
39
|
+
6. **Types, consts and code in separate files.** Interfaces and type aliases → `types.ts`; enums and
|
|
40
|
+
constants (including computed constant expressions such as an `Object.freeze([...LIST, ...])`) →
|
|
41
|
+
`consts.ts`; code everywhere else. Package-private declarations go to `types.local.ts` /
|
|
42
|
+
`consts.local.ts`, which no barrel re-exports.
|
|
43
|
+
7. **A same-named folder for an object that has parts.** `helpers/file.ts` holds the factory;
|
|
44
|
+
`helpers/file/` holds its `types.ts`, `consts.ts` and the sub-helpers only it uses. A directory-level
|
|
45
|
+
`types.ts` is right for declarations two or more objects share, or for a small single-owner
|
|
46
|
+
directory; a types file that serves many unrelated owners is split into their folders.
|
|
47
|
+
8. **Ready instances.** A zero-argument helper may export one ready instance as the file's last
|
|
48
|
+
statement (`export const pathHelper = createPathHelper()`). A bound object never has one — it is
|
|
49
|
+
reached through its binding target (below). Never build a bound object inside a loop or a render.
|
|
50
|
+
|
|
51
|
+
## Kinds and names
|
|
52
|
+
|
|
53
|
+
| Kind | What it is | Factory | Interface | Where |
|
|
54
|
+
|---|---|---|---|---|
|
|
55
|
+
| util | package-internal functionality, never re-exported by the package | `createXxxUtils()` / `makeXxxUtils(target)` | `XxxUtils` | `utils/xxx.ts` + `utils/xxx/` |
|
|
56
|
+
| helper | reusable functionality, may be exported, not bound to one record | `createXxxHelper(opts?)` / `makeXxxHelper(target)` | `XxxHelper` | `xxx.ts` + `xxx/` |
|
|
57
|
+
| service | lives in a context, has an alias and a lifecycle | `createXxxService(alias = DEFAULT_ALIAS)` via `createService`, plus `appendXxxService(ctx)` | `XxxService extends InitializedService` | `services/xxx.ts` + `xxx/` |
|
|
58
|
+
| model | logic over ONE plain record (plus collaborators) | `makeXxxModel(record, deps?)` | `XxxModel` | `models/xxx.ts` + `xxx/` |
|
|
59
|
+
| resource | store access and store-level queries | `makeXxxResource(…)` | `XxxResource` | `resources/xxx.ts` + `xxx/` |
|
|
60
|
+
|
|
61
|
+
`create*` builds something standalone or configured by options; `make*` builds something bound to
|
|
62
|
+
its first argument.
|
|
63
|
+
|
|
64
|
+
## How a bound object is reached
|
|
65
|
+
|
|
66
|
+
| Bound to | Shape | At the call site |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| the context, with a lifecycle, a cache or a replaceable seam | a service | `ctx.service<XxxService>(ALIAS)`, or an accessor its `append*` installs (`ctx.xxx()`) |
|
|
69
|
+
| the context of an app that owns its context type | `makeXxxHelper(ctx)`, installed lazily on the context: `context.projects = memoHelper.once(() => makeProjectHelper(context))` | `ctx.projects().getDevSlot(id)` |
|
|
70
|
+
| the context, inside a library | `makeXxx(ctx)` plus `export const xxxOf = memoHelper.oncePer(makeXxx)` | `const xxx = xxxOf(ctx)` once per function |
|
|
71
|
+
| one request | `makeRequestScope(ctx, request)` | built once at the top of a handler, never stored |
|
|
72
|
+
| a collaborator (a FileHelper, a deps bag) | `makeXxx(collaborator)`, an accessor on the collaborator or `oncePer` | `files.layout().recordDeviation(d)` |
|
|
73
|
+
| a record | a model | `makeSlotModel(slot).namespace()` |
|
|
74
|
+
|
|
75
|
+
`memoHelper` (`once`, `oncePer`) comes from `@owlmeans/context`. Tests build the object directly:
|
|
76
|
+
`makeProjectHelper(fakeContext)`.
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// helpers/slug/types.ts — the type comes first
|
|
82
|
+
export interface SlugHelper {
|
|
83
|
+
/** The URL-safe form of a title. */
|
|
84
|
+
slugOf: (title: string) => string
|
|
85
|
+
/** A title read back from a slug. */
|
|
86
|
+
titleOf: (slug: string) => string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// helpers/slug.ts
|
|
90
|
+
import type { SlugHelper } from './slug/types.js'
|
|
91
|
+
|
|
92
|
+
export const createSlugHelper = (): SlugHelper => {
|
|
93
|
+
const words = (text: string): string[] => text.split(/\W+/).filter(word => word !== '') // private
|
|
94
|
+
|
|
95
|
+
const slugOf = (title: string): string => words(title.toLowerCase()).join('-')
|
|
96
|
+
const titleOf = (slug: string): string => words(slug).join(' ')
|
|
97
|
+
|
|
98
|
+
return { slugOf, titleOf }
|
|
99
|
+
}
|
|
100
|
+
export const slugHelper = createSlugHelper()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A service follows `@owlmeans/api`'s `createApiService(alias): ApiClient` — `createService<ApiClient>`
|
|
104
|
+
with the members in place, `assertContext(client.ctx, …)` inside them, and an `appendApiClient(ctx)`
|
|
105
|
+
that registers it. A model follows `@owlmeans/planning`'s `makeWorkcardModel(record, facade):
|
|
106
|
+
WorkcardModel`.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// models/invoice/types.ts
|
|
110
|
+
export interface InvoiceModel { readonly record: Invoice; isPaid: () => boolean; owed: () => number }
|
|
111
|
+
// models/invoice.ts
|
|
112
|
+
export const makeInvoiceModel = (record: Invoice): InvoiceModel => ({
|
|
113
|
+
record,
|
|
114
|
+
isPaid: () => record.paidAt != null,
|
|
115
|
+
owed: () => (record.paidAt != null ? 0 : record.amountMinor),
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A model is built from its record when it is needed — never stored on the record, never serialized.
|
|
120
|
+
|
|
121
|
+
**Wrong, and how it is fixed**
|
|
122
|
+
|
|
123
|
+
| Wrong | Right |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `const a = …; const b = …; export const createX = () => ({ a, b })` + `type X = ReturnType<typeof createX>` | `interface X` in `x/types.ts`; `createX = (): X => { const a = …; const b = …; return { a, b } }` |
|
|
126
|
+
| `xHelper.read(files, path)`, `xHelper.write(files, …)` — the same collaborator in every call | `makeX(files): X` and `x.read(path)` |
|
|
127
|
+
| `export const getDevSlot = (ctx, id) => …` beside five more ctx-first functions | `makeProjectHelper(ctx): ProjectHelper`, reached as `ctx.projects()` |
|
|
128
|
+
| `export const SEED_SOURCES = Object.freeze([…])` in a code file | the constant in `consts.ts` |
|
|
129
|
+
| one `types.ts` holding the interfaces of twenty unrelated helpers | each helper's interface in its own `<helper>/types.ts` |
|
|
130
|
+
|
|
131
|
+
## Exempt
|
|
132
|
+
|
|
133
|
+
These stay plain exports: declaration-builder DSLs that are the framework's vocabulary (`route`,
|
|
134
|
+
`protocol`, `typed`, `contract`, `bind`, `logger`, `frontend`/`backend`, `declare*`, config plugins),
|
|
135
|
+
factories themselves (`make*`, `create*`, `append*`), React components and `use*` hooks, entry
|
|
136
|
+
scripts, error classes and schema files.
|
|
137
|
+
|
|
138
|
+
## Generated applications
|
|
139
|
+
|
|
140
|
+
Applications the viable agent generates follow the same rules, in these fixed shapes:
|
|
141
|
+
|
|
142
|
+
- **Domain model per entity.** `backend/src/models/<entity>/types.ts` declares
|
|
143
|
+
`interface <Base>Model` first; `models/<entity>/<base>.ts` exports only
|
|
144
|
+
`make<Base>Model = (ctx: Context): <Base>Model => { const op = async (actor: Actor, …) => …; return { op, … } }`.
|
|
145
|
+
It is built where it is used — `await makeTaskModel(ctx).complete(actorOf(request), id)` — and is
|
|
146
|
+
never registered as a context service. Its member names are the endpoint keys of the entity.
|
|
147
|
+
- **Handlers and job processors**: one plain exported function per file
|
|
148
|
+
(`api/src/app/<entity>/<action>.ts` beside a GENERATED `index.ts` barrel, `worker/src/jobs/<job>.ts`);
|
|
149
|
+
a handler's body is one call into the model.
|
|
150
|
+
- **Seed helpers are objects** whose members keep the old function names:
|
|
151
|
+
`actorOf(request): Actor` with `actor.inOrganization(…)`, `actor.assertOrganization(…)`,
|
|
152
|
+
`actor.grantedIds(…)`, `actor.organizationScope(…)`; `recordOwnerOf(request)`, `visitOf(request)`,
|
|
153
|
+
`planningAccessOf(request, ctx)`, `landingHandoff`, `visitKey`. Their types live in the module's
|
|
154
|
+
same-named folder (`lib/actor/types.ts`); the old free functions stay as
|
|
155
|
+
`@deprecated generated-app:factory-objects` wrappers so a project generated before still compiles.
|
|
156
|
+
- A model module generated before this shape keeps its plain functions: its callers import them by
|
|
157
|
+
name, so it is extended in that shape and never converted in place.
|
|
158
|
+
- **Resources** keep their maker and their `xResource(ctx)` accessor; the resource's interface lives
|
|
159
|
+
in `resources/<entity>/types.ts`.
|
|
160
|
+
- Shared types in `common` are interfaces.
|
|
161
|
+
- A file the template shipped keeps the shape the platform gives it.
|
|
162
|
+
|
|
163
|
+
## Self-check before you finish
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
{ git diff --name-only HEAD; git ls-files --others --exclude-standard; } | grep -E '\.tsx?$' \
|
|
167
|
+
| grep -vE '\.(test|spec)\.tsx?$|\.d\.ts$|(^|/)(tests?|fixtures)/' > /tmp/changed.txt
|
|
168
|
+
# derived helper types and wrapper factories
|
|
169
|
+
xargs -r grep -nE 'ReturnType<typeof|= \(\) => \(\{ *[a-zA-Z]+, ' < /tmp/changed.txt
|
|
170
|
+
# object-shaped type aliases that should be interfaces
|
|
171
|
+
xargs -r grep -nE '^(export )?type [A-Za-z0-9_]+(<[^=]*>)? *= *\{' < /tmp/changed.txt
|
|
172
|
+
# types, enums or CONSTANT_CASE constants declared in code files
|
|
173
|
+
grep -vE '(^|/)(types|consts)(\.local)?\.ts$' /tmp/changed.txt \
|
|
174
|
+
| xargs -r grep -nE '^export (interface |enum |type [A-Za-z0-9_]+ *(<[^=]*>)? *=|const [A-Z][A-Z0-9_]+ *[:=])'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Then read the diff once for what no grep sees: a second exported function of one domain, a member
|
|
178
|
+
that takes the same first argument as its siblings, a private helper exposed on the interface, a
|
|
179
|
+
bound object built in a loop, a barrel re-exporting a `.local` file.
|
|
180
|
+
|
|
181
|
+
Classification details, folder decisions and the pitfalls of moving code into closures: the
|
|
182
|
+
`code-structure-reference` part ([parts/code-structure-reference.md](parts/code-structure-reference.md)).
|
|
183
|
+
|
|
184
|
+
## Parts
|
|
185
|
+
|
|
186
|
+
| Part | Load it when |
|
|
187
|
+
|---|---|
|
|
188
|
+
| [code-structure-reference](parts/code-structure-reference.md) | classifying a declaration, deciding a folder, converting a module, or moving code into closures |
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-structure-reference
|
|
3
|
+
description: Classifying a declaration, file roles, deciding a module's shape, converting a module step by step, choosing the binding target, interface conversions, and pitfalls.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes:
|
|
6
|
+
mentions:
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# owlmeans-code-structure — reference
|
|
11
|
+
|
|
12
|
+
## Contents
|
|
13
|
+
- Classifying a declaration
|
|
14
|
+
- File roles
|
|
15
|
+
- Deciding the shape of a module
|
|
16
|
+
- Converting a module, step by step
|
|
17
|
+
- Binding: choosing the target
|
|
18
|
+
- Interfaces over types: the conversions
|
|
19
|
+
- Pitfalls
|
|
20
|
+
|
|
21
|
+
## Classifying a declaration
|
|
22
|
+
|
|
23
|
+
| Kind | What it is | Goes to |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| type | `interface`, `type` alias, `declare const x: unique symbol` brand | `types.ts` / `types.local.ts` |
|
|
26
|
+
| constant | an enum, or a `const` whose initializer is a literal, an object/array of constants, a template over constants, `Object.freeze`/`as const`/`satisfies` of those, `new Set/Map/RegExp` over constants, or `.map/.filter/.concat` over constants | `consts.ts` / `consts.local.ts` |
|
|
27
|
+
| state | `let`/`var`, a cache, a registry, a holder (`new Map()`, `[]`, `{}` that is filled later) | stays with the code that owns it, inside its factory when per-instance, module level when process-wide |
|
|
28
|
+
| instance | a value built by a factory (`createPathResolver(DEFAULT_TOPOLOGY)`) | the factory's file, as its ready instance |
|
|
29
|
+
| code | functions, classes, side-effect statements | code files, inside a factory |
|
|
30
|
+
|
|
31
|
+
A constant computed by calling project code is either rewritten as a constant expression (a
|
|
32
|
+
template over constants instead of `srcOf(...)`) or is an instance.
|
|
33
|
+
|
|
34
|
+
## File roles
|
|
35
|
+
|
|
36
|
+
| File | Holds |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `x.ts` | one factory (plus its ready instance), or one exempt export (a component, a hook, one handler) |
|
|
39
|
+
| `x/types.ts` | `x`'s interface and the types only `x` owns |
|
|
40
|
+
| `x/consts.ts`, `x/*.local.ts` | its constants; its package-private declarations |
|
|
41
|
+
| `x/<sub>.ts` | a sub-helper only `x` uses — itself a factory, with its own folder when it has parts |
|
|
42
|
+
| `<dir>/types.ts` | declarations two or more objects of the directory share, or a small single-owner directory |
|
|
43
|
+
| `index.ts` | re-exports only: `export type * from './x/types.js'`, `export * from './x.js'`; never a `.local` file, never an enum through `export type *` |
|
|
44
|
+
| `errors.ts`, `schemas.ts` | error classes; JSON schemas and the code that builds them |
|
|
45
|
+
|
|
46
|
+
A types file imports only types files, consts files and external packages — never a code file.
|
|
47
|
+
|
|
48
|
+
## Deciding the shape of a module
|
|
49
|
+
|
|
50
|
+
1. List what other files use from it. Everything else becomes private.
|
|
51
|
+
2. Group what is used by domain. One group = one object; a second group = a second file.
|
|
52
|
+
3. Look at the first parameters. A shared context, request, collaborator or record moves into the
|
|
53
|
+
factory (see *Binding*). Members that take different first arguments may mean two objects.
|
|
54
|
+
4. A group whose members all read one record is a model; one that lives in a context with a
|
|
55
|
+
lifecycle is a service; one that reads and writes a store is a resource; the rest are helpers
|
|
56
|
+
(reusable) or utils (package-internal).
|
|
57
|
+
5. Name it by the kind: `createXxxHelper` / `makeXxxHelper`, `createXxxUtils`, `createXxxService`,
|
|
58
|
+
`makeXxxModel`, `makeXxxResource`.
|
|
59
|
+
|
|
60
|
+
## Converting a module, step by step
|
|
61
|
+
|
|
62
|
+
1. Write `x/types.ts`: the interface, one member per public function, with explicit parameter and
|
|
63
|
+
return types and the JSDoc moved from the functions.
|
|
64
|
+
2. Rewrite `x.ts`: `export const createX = (deps): X => { … return { … } }`, with every function of
|
|
65
|
+
the group and every helper only they use inside the closure, in their original order.
|
|
66
|
+
3. Drop the bound parameter from the members and from the calls between them.
|
|
67
|
+
4. Move constants and types the module still declares to `x/consts.ts` and `x/types.ts`.
|
|
68
|
+
5. Update every caller: `fn(ctx, a)` → `xOf(ctx).fn(a)`, `fn(a)` → `xHelper.fn(a)`, hoisting a
|
|
69
|
+
repeated `xOf(ctx)` to one local per function.
|
|
70
|
+
6. Build from clean (delete `build/` and `*.tsbuildinfo`), run the package's tests, and grep the
|
|
71
|
+
tests that read source as text (`readFileSync` of a `.ts`) for the moved names.
|
|
72
|
+
|
|
73
|
+
A published package that removes an export keeps a deprecated delegate at the end of the module
|
|
74
|
+
that exported it (`/** @deprecated compat:factory-refactor use xOf(ctx).fn() */`) until its version
|
|
75
|
+
line changes — never gathered into one `compat.ts`, because a subpath export (`pkg/login`) resolves
|
|
76
|
+
that module, not the package root. No code in the OwlMeans repositories may import it; a private
|
|
77
|
+
package drops its delegates once nothing imports them.
|
|
78
|
+
|
|
79
|
+
## Binding: choosing the target
|
|
80
|
+
|
|
81
|
+
- **A service** when the object has a lifecycle (init, connections), holds state shared by the
|
|
82
|
+
process, or must be replaceable by an application.
|
|
83
|
+
- **A context accessor** when an application owns its context type: declare `xxx: () => XxxHelper` on
|
|
84
|
+
the context interface and install it in the context factory with `memoHelper.once`.
|
|
85
|
+
- **`oncePer`** inside a library, where the helper must not appear on the public context type:
|
|
86
|
+
`export const xxxOf = memoHelper.oncePer(makeXxx)`.
|
|
87
|
+
- **A request scope** for what depends on the caller (actor, organization, channel): build it once at
|
|
88
|
+
the top of the handler and pass it down; never store it.
|
|
89
|
+
- **A collaborator accessor** when a helper always works over one FileHelper, stage or deps bag.
|
|
90
|
+
- **A model** when every member reads the same record.
|
|
91
|
+
|
|
92
|
+
Construction must stay cheap: no I/O and no awaits in a factory body; tables a member needs are built
|
|
93
|
+
lazily with `memoHelper.once`.
|
|
94
|
+
|
|
95
|
+
## Interfaces over types: the conversions
|
|
96
|
+
|
|
97
|
+
| From | To |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `type X = { a: A }` | `interface X { a: A }` |
|
|
100
|
+
| `type X = A & { b: B }` | `interface X extends A { b: B }` |
|
|
101
|
+
| `type X = Omit<A, 'k'> & { k: K }` | `interface X extends Omit<A, 'k'> { k: K }` |
|
|
102
|
+
| `type X = Partial<Pick<A, 'a' \| 'b'>>` | `interface X extends Partial<Pick<A, 'a' \| 'b'>> {}` |
|
|
103
|
+
| `type F = (a: A) => R` | `interface F { (a: A): R }` |
|
|
104
|
+
| `type F = <T>(a: T) => R<T>` | `interface F { <T>(a: T): R<T> }` |
|
|
105
|
+
|
|
106
|
+
Keep the alias, with a one-line reason, where the interface breaks the program: it is used where an
|
|
107
|
+
implicit index signature is required, its `extends` parts conflict, or a declaration of the same name
|
|
108
|
+
would merge with it.
|
|
109
|
+
|
|
110
|
+
## Pitfalls
|
|
111
|
+
|
|
112
|
+
- **Read before initialization.** A factory body runs when called; a ready instance runs at import.
|
|
113
|
+
Keep the instance the file's last statement, and never let a module-level constant call a factory
|
|
114
|
+
of the same import cycle.
|
|
115
|
+
- **Import cycles.** Bound objects import types, constants and pure helpers only; composition roots
|
|
116
|
+
(context factories) import the factories. Reach another bound object through the binding target.
|
|
117
|
+
- **Declaration emit.** A private type that appears in a public signature must move to `types.ts`;
|
|
118
|
+
an inferred return type that names an unexported type fails the build (`TS4023`, `TS2742`).
|
|
119
|
+
- **State moved into a closure** becomes per instance. Process-wide state (a queue, a merge chain, a
|
|
120
|
+
cache every caller must share) stays at module level or in a per-context memo.
|
|
121
|
+
- **Inheritance by prototype.** A helper derived with `Object.create(base)` inherits accessors bound to
|
|
122
|
+
`base`; build an accessor over the derived object when its calls must go through it.
|
|
123
|
+
- **Enums** never move into types files and never pass through `export type *`.
|
|
124
|
+
- **Same name from two modules** in one barrel fails with `TS2308`; rename or re-export explicitly.
|
|
125
|
+
- **Tests that read source text** break silently when code moves; grep them in the same change.
|