@orkestrel/scaffold 0.0.2 → 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 +29 -15
- package/dist/bin/scaffold.js +1162 -1200
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +124 -925
- package/dist/host/CLAUDE.md +335 -495
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
- package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
- package/dist/host/claude/agents/application.md +30 -0
- package/dist/host/claude/agents/builder.md +10 -7
- package/dist/host/claude/agents/checker.md +14 -8
- package/dist/host/claude/agents/codex.md +94 -0
- package/dist/host/claude/agents/grok.md +37 -34
- package/dist/host/claude/agents/implementer.md +40 -0
- package/dist/host/claude/agents/orkestrel.md +88 -223
- package/dist/host/claude/agents/planner.md +13 -34
- package/dist/host/claude/agents/reviewer.md +38 -24
- package/dist/host/claude/agents/verifier.md +14 -10
- package/dist/host/claude/rules/application.md +57 -0
- package/dist/host/claude/rules/architecture.md +180 -0
- package/dist/host/claude/rules/browser.md +28 -0
- package/dist/host/claude/rules/documentation.md +64 -0
- package/dist/host/claude/rules/names.md +209 -0
- package/dist/host/claude/rules/patterns.md +130 -0
- package/dist/host/claude/rules/quality.md +45 -0
- package/dist/host/claude/rules/styles.md +64 -0
- package/dist/host/claude/rules/tests.md +123 -0
- package/dist/host/claude/rules/typescript.md +78 -0
- package/dist/host/claude/rules/workspace.md +180 -0
- package/dist/host/claude/settings.json +112 -0
- package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
- package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
- package/dist/host/codex/agents/analyst.toml +15 -0
- package/dist/host/codex/agents/application.toml +25 -0
- package/dist/host/codex/agents/builder.toml +24 -0
- package/dist/host/codex/agents/checker.toml +15 -0
- package/dist/host/codex/agents/grok.toml +17 -0
- package/dist/host/codex/agents/implementer.toml +17 -0
- package/dist/host/codex/agents/opus.toml +25 -0
- package/dist/host/codex/agents/orkestrel.toml +16 -0
- package/dist/host/codex/agents/planner.toml +20 -0
- package/dist/host/codex/agents/reviewer.toml +18 -0
- package/dist/host/codex/agents/verifier.toml +14 -0
- package/dist/host/codex/config.toml +66 -0
- package/dist/host/cursor/mcp.json +12 -0
- package/dist/host/dotfiles/mcp.json +8 -0
- package/dist/host/dotfiles/oxlintrc.json +307 -1
- package/dist/host/guides/src/scaffold.md +2059 -2007
- package/dist/host/manifest.json +376 -137
- package/dist/host/scripts/codex.sh +49 -0
- package/dist/host/scripts/cursor.sh +33 -47
- package/dist/host/scripts/deps.sh +34 -16
- package/dist/host/scripts/ollama.sh +6 -149
- package/dist/host/tests/setupPolicy.ts +390 -0
- package/dist/src/core/index.cjs +6603 -1200
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +802 -267
- package/dist/src/core/index.d.ts +802 -267
- package/dist/src/core/index.js +6521 -1192
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +3132 -593
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +643 -95
- package/dist/src/server/index.d.ts +643 -95
- package/dist/src/server/index.js +3054 -594
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -12
- package/dist/host/claude/agents/composer.md +0 -64
- package/dist/host/claude/agents/researcher.md +0 -38
- package/dist/host/claude/agents/scout.md +0 -35
- package/dist/host/github/workflows/ci.yml +0 -64
package/dist/host/AGENTS.md
CHANGED
|
@@ -1,939 +1,138 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
3
|
> TypeScript · types-first · zero unsolicited dependencies · single-word public APIs.
|
|
4
|
-
> This
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
These
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
**Dependency direction:** `browser` and `server` import from `core`; `core` imports from neither. Apps import from libraries.
|
|
31
|
-
|
|
32
|
-
**Commands** (gates before commit, in order): `npm run format` · `npm run lint` (house rules) · `npm run check` (typecheck) · `npm run build` · `npm test`. Scope tests — `npm run test:src:core`, `test:app:browser`, etc. — never run the full suite casually.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 1. Non-negotiable rules
|
|
37
|
-
|
|
38
|
-
- **NEVER** use `any` — use `unknown` and narrow with type guards.
|
|
39
|
-
- **NEVER** use `!` (non-null assertion) — use conditionals or optional chaining.
|
|
40
|
-
- **NEVER** use `as` (type assertion) — narrow through validation.
|
|
41
|
-
- **NEVER** use `@ts-nocheck`, `@ts-ignore`, `@ts-expect-error`, or `eslint-disable` — fix the underlying issue.
|
|
42
|
-
- **NEVER** add npm packages unless explicitly requested — prefer native APIs.
|
|
43
|
-
- **NEVER** remove a symbol to satisfy a linter — implement it or annotate `// TODO: [Feature] Brief purpose`.
|
|
44
|
-
- **NEVER** undo user edits in `*/types.ts` — that file is the source of truth.
|
|
45
|
-
- **NEVER** use `readonly` on parameters (`readonly` signals immutability to consumers, not producers).
|
|
46
|
-
- **NEVER** use the `private` keyword — use `#` private fields (runtime-enforced).
|
|
47
|
-
- **NEVER** use default exports (except where a framework strictly requires them — Vue SFCs, config files).
|
|
48
|
-
- **ALWAYS** use `readonly` for interface properties and public return types.
|
|
49
|
-
- **ALWAYS** define reusable and public types in `*/types.ts` before implementation.
|
|
50
|
-
- **ALWAYS** finish the implementation — no deferred logic, empty stubs, or hidden follow-up work.
|
|
51
|
-
- **ALWAYS** match the existing naming and file-placement patterns exactly.
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## 2. Methodology — TTTDD (Types Then Tests Driven Development)
|
|
56
|
-
|
|
57
|
-
1. **Types drive the public API.** All interfaces and type aliases go in `*/types.ts` first. That file is the single source of truth — implementation conforms to it, never the reverse.
|
|
58
|
-
2. **Implement to satisfy the types.** Write the class or function that fulfills the contract. Extract internals to the appropriate centralized file.
|
|
59
|
-
3. **Consolidate.** After implementation, deduplicate. Patterns that repeat across files get extracted to centralized files.
|
|
60
|
-
4. **Harden with tests.** Cover happy paths, edge cases, error conditions, and boundary values.
|
|
61
|
-
5. **Document.** Write/update guides so any reader (human or AI) can understand and extend the code.
|
|
62
|
-
|
|
63
|
-
**User changes override everything.** If the user modifies a type or interface mid-task, that change is **immediately authoritative** — refactor all affected code to match. Failed tests after a type change indicate implementation that hasn't caught up, not bugs in the tests. Use `npm run check` to find every site that needs updating.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## 3. Code style
|
|
68
|
-
|
|
69
|
-
- Tabs for indentation.
|
|
70
|
-
- No semicolons (except where required for ASI).
|
|
71
|
-
- Single quotes for strings.
|
|
72
|
-
- Named exports only.
|
|
73
|
-
- ESM imports with explicit `.js` extension: `import { x } from './foo.js'`.
|
|
74
|
-
- `#` private fields, never the `private` keyword.
|
|
75
|
-
- Import order: type imports first (`import type`), then internal/value modules.
|
|
76
|
-
- No empty lines between consecutive imports of the same kind.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## 4. Naming — the signature of this codebase
|
|
81
|
-
|
|
82
|
-
> **Names are the public API.** They must be predictable enough that a consumer can guess the correct identifier without consulting documentation.
|
|
83
|
-
|
|
84
|
-
### 4.1 The single-word principle (the load-bearing rule)
|
|
85
|
-
|
|
86
|
-
**Every property, method, option key, and event name on an entity should be a single descriptive word.** The entity it lives on supplies all the disambiguating context — the name does not need to repeat it.
|
|
87
|
-
|
|
88
|
-
This is not a stylistic preference. It is the foundation that the rest of this document depends on. If a name needs more than one word, that is almost always a signal that the **API shape is wrong**, not that the name is too short — see §4.2.
|
|
89
|
-
|
|
90
|
-
**The reasoning:**
|
|
91
|
-
|
|
92
|
-
- A property on `AgentInterface` is already an agent property. Calling it `agentId` adds nothing — use `id`.
|
|
93
|
-
- A method on `ToolManager` is already a tool-manager method. Calling it `addTool` is redundant; `add()` is enough.
|
|
94
|
-
- An option in `DatabaseOptions` is already a database option. Calling it `databasePath` repeats the entity twice; `path` is enough.
|
|
95
|
-
|
|
96
|
-
**Scope of the rule — what it applies to (entity-scoped names):**
|
|
97
|
-
|
|
98
|
-
- Properties and getters on a class or interface
|
|
99
|
-
- Methods on a class or interface
|
|
100
|
-
- Option keys (the entity is the options interface itself)
|
|
101
|
-
- Event names inside an `EventMap` (the entity is the emitter that owns the map)
|
|
102
|
-
|
|
103
|
-
**What it does NOT apply to — see §4.3 / §4.5 for these:**
|
|
104
|
-
|
|
105
|
-
- Standalone helper functions at module scope (no entity context to lean on)
|
|
106
|
-
- Type identifiers (their role suffix is required — see §4.5)
|
|
107
|
-
- Constants (the qualifier is part of the meaning — `DEFAULT_TIMEOUT_MS`)
|
|
108
|
-
|
|
109
|
-
**Concrete examples from real interfaces:**
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
// AgentInterface — entire public surface in single words
|
|
113
|
-
interface AgentInterface {
|
|
114
|
-
readonly emitter: EmitterInterface<AgentEventMap>
|
|
115
|
-
readonly id: string
|
|
116
|
-
readonly context: AgentContextInterface
|
|
117
|
-
readonly status: AgentStatus
|
|
118
|
-
generate(): Promise<string>
|
|
119
|
-
stream(options?: AgentStreamOptions): AsyncGenerator<string, string>
|
|
120
|
-
abort(): void
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// IndexedList<T> — all members single words, intent obvious from the container
|
|
124
|
-
interface IndexedList<T> {
|
|
125
|
-
readonly size: number
|
|
126
|
-
has(id: string): boolean
|
|
127
|
-
item(id: string): T | undefined
|
|
128
|
-
items(): readonly T[]
|
|
129
|
-
append(id: string, item: T): void
|
|
130
|
-
prepend(id: string, item: T): void
|
|
131
|
-
insert(index: number, id: string, item: T): void
|
|
132
|
-
remove(): void
|
|
133
|
-
remove(id: string): boolean
|
|
134
|
-
remove(ids: string[]): boolean
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**Hard targets (for entity-scoped names):**
|
|
139
|
-
|
|
140
|
-
- Public properties: **one word**
|
|
141
|
-
- Public methods: **one word** (two only when no single verb captures the action)
|
|
142
|
-
- Option keys: **one word** when ungrouped; otherwise an entity name keying a nested object (see §4.2.1)
|
|
143
|
-
- Event names: **one word**, present-tense verb or noun
|
|
144
|
-
- Private methods: 2–3 words allowed — privacy reduces the cost of length
|
|
145
|
-
|
|
146
|
-
### 4.2 When a single word is not enough — split, don't compound
|
|
147
|
-
|
|
148
|
-
If you find yourself reaching for a prefix or suffix on an entity member to disambiguate (`addInstruction`, `removeDocument`, `databasePath`, `serverTimeout`), **stop**. The compound name is a symptom that the surface is doing too much. Three strategies, by shape:
|
|
149
|
-
|
|
150
|
-
#### 4.2.1 Options — group by entity as the key
|
|
151
|
-
|
|
152
|
-
Flat option names with prefixes are wrong. Group related options into a nested object whose **key is the entity** they configure.
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
// ✗ Wrong — prefixes repeat the entity
|
|
156
|
-
interface ServerOptions {
|
|
157
|
-
readonly serverPort?: number
|
|
158
|
-
readonly serverTimeout?: number
|
|
159
|
-
readonly databasePath?: string
|
|
160
|
-
readonly databaseTimeout?: number
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ✓ Right — entity is the key, fields stay single-word
|
|
164
|
-
interface ServerOptions {
|
|
165
|
-
readonly server?: { readonly port?: number; readonly timeout?: number }
|
|
166
|
-
readonly database?: { readonly path?: string; readonly timeout?: number }
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
The shape now answers "which entity owns this setting?" structurally instead of through string prefixes. Every leaf is a single word. Adding a third option (`cache`) is a new key, not a new prefix family.
|
|
171
|
-
|
|
172
|
-
#### 4.2.2 Class members — split into a sub-entity exposed as a property
|
|
173
|
-
|
|
174
|
-
If a class accumulates verbs like `addX`, `removeX`, `addY`, `removeY`, those verbs do not belong on the parent class. Each prefix family is a separate entity. Extract it into its own class (typically a Manager) and expose it as a **single-word property** named after the domain noun (plural).
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
// ✗ Wrong — parent inflates with prefixed methods
|
|
178
|
-
interface AgentContextInterface {
|
|
179
|
-
addInstruction(instruction: InstructionInput): InstructionInterface
|
|
180
|
-
removeInstruction(id: string): boolean
|
|
181
|
-
addDocument(document: DocumentInput): DocumentInterface
|
|
182
|
-
removeDocument(id: string): boolean
|
|
183
|
-
addTool(tool: ToolInput): ToolInterface
|
|
184
|
-
removeTool(id: string): boolean
|
|
185
|
-
// ... and on, and on
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ✓ Right — each domain is its own manager, exposed as a single-word property
|
|
189
|
-
interface AgentContextInterface {
|
|
190
|
-
readonly instructions: InstructionManagerInterface
|
|
191
|
-
readonly documents: DocumentManagerInterface
|
|
192
|
-
readonly tools: ToolManagerInterface
|
|
193
|
-
// callers reach functionality through the manager:
|
|
194
|
-
// context.instructions.add(input)
|
|
195
|
-
// context.documents.remove(id)
|
|
196
|
-
// context.tools.execute(call)
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
This is the dominant composition pattern. Every domain noun on a parent entity is a single-word property pointing to its own Manager class. The Manager owns the verbs (`add`, `remove`, `clear`, …) and those verbs stay single-word because their context comes from the Manager, not the parent.
|
|
201
|
-
|
|
202
|
-
#### 4.2.3 Functions — split variants instead of dispatching by parameter
|
|
203
|
-
|
|
204
|
-
If a function takes a discriminator parameter to choose between behaviors (`parse(format, input)` where `format` is `'json' | 'yaml' | 'toml'`), split it into separate named functions. Each variant lives independently and the call site reads naturally.
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
// ✗ Wrong — discriminator parameter dispatches between behaviors
|
|
208
|
-
function parse(input: string, format: 'json' | 'yaml' | 'toml'): unknown {
|
|
209
|
-
/* big switch */
|
|
210
|
-
}
|
|
211
|
-
// callers: parse(text, 'json'), parse(text, 'yaml')
|
|
212
|
-
|
|
213
|
-
// ✓ Right — each variant is its own function
|
|
214
|
-
function parseJson(input: string): unknown {
|
|
215
|
-
/* ... */
|
|
216
|
-
}
|
|
217
|
-
function parseYaml(input: string): unknown {
|
|
218
|
-
/* ... */
|
|
219
|
-
}
|
|
220
|
-
function parseToml(input: string): unknown {
|
|
221
|
-
/* ... */
|
|
222
|
-
}
|
|
223
|
-
// callers: parseJson(text), parseYaml(text)
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
These resulting names are multi-word — that is correct. Helpers live at module scope with no entity context, so they need enough words to be self-descriptive at the call site (§4.3). The principle being enforced is **"split variants into named functions,"** not "make the names single-word." A discriminator parameter folds multiple behaviors behind one name; splitting exposes them honestly. If a family of related functions starts to feel like it deserves a shared context, that's a signal to promote it into a class or interface and let §4.2.2 take over.
|
|
227
|
-
|
|
228
|
-
#### 4.2.4 Parameters — no behavior-selecting magic strings
|
|
229
|
-
|
|
230
|
-
A string (or other literal) parameter must never select WHICH behavior a function performs. If passing a different value routes the call to a different branch — a different algorithm, a different shape of work — that parameter is a magic string, and the function is several functions hiding behind one name. Split it into separately named functions (§4.2.3); when the variants share an entity, promote them to methods on a class (§4.2.2).
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
// ✗ Wrong — position is a mode literal that switches behavior
|
|
234
|
-
function addFactor(
|
|
235
|
-
group: FactorGroup,
|
|
236
|
-
factor: Factor,
|
|
237
|
-
position: 'start' | 'end' | { readonly before: string } | { readonly after: string },
|
|
238
|
-
): FactorGroup {
|
|
239
|
-
/* branches on position */
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ✓ Right — one name per behavior; the optional target id is data, not a mode
|
|
243
|
-
function appendFactor(group: FactorGroup, factor: Factor, target?: string): FactorGroup {
|
|
244
|
-
/* ... */
|
|
245
|
-
}
|
|
246
|
-
function prependFactor(group: FactorGroup, factor: Factor, target?: string): FactorGroup {
|
|
247
|
-
/* ... */
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
Same test as the binary-toggle rule (§4.4): does the value switch behavior, or is it data? These are data and stay unions: a discriminant on a data value you narrow on (reasoning, an error code); a value enum applied uniformly (`Comparison`, `Aggregation`, `MathOperation`); a key or field selector applied uniformly (a `clear(definition, key)` that deletes whichever optional key is named performs the same operation for every value). The line: a value that picks a DIFFERENT thing to do — split; a value that picks a different DATUM to do the SAME thing to — data.
|
|
252
|
-
|
|
253
|
-
### 4.3 Helpers are different — module scope has no entity context
|
|
254
|
-
|
|
255
|
-
Standalone helper functions exported from `helpers.ts` (or any module) are called bare at the consumer site:
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
import { computeHash, extractColumns, normalizePath } from './helpers.js'
|
|
259
|
-
|
|
260
|
-
const hash = computeHash(content)
|
|
261
|
-
const cols = extractColumns(definition)
|
|
262
|
-
const path = normalizePath(input)
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
There is no parent class or interface providing context. The helper name is the **entire** signal the reader gets. So helpers default to a self-descriptive `{verb}{Noun}` form and need enough words to make the action clear without ambiguity.
|
|
266
|
-
|
|
267
|
-
- **Default form:** `{verb}{Noun}` — `generateId`, `computeHash`, `extractColumns`, `derivePhaseStatus`, `inferLanguage`, `normalizePath`, `sanitizeFilename`, `matchesGlobPattern`, `belongsTo`, `hasMany`.
|
|
268
|
-
- **Single-word helpers are allowed** only when the verb alone is fully unambiguous and the helper takes obvious arguments — `delay(ms)`, `clamp(n, bounds)`, `tokenize(input)`, `similarity(a, b)`. If you have to think about what `process(x)` or `handle(x)` does, the name is too short.
|
|
269
|
-
- **Identical-verb-means-identical-thing still applies** (§4.4, §4.8): every `extract*` extracts from a structure, every `infer*` derives a value from another, every `compute*` runs a deterministic calculation, every `matches*` is a predicate. Pick a verb prefix per concept and use it everywhere.
|
|
270
|
-
- **When a helper family grows,** that's a signal to promote it to a class (§4.2.2). Five `extract*Foo` helpers operating on the same shape probably belong on a `FooReader` class with single-word methods.
|
|
271
|
-
|
|
272
|
-
### 4.4 General rules
|
|
273
|
-
|
|
274
|
-
- Names describe **what a thing is**, not how it works internally.
|
|
275
|
-
- Prefer short, common English words — avoid jargon, abbreviations, acronyms (unless universally understood: URL, ID, HTML).
|
|
276
|
-
- One concept = one word, used everywhere (always `count`, never `length`/`size`/`total` interchangeably).
|
|
277
|
-
- If two names feel synonymous, pick one and use it project-wide.
|
|
278
|
-
- Properties are **nouns** — `count`, not `getCount`.
|
|
279
|
-
- Methods are **verbs** — `abort`, not `aborter`.
|
|
280
|
-
- Booleans read as **assertions** — `aborted`, `exhausted`, `expired`.
|
|
281
|
-
- A **binary behavioral toggle is a boolean**, not a two-value union — `bail`, not `failure: 'continue' | 'halt'`. The test is whether it _switches behavior_: discriminated-union tags (§4.5), multi-value / lifecycle unions (§10), conventional value pairs (`ascending`/`descending`, `and`/`or`), and external-spec literals are **not** toggles — they stay unions.
|
|
282
|
-
- No `get`/`set` prefixes — accessors use bare nouns.
|
|
283
|
-
- Identical verbs across the project mean identical things — see Lifecycle Vocabulary (§10).
|
|
284
|
-
- A **discriminant is named for the axis it splits on**, never `kind` — `relationship`, `command`, `category` say what varies; `kind`/`type` say nothing.
|
|
285
|
-
|
|
286
|
-
### 4.4.1 Acronym casing in identifiers
|
|
287
|
-
|
|
288
|
-
When a name **does** contain an acronym (the universally-understood ones the rule above permits), the acronym keeps its **canonical case** — never PascalCase-folded to title case.
|
|
289
|
-
|
|
290
|
-
- **An initialism is UPPERCASE** — `JSON`, `HTTP`, `NDJSON`, `SSE`, `URL`, `URI`, `SQL`, `API`, `DB`, `TTL`, `UUID`, `RFC`, `JWT`. A **brand / proper name keeps its branding** — `SQLite`, `IndexedDB`, `GitHub`, `OAuth`.
|
|
291
|
-
- **In a PascalCase type / class / interface, the acronym is always its canonical case:** `JSONSchema`, `NDJSONParser`, `HTTPServer`, `SSEStream`, `URLPattern`, `SQLiteDriver`, `IndexedDBDriver`.
|
|
292
|
-
- **In a camelCase value / method / property, an acronym lowercases ONLY when it LEADS the name** (to satisfy camelCase) — `jsonValue`, `urlPath`, `sqlText` — and keeps its canonical case anywhere else: `parseJSON`, `toJSON`, `fromNDJSON`, `signJWT`, `compileSQL`.
|
|
293
|
-
- **Module FOLDERS stay lowercase** (§4.7) — `http/`, `sqlite/`, `indexeddb/`, `websocket/`. This rule governs **identifiers**, not folder names.
|
|
294
|
-
- A few **ecosystem-conventional** lowercase identifiers stay as the ecosystem writes them: `id` (a property), and a leading `url` / `uri` in a camelCase name.
|
|
295
|
-
- **The drift to avoid is PascalCase-folding an acronym** — `Json`, `Ndjson`, `Http`, `Sse`, `Sqlite` are **WRONG**; write `JSON`, `NDJSON`, `HTTP`, `SSE`, `SQLite`.
|
|
296
|
-
|
|
297
|
-
### 4.5 Type-level identifiers (PascalCase + role suffix)
|
|
298
|
-
|
|
299
|
-
Type names are the one place suffixes are required — they encode the type's **role**, which the consumer cannot infer from context alone.
|
|
300
|
-
|
|
301
|
-
| Kind | Suffix | Pattern | Purpose |
|
|
302
|
-
| -------------------- | -------------------- | -------------------------- | ----------------------------------------- |
|
|
303
|
-
| Behavioral interface | `Interface` | `{Entity}Interface` | Contract for a class |
|
|
304
|
-
| Options / config | `Options` | `{Entity}Options` | Input bag for creating/configuring |
|
|
305
|
-
| Creation input | `Input` | `{Entity}Input` | Minimal data needed to create an instance |
|
|
306
|
-
| Outcome / output | `Result` | `{Entity}Result` | Structured return value |
|
|
307
|
-
| Execution context | `Context` | `{Entity}Context` | Ambient state passed through a call chain |
|
|
308
|
-
| Event map | `EventMap` | `{Entity}EventMap` | Map of event names → arg tuples |
|
|
309
|
-
| Union / enum-like | `{Noun}` | `{Entity}{Noun}` | Constrained set of literal values |
|
|
310
|
-
| Plain data / value | _(none)_ | `{Entity}` | Records or events with no behavior |
|
|
311
|
-
| Function type | `Handler`/`Function` | `{Entity}Handler` | Function type invoked by the framework |
|
|
312
|
-
| Manager interface | `ManagerInterface` | `{Entity}ManagerInterface` | Collection contract w/ lookup + lifecycle |
|
|
313
|
-
|
|
314
|
-
### 4.6 Value-level identifiers
|
|
315
|
-
|
|
316
|
-
| Kind | Casing | Pattern | Examples |
|
|
317
|
-
| ---------------- | ---------------- | ------------------------- | -------------------------------------- |
|
|
318
|
-
| Class | PascalCase | `{Entity}` | `Agent`, `Greeter`, `Timeout` |
|
|
319
|
-
| Manager class | PascalCase | `{Entity}Manager` | `ToolManager`, `ScopeManager` |
|
|
320
|
-
| Factory function | camelCase | `create{Entity}` | `createAgent`, `createGreeter` |
|
|
321
|
-
| Type guard | camelCase | `is{Condition}` | `isRecord`, `isSuccess`, `isFailure` |
|
|
322
|
-
| Helper function | camelCase | `{verb}{Noun}` (see §4.3) | `generateId`, `computeHash`, `delay` |
|
|
323
|
-
| Constant | UPPER_SNAKE_CASE | `{QUALIFIER}_{NOUN}` | `DEFAULT_TIMEOUT_MS`, `MAX_ITERATIONS` |
|
|
324
|
-
| Property / field | camelCase | bare noun (single word) | `count`, `signal`, `status`, `context` |
|
|
325
|
-
| Method | camelCase | bare verb (single word) | `abort()`, `start()`, `execute()` |
|
|
326
|
-
| Boolean | camelCase | adjective/past-participle | `aborted`, `paused`, `expired` |
|
|
327
|
-
|
|
328
|
-
### 4.6.1 The derivation & construction idioms (one prefix/suffix per role)
|
|
329
|
-
|
|
330
|
-
A small set of affixes carry a fixed **role** across the whole codebase. Each says exactly what a symbol does and how it composes — pick the affix by role, never by feel, and never blur two roles under one shape.
|
|
331
|
-
|
|
332
|
-
- **`is*` — a guard.** A `Guard<T> = (value: unknown) => value is T`: total, never throws, returns `false` for anything off-shape (`isRecord`, `isSuccess`, `isJSONPrimitive`).
|
|
333
|
-
- **`parse*` — a coercer.** Returns `T | undefined` — "give me a `T` or nothing" (`parseNumber`, `parseJSON`). Cross-type conversion lives here, not on a guard.
|
|
334
|
-
- **`create*` — an entity / value factory.** Constructs and returns an instance (`createAgent`, `createGreeter`, `createDatabase`).
|
|
335
|
-
- **`*Of` — a combinator / builder.** Takes the **constituent parts** and returns the container / guard / value built from them: `arrayOf(guard)`, `recordOf(guard)`, `unionOf(...guards)`, `boundsOf(min, max)`, `rangeOf(start, end)`. A builder consumes the **pieces**.
|
|
336
|
-
- **`{noun}To{Noun}` — a projection.** Takes a **whole** and returns a derived view of it: `definitionToSnapshot`, `stepToForm`, `fileToDocument`, `schemaToParameters`. A projection consumes the **whole** — this is what distinguishes it from `*Of` (which consumes the parts). When you reach for `*Of` to name a "make a Y out of an X" helper, you want `xToY` instead; reserve `*Of` for parts-to-container builders.
|
|
337
|
-
- **`*Shape` — a shaper.** A `ContractShape` **value** — a JSON-Schema blueprint, not a function and not a type (`stringShape`, `objectShape`). Shapers live in `shapers.ts`; the compilers turn a shape into a guard / parser / schema / generator.
|
|
338
|
-
- **`_*` (leading underscore) — an intentionally-unused binding.** Suppresses the unused-binding lint for a binding that is GENUINELY ignored (a callback-conformance param, a rest-omit, a swallowed `catch (_e)`); it is NEVER a privacy marker (privacy is `#`). Keep it rare and verified-intentional — never a forgotten wiring (§4.8).
|
|
339
|
-
|
|
340
|
-
Two members carry a fixed concept-name regardless of the entity:
|
|
341
|
-
|
|
342
|
-
- A **discriminant names its axis, never `kind` / `type`** — `modality`, `operation`, `via`, `relationship` say what varies; `kind` says nothing (§4.4 / §4.8).
|
|
343
|
-
- A **tally is `count`** — never `length` / `size` / `total` used interchangeably (§4.4).
|
|
344
|
-
- **Descriptive tallies when several counts coexist.** A bare `count` is right only when there is ONE unambiguous tally in scope. When a type exposes SEVERAL distinct counts, each takes a DESCRIPTIVE name for what it counts — not a bare `count`. E.g. a workers `Pool` exposes `size` (total), `idle`, and `active`, not three `count`s; a `Queue` keeps `count` (its single pending length) + `active`. Use the most descriptive word for what is represented; reserve bare `count` for the single, unambiguous tally.
|
|
345
|
-
- A **binary behavioral toggle is a boolean**, not a two-value union (§4.4) — `bail`, not `'continue' | 'halt'`.
|
|
346
|
-
|
|
347
|
-
### 4.7 File & folder naming
|
|
348
|
-
|
|
349
|
-
| Kind | Pattern | Examples |
|
|
350
|
-
| -------------- | ------------------------------ | -------------------------------- |
|
|
351
|
-
| Domain folder | lowercase plural of the entity | `agents/`, `tools/`, `greeters/` |
|
|
352
|
-
| Implementation | PascalCase entity name | `Agent.ts`, `ToolManager.ts` |
|
|
353
|
-
| Test file | PascalCase entity + `.test` | `Agent.test.ts` |
|
|
354
|
-
| Docs | lowercase domain + `.md` | `agents.md` |
|
|
355
|
-
|
|
356
|
-
### 4.8 Anti-patterns
|
|
357
|
-
|
|
358
|
-
- Don't compound when you can split — see §4.2 (the dominant failure mode).
|
|
359
|
-
- Don't prefix interfaces with `I` — use the `Interface` suffix (`AgentInterface`, not `IAgent`).
|
|
360
|
-
- Don't use generic words — `data`, `info`, `item`, `thing`, `obj` are too vague.
|
|
361
|
-
- Don't name a discriminant `kind` (or `type`) — name the axis it splits (`relationship`, `command`, `category`); `kind` is the discriminant-level `data`/`thing`.
|
|
362
|
-
- Don't encode types in names — `nameString`, `countNumber` are redundant.
|
|
363
|
-
- Don't model a binary behavioral toggle as a two-value union — make it a boolean (`bail`, not `'continue' | 'halt'`); see §4.4.
|
|
364
|
-
- Don't mix synonyms — pick one term per concept (don't use `cancel` for `abort`, `reset` for `clear`, `run` for `execute`).
|
|
365
|
-
- Don't pass a string/mode parameter that selects a function's behavior — split into named functions (§4.2.3/§4.2.4). A behavior-switching value is a magic string; a discriminant, value enum, or key selector treated uniformly is data.
|
|
366
|
-
- Don't use the `Handler` suffix on classes — `Handler` is for function types only.
|
|
367
|
-
- Don't abbreviate — `config` not `cfg`, `document` not `doc`, `message` not `msg`.
|
|
368
|
-
- Don't pluralize type names — `MessageRole`, not `MessageRoles`.
|
|
369
|
-
- Don't mark methods `@internal` in TSDoc — make them `#` private instead.
|
|
370
|
-
- **The leading `_` is the intentionally-unused marker, never a privacy marker (privacy is `#`).** A leading `_` suppresses the unused-binding lint (`argsIgnorePattern` / `varsIgnorePattern` / `caughtErrorsIgnorePattern`). Reserve it for a binding that is GENUINELY ignored: an interface / callback-conformance param the impl truly doesn't need, a destructuring rest-omit, a swallowed `catch (_e)`, an unused loop var. Keep it RARE — every `_` must be **verified-intentional, never a forgotten wiring**: if a `_`-param carries data the function SHOULD use, drop the `_` and wire it; if it exists for no signature-compat reason, remove it. Prefer a one-line inline comment justifying each `src/` `_` (the convention is far-and-few-between by design).
|
|
371
|
-
- Don't write `getX` / `setX` accessors — use bare nouns.
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## 5. Project structure & the centralized-file pattern
|
|
376
|
-
|
|
377
|
-
| Content | Location | Notes |
|
|
378
|
-
| ---------------- | ------------------------ | ---------------------------------------- |
|
|
379
|
-
| Types/interfaces | `*/types.ts` | **SOURCE OF TRUTH** — no types elsewhere |
|
|
380
|
-
| Constants | `*/constants.ts` | UPPER_SNAKE_CASE, `Object.freeze` values |
|
|
381
|
-
| Helpers/guards | `*/helpers.ts` | Pure utilities |
|
|
382
|
-
| Errors | `*/errors.ts` | Error classes and their type guards |
|
|
383
|
-
| Implementations | `*/[domain]/[Entity].ts` | One class per file |
|
|
384
|
-
| Public barrel | `*/index.ts` | The **sole** public export surface |
|
|
385
|
-
| Tests | `tests/` | Mirror source structure |
|
|
386
|
-
|
|
387
|
-
Each surface grows through the centralized-file pattern. The full set of recognized centralized files (use only the ones a surface actually needs):
|
|
388
|
-
|
|
389
|
-
- `types.ts` · `constants.ts` · `helpers.ts` · `validators.ts` · `combinators.ts` · `parsers.ts` · `shapers.ts` · `compilers.ts` · `factories.ts` · `middlewares.ts` · `seeders.ts` · `schemas.ts` · `relations.ts` · `errors.ts` · `index.ts`
|
|
390
|
-
|
|
391
|
-
**Implementation files contain ONLY** class implementations with `#` private fields and imports. They must **NOT** contain interface definitions, type aliases, constants, or free-standing helper functions — extract those to the appropriate centralized file. Keep compiler/parser recursion in the **functional core**: a **pure** branch — referentially transparent, touching no instance state and calling no sibling method — stays an **exported, centralized helper** (`helpers.ts` / `compilers.ts` / `parsers.ts`), testable in isolation, and is never hidden behind a file-local non-exported function. A branch that must reach the **imperative shell** — instance `#`state or a sibling method — is not a free function but a **method**: public when it is the interface contract, a `#` private method (§7) otherwise. These are complementary, not in tension: extraction pushes every pure **leaf** out to a centralized helper, while an **orchestration** step lives as a class method — and a public method is a real **composition** of those leaves, never a thin 1:1 delegate that only forwards to one helper. "Orchestration" is broader than "stateful": a pure but **compositional or recursive algorithm step** (a chaining pass, a `solve`/`isolate`/`prove` routine, a relational join) stays a `#` private method even when it touches no `#`state and — after its own leaves are extracted — calls only helpers, because it is the class's behavior, not a leaf (§7 gives the ordered leaf test).
|
|
392
|
-
|
|
393
|
-
**Extract aggressively — `export`ed or not.** The rule is about WHERE a member lives, not whether it is public. A file-local `interface` / `type` / `const` / free `function` that is _only_ used inside its own impl file and never exported **must still be extracted** to the module's centralized file (a private local `interface Waiter` → `types.ts`; a module-scope `const LIMIT` → `constants.ts`; a free `function compilePath` → `helpers.ts`). "It's only used here / it's not exported" is **not** a reason to leave it in the impl file — an implementation file holds the class and nothing else. Centralizing a type then publishes it through the barrel (`export type *`), and a published symbol is documented + parity-covered (§22) like any other; that visibility is the intended, accepted cost of the rule, not a reason to dodge it. The one and only exception is a file that **must be self-contained for the runtime to work** — e.g. a worker entrypoint (`serve.ts` / `main.ts`) loaded as raw source inside a spawned worker thread, which therefore _cannot_ import from a core alias or sibling centralized files, so its guards/types live inline by necessity (and say so in a comment). Such files are rare and load-bearing; everything else extracts, no exceptions. Run a dedicated clean-up sweep to confirm no impl file harbors a stray local type/const/helper — and that **no centralized file harbors ANY non-exported member at all**. Every declaration in a centralized file is `export`ed: a `type` in `types.ts`, a `const` in `constants.ts`, a guard in `validators.ts`, a parser in `parsers.ts`, a function in `helpers.ts` — all public, no exceptions. A non-exported member in a centralized file (even a private helper type aliased only by another type in the same file) is itself a defect: `export` it (or, if it is a trivial single-use internal, fold it away entirely so nothing non-exported remains). The sole non-exported module-scope declarations anywhere are the runtime-self-contained exception's.
|
|
394
|
-
|
|
395
|
-
**Centralized files are kind-pure — the rule cuts both ways.** Extraction is not only _out of impl files_; each centralized file holds ONLY its kind, so a member in the wrong centralized file is as misplaced as one left in an impl file. `constants.ts` is the **sole** home for a module-scope constant — a `const` sitting in `helpers.ts` is misplaced and moves to `constants.ts` (or is promoted per the surface-root / core rules). `constants.ts` holds UPPER*SNAKE `Object.freeze`d **data**; a camelCase namespace whose members are \_functions* (a builder / utility DSL) is a **helper**, not a data constant, and lives in `helpers.ts`.
|
|
396
|
-
|
|
397
|
-
**`helpers.ts` is the exported, reusable-utility surface — every function in it is `export`ed.** A helper is shared infrastructure: exporting it makes it consolidatable and reachable by its own unit test. A would-be non-exported module-scope helper therefore has exactly two dispositions — **fold** it into its sole caller when it is a trivial, single-use internal (so no hidden function remains), or **extract + `export` + test** it when it is non-trivial or reusable (then route the other sites that hand-roll the same logic through it). Never leave a hidden, non-exported function loitering in `helpers.ts`. A reusable fragment — a regex, a JSON-body parse, a header flatten — is **one** exported helper that every site calls, **including tests and fixtures (§16.1)**; it is never reimplemented inline. A `factories.ts` / `compilers.ts` / `parsers.ts` is itself a centralized file, **not** an exemption: a factory's private glue step extracts to an exported `helpers.ts` function (a one-off used by a single factory still extracts — "only one caller" is not a reason to hide it), and a compiler's / parser's **pure** recursion branches stay **`export`ed** in `compilers.ts` / `parsers.ts` per the recursion rule above (a branch that must reach instance `#`state or a sibling method is a `#` private method instead — §7). The only non-exported module-scope function anywhere is in the runtime-self-contained file; every other free function — factory glue, compiler branch, guard step — is exported and unit-tested.
|
|
398
|
-
|
|
399
|
-
**`middlewares.ts` holds middleware factory functions — a middleware is a function, never a class.** A middleware is a behavior, not an entity: the `Middleware` contract is `(context, next) => …`, so each ships as a factory `createX(options): Middleware` that closes over its config — the right shape for a behavior (and the universal web idiom), never a class with a lone `handle` method. `middlewares.ts` is its kind-pure home, kept distinct from `factories.ts` (which constructs entities / managers / stores / route inputs / transports / data) so a request handler is never confused with an object constructor. A middleware's state is **closure-private by default** (a rate limiter's per-key tally); the moment a consumer must _address_ that state — swap it for a durable backend, share it, inspect it — the **state**, not the middleware, is extracted into a pluggable class taken as an option (default in-memory), exactly as a `createSession` takes a `SessionStore`. Reusable cross-middleware machinery (a cookie signer/parser, a multipart stream parser) is likewise a class/helper the factories compose. The only _manager_ of middleware is the spine's `MiddlewareManager` (the composition chain) — there is no per-middleware manager.
|
|
400
|
-
|
|
401
|
-
**Purpose of the centralized files:** deduplicate and consolidate. When a helper appears in (or is hand-rolled in) two places, extract it to one exported helper and route every site through it. When a constant is used across modules, centralize it. When an impl file grows a private local type or helper, extract it — the impl file stays a pure class.
|
|
402
|
-
|
|
403
|
-
**No function is ever declared or assigned inside another function or method body.** No local `function` declaration, no `const fn = () => {}` binding, no nested `function*` — regardless of how many call sites would share it. Extract the logic to the appropriate centralized module (or a `#` private method when it is inherently instance-bound — reaches `this`state or a sibling method, per §7) and import or call it from there. The ONLY function-valued expression allowed inside a body is an anonymous callback passed directly in argument position (an array `.map`, a `Timeout` handler, a `try`/`catch` wrapper). "Only one caller" is not an exemption — a single-use nested helper still evades the export-and-test law (§2.4) exactly as a hidden multi-use one would, so it extracts on the same terms as everything else in this section.
|
|
404
|
-
|
|
405
|
-
**Shared / core layer.** When multiple packages or surfaces exist, shared logic belongs in a central core/shared layer. Every cross-surface concept consolidates here. Other surfaces import from core; core never imports from them.
|
|
406
|
-
|
|
407
|
-
**Surface root vs. modules.** A surface that grows several modules (sub-domains, each its own folder with its own centralized files) keeps cross-module types and helpers at the **surface root** (`<surface>/types.ts`, `<surface>/helpers.ts`) and module-specific ones inside the module (`<surface>/<module>/types.ts`, `helpers.ts`, `errors.ts`, `constants.ts`). The surface barrel re-exports the root files and each module barrel, so everything still flows through the single top-level `index.ts`. Promote a member to the root only when **two or more modules** use it — a member used by one module stays in that module (the same rule the cross-surface core layer follows, one level down). A surface root's own cross-cutting surface is covered by behavioral tests (a `helpers.test.ts` mirroring the root); a module's documented surface is parity-tested against its own directory (§22).
|
|
408
|
-
|
|
409
|
-
**Entity sub-folders.** A module's implementation classes live one per file (§5 table); when a domain noun grows its own family — an entity plus its manager, or several sibling implementations of one shape — those class files nest in a sub-folder named for the entity (`phases/`, `tasks/`, `routes/`, `transports/`), co-locating each entity with its manager. The sub-folder holds **only** the entity's class files — its `{Entity}Interface` stays in the module-root `types.ts`, its `create*` factories in the module-root `factories.ts`, and the module barrel re-exports the nested classes; an entity sub-folder grows **no** centralized files of its own.
|
|
410
|
-
|
|
411
|
-
**Extension-category nesting.** Beyond a domain's plain entity sub-folders, a known **extension category** — a family of classes implementing a contract that is a _designed extension point_ (a driver seam, a store seam, a transport) — nests into a sub-folder named for the category (`drivers/`, `stores/`, `transports/`), **even at a single file**, because the category is a known growth point. This holds in BOTH directions: when a layer extends ANOTHER domain's contract (concrete drivers in a consumer surface implementing core's `DriverInterface`) AND within the domain that DEFINES the contract (the default in-memory driver alongside the interface). The contract **interface stays in the module-root `types.ts`** (the source of truth); only the concrete extension **classes** nest. The test is a designed GROWTH point, NOT a coincidental name: a module's OWN core primitives stay at root — native-object wrappers that each wrap one underlying object 1:1 are co-equal core primitives, so "Store" there is the native object, not a swappable persistence backend — they stay flat.
|
|
412
|
-
|
|
413
|
-
**Stores.** A store is an extension category (it nests in `stores/` per the rule above). There are two blessed molds, chosen by access shape: **point-access** (`get` / `set` / `delete` — address one record at a time; the `{X}StoreInterface` shape) and **bulk-restore** (`save` / `load` / `remove` / `clear` — persist and restore a whole collection, e.g. a queue that resumes its entire outstanding set). Either mold shares the same invariants: the stored value carries its **own** id (no separate id parameter on `set` / `save`), every primitive is async (returns a `Promise`), and a delete / remove of an absent key is a no-op (never throws). A store family follows extension-category nesting — the concrete stores (a memory store plus a database-backed store over the core persistence layer) nest in `stores/`, the `…StoreInterface` stays in the module-root `types.ts`, and the `create…Store` factories in `factories.ts`.
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
## 6. Barrel export rules
|
|
418
|
-
|
|
419
|
-
- `*/index.ts` is the **sole** public barrel — all public API flows through it.
|
|
420
|
-
- **NEVER** re-export from any non-index file — only `index.ts` may `export *`.
|
|
421
|
-
- **NEVER** re-export a symbol that originates in another package — update imports at the consumer site to point to the originating package.
|
|
422
|
-
- Each implementation file exports its class directly; `index.ts` re-exports with `export *`.
|
|
423
|
-
- Use `export type *` for types and `export *` for values.
|
|
424
|
-
- When a symbol moves between packages, update every import site — do not leave re-exports as shims.
|
|
425
|
-
|
|
426
|
-
```ts
|
|
427
|
-
export type * from './types.js'
|
|
428
|
-
export * from './constants.js'
|
|
429
|
-
export * from './errors.js'
|
|
430
|
-
export * from './validators.js'
|
|
431
|
-
export * from './helpers.js'
|
|
432
|
-
export * from './factories.js'
|
|
433
|
-
export * from './greeters/Greeter.js'
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
---
|
|
437
|
-
|
|
438
|
-
## 7. Class implementation structure
|
|
439
|
-
|
|
440
|
-
Order inside every class:
|
|
441
|
-
|
|
442
|
-
1. `#` private fields — context, options, status, result, child managers
|
|
443
|
-
2. Constructor — initialize context and options; instantiate child managers
|
|
444
|
-
3. Public API — getters and methods implementing the interface (single-word names per §4.1)
|
|
445
|
-
4. `#` private helper methods
|
|
446
|
-
|
|
447
|
-
Child managers (the §4.2.2 pattern) are stored as `#` private fields and exposed through `readonly` getters that return their interface type.
|
|
448
|
-
|
|
449
|
-
A **public method**, a **`#` private method**, and a **centralized helper** (§5) are three distinct roles — do not blur them. A public method implements the interface contract and is a genuine **composition** of helpers (and, when stateful, of `#` private methods); it is never a thin 1:1 delegate that only forwards to a single helper. A `#` private method is the imperative-shell counterpart — an internal step that must touch instance `#`state or call a sibling method, so it cannot be a pure helper. A centralized helper is a pure, referentially-transparent leaf with no instance state — the testable functional core, exported from `helpers.ts`. Push every pure leaf out to a helper; keep only genuinely stateful orchestration as a `#` private method.
|
|
450
|
-
|
|
451
|
-
**Purity is necessary but NOT sufficient for extraction — a leaf is not the same as a referentially-transparent method.** Apply the leaf test in order. (1) If the member reaches instance `#`state or calls a sibling method, it stays a `#` private method — done, it is not a candidate. (2) If it is pure, ask whether it is a **leaf** — a self-contained computation understandable and testable with zero knowledge of the owning class: a key/format/compare/convert/lookup/invert/instantiate step, a single unification, a stateless projection — or an **orchestration** — the recursive spine of an algorithm, or a composition that sequences leaves into the class's actual behavior: a chaining pass, a `solve`/`isolate`/`prove` routine, a relational join. A leaf is extracted to `helpers.ts`; an orchestration stays a `#` private method **even when it is referentially transparent and — once its own leaves are extracted — calls only helpers**, because it is still an algorithm, not a leaf. (3) A class's **defining engine internals** — the comparison core of a comparator, the empty-identity of an aggregator, the dispatch of an orchestrator — likewise stay methods: extracting them would reduce the class to a **thin delegate**, the same anti-pattern this section forbids for public methods, applied to the class itself. The operative question is not "does this read `this`?" but "is this a **leaf**, or is this the class's **behavior**?" When unsure, extract genuine leaves and keep recursive/compositional algorithm steps as `#` private methods — a class must remain a real composition of leaves, never a hollow shell that forwards to helpers.
|
|
452
|
-
|
|
453
|
-
---
|
|
454
|
-
|
|
455
|
-
## 8. Options design
|
|
456
|
-
|
|
457
|
-
This is the §4.2.1 strategy applied. Every options interface follows it.
|
|
458
|
-
|
|
459
|
-
- Top-level option keys are single words.
|
|
460
|
-
- Settings that need grouping live under a nested object whose **key is the entity** (`server`, `database`, `cache`), never a flat name with a prefix.
|
|
461
|
-
- Each leaf field is a single word (`port`, `timeout`, `path`).
|
|
462
|
-
- The `on` key is reserved everywhere for `EmitterHooks` (initial event listeners) — do not use it for anything else.
|
|
463
|
-
|
|
464
|
-
```ts
|
|
465
|
-
interface ServerOptions {
|
|
466
|
-
readonly on?: EmitterHooks<ServerEventMap>
|
|
467
|
-
readonly server?: { readonly port?: number; readonly timeout?: number }
|
|
468
|
-
readonly database?: { readonly path?: string }
|
|
469
|
-
}
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
Document each option's meaning under `@remarks`, not via long names.
|
|
473
|
-
|
|
474
|
-
---
|
|
475
|
-
|
|
476
|
-
## 9. Managers — accessors & batch operations
|
|
477
|
-
|
|
478
|
-
### 9.1 Accessor pattern
|
|
479
|
-
|
|
480
|
-
Managers expose items via **singular** and **plural** accessors named after the domain noun. Both names are single words, both backed by the parent's context.
|
|
481
|
-
|
|
482
|
-
| Accessor | Returns | Purpose |
|
|
483
|
-
| ------------- | ------------------------------ | ------------------ |
|
|
484
|
-
| `entity(key)` | `EntityInterface \| undefined` | Look up ONE by key |
|
|
485
|
-
| `entities()` | `readonly EntityInterface[]` | List ALL in order |
|
|
486
|
-
|
|
487
|
-
Example: `TimeoutManager` exposes `timeout(id)` and `timeouts()`; `AgentManager` exposes `agent(id)` and `agents()`. The Manager class itself is the entity context — the methods do not need a `Timeout`/`Agent` prefix.
|
|
488
|
-
|
|
489
|
-
### 9.2 Batch operation pattern
|
|
490
|
-
|
|
491
|
-
Verbs that act on collections take three overload shapes — **same single-word verb**, different parameters.
|
|
492
|
-
|
|
493
|
-
| Signature | Returns | Behavior |
|
|
494
|
-
| --------------- | --------- | --------------------------------------------- |
|
|
495
|
-
| `method()` | `void` | Applies to ALL items |
|
|
496
|
-
| `method(id)` | `boolean` | Applies to ONE item |
|
|
497
|
-
| `method(ids[])` | `boolean` | Applies to LISTED items (true if all succeed) |
|
|
498
|
-
|
|
499
|
-
Never split this into `methodAll`, `methodOne`, `methodMany` — the verb stays single-word and overloads carry the variation.
|
|
500
|
-
|
|
501
|
-
When the single-item type can itself be a list (a key type that includes arrays, or an open record), the one-item and array overloads overlap — declare the **array overload first** so a list resolves to the batch form, and document how to express a single list-valued item so it isn't read as a batch.
|
|
502
|
-
|
|
503
|
-
---
|
|
504
|
-
|
|
505
|
-
## 10. Lifecycle vocabulary (no synonyms)
|
|
506
|
-
|
|
507
|
-
| Verb | Meaning |
|
|
508
|
-
| --------- | ------------------------------------------ |
|
|
509
|
-
| `start` | Begin or restart an operation |
|
|
510
|
-
| `stop` | End an operation permanently |
|
|
511
|
-
| `pause` | Suspend an operation (resumable) |
|
|
512
|
-
| `resume` | Continue a paused operation |
|
|
513
|
-
| `skip` | Mark as intentionally not executed |
|
|
514
|
-
| `abort` | Cancel with signal propagation |
|
|
515
|
-
| `clear` | Reset state without destroying the entity |
|
|
516
|
-
| `destroy` | Tear down the entity and release resources |
|
|
517
|
-
| `execute` | Run the primary work to completion |
|
|
518
|
-
|
|
519
|
-
Do not introduce synonyms (`cancel` for `abort`, `reset` for `clear`, `run` for `execute`). Every lifecycle verb is single-word and has exactly one meaning across the entire codebase.
|
|
520
|
-
|
|
521
|
-
---
|
|
522
|
-
|
|
523
|
-
## 11. Immutability
|
|
524
|
-
|
|
525
|
-
- Never mutate inputs — copy-on-write for internal state.
|
|
526
|
-
- Public getters return copies or readonly views, never mutable references.
|
|
527
|
-
- Use `readonly T[]`, `ReadonlyMap<K, V>`, `ReadonlySet<T>` for return types and properties.
|
|
528
|
-
- **NEVER** use `readonly` on parameters.
|
|
529
|
-
|
|
530
|
-
---
|
|
531
|
-
|
|
532
|
-
## 12. Error handling & `Result`
|
|
533
|
-
|
|
534
|
-
| Condition | Strategy |
|
|
535
|
-
| ------------------------------------ | ---------------------------- |
|
|
536
|
-
| Programmer error / invalid arguments | `throw` (an `AppError`) |
|
|
537
|
-
| External operation (I/O, network) | `Result<T, E>` or throw |
|
|
538
|
-
| Optional lookup that may not exist | Return `undefined` |
|
|
539
|
-
| Inside a type guard | Return `false` — never throw |
|
|
540
|
-
|
|
541
|
-
### Result type
|
|
542
|
-
|
|
543
|
-
For operations that can succeed or fail without throwing:
|
|
544
|
-
|
|
545
|
-
```ts
|
|
546
|
-
interface Success<T> {
|
|
547
|
-
readonly success: true
|
|
548
|
-
readonly value: T
|
|
549
|
-
}
|
|
550
|
-
interface Failure<E> {
|
|
551
|
-
readonly success: false
|
|
552
|
-
readonly error: E
|
|
553
|
-
}
|
|
554
|
-
type Result<T, E = Error> = Success<T> | Failure<E>
|
|
4
|
+
> This is the authoritative root for how humans and agents write code in this project.
|
|
5
|
+
|
|
6
|
+
## Authority and loading
|
|
7
|
+
|
|
8
|
+
- These instructions apply to every project in this style; never import assumptions, names, or logic from another repository.
|
|
9
|
+
- The user's current instruction wins. Otherwise, this file and its linked rules outrank existing code. Existing code is evidence to verify, not ground truth.
|
|
10
|
+
- `*/types.ts` is authoritative for public APIs. Implementation and tests conform to it; never undo a user's type edit.
|
|
11
|
+
- `CLAUDE.md` governs Claude-specific orchestration only. It cannot weaken this coding contract.
|
|
12
|
+
- `.codex/config.toml` governs Codex-specific orchestration only. It cannot weaken this coding contract.
|
|
13
|
+
- `.agents/skills/` contains reusable workflows. An explicitly invoked or dispatch-named skill and its required references are binding process instructions, but cannot weaken this file, applicable rules, or the governing guide/spec.
|
|
14
|
+
- External delegates, including Cursor and Codex/OpenAI models, have no exemption. Every dispatch must restate the non-negotiables, applicable rules, guide/spec, and owned files; every result receives independent review.
|
|
15
|
+
- Before working, read in order:
|
|
16
|
+
1. this file;
|
|
17
|
+
2. every applicable file in `.claude/rules/` from the rule map below;
|
|
18
|
+
3. every explicitly invoked or dispatch-named skill and the references it requires;
|
|
19
|
+
4. `guides/README.md`, the matching guide/spec, and `ROADMAP.md` when present.
|
|
20
|
+
- Rules state **how to write**. Guides/specs state **what to build** and the domain workflow. When they conflict, stop and surface the conflict.
|
|
21
|
+
|
|
22
|
+
## Project model
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
src/ published library: core, browser, server, optional styles
|
|
26
|
+
app/ application: core, browser, server
|
|
27
|
+
tests/ mirrors source; setup*.ts owns shared test infrastructure
|
|
28
|
+
configs/ thin target wrappers around root Vite/TypeScript configuration
|
|
555
29
|
```
|
|
556
30
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
**
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
**
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
-
|
|
584
|
-
- **
|
|
585
|
-
-
|
|
586
|
-
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
- **
|
|
595
|
-
- **
|
|
596
|
-
- **
|
|
597
|
-
- **
|
|
598
|
-
|
|
599
|
-
**
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
### 16.1 Test helper pattern
|
|
631
|
-
|
|
632
|
-
Test helpers are shared infrastructure, not local clutter. A fixture, event factory, async wait, recorder, renderer, scenario builder, fake/scripted collaborator, or DOM builder belongs in a setup file — **extract it the moment it could serve another test, not only after it has been copied.** A helper duplicated (or near-duplicated) across two files is a defect to consolidate into one general form.
|
|
633
|
-
|
|
634
|
-
- Put general, environment-agnostic helpers in `tests/setup.ts` (no `node:*`, no `document`/`window`/Vue).
|
|
635
|
-
- Put environment-specific helpers in the matching setup file: `tests/setupServer.ts` (node-only, `node:fs` loaders anchored to `WORKSPACE_ROOT`), `tests/setupBrowser.ts` / `tests/setupStyles.ts` (DOM/Vue/CSS).
|
|
636
|
-
- Setup files must export every reusable helper, fixture type, factory, constant, and guard they define.
|
|
637
|
-
- Test files import helpers from setup files rather than defining local fixture factories.
|
|
638
|
-
- Helper names follow §4.3: `createRecorder`, `buildElement`, `appendItems`, `renderRows`, `waitForDelay`, `extractDetail`.
|
|
639
|
-
- Prefer small factory functions that seed a real scenario over inline setup blocks repeated across tests.
|
|
640
|
-
- **Reuse the framework's helper — never reimplement it in a test.** When the library already exports a helper for the job (a parser, a header flattener, a signer), the test or fixture imports it rather than hand-rolling its own copy — a fixture that re-implements framework logic is the same duplication defect, one layer over (§5).
|
|
641
|
-
|
|
642
|
-
Use a **recorder** instead of a test-framework spy when the test only needs to count calls or inspect arguments. A recorder is a real callback with recorded calls; it is not a mock of behavior.
|
|
643
|
-
|
|
644
|
-
```ts
|
|
645
|
-
interface TestRecorderInterface<TArgs extends readonly unknown[]> {
|
|
646
|
-
readonly calls: readonly TArgs[]
|
|
647
|
-
readonly count: number
|
|
648
|
-
readonly handler: (...args: TArgs) => void
|
|
649
|
-
clear(): void
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
Use a single delay helper instead of inline `setTimeout` promises:
|
|
654
|
-
|
|
655
|
-
```ts
|
|
656
|
-
export function waitForDelay(ms = 0): Promise<void> {
|
|
657
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
658
|
-
}
|
|
31
|
+
- `core` is host-independent. Browser and server may import core; core imports neither.
|
|
32
|
+
- `app/core` is host-independent. `app/server` may import app/core plus core/server libraries, never browser code. `app/browser` may import app/core plus core/browser libraries and reaches server behavior through shared contracts/transports, never Node/server implementation imports.
|
|
33
|
+
- Environment boundaries use the project toolchain directly: Oxlint restricts declared package, alias, and conventional relative imports; scoped TypeScript projects remove host globals from core and the opposite host; Vite resolves and builds the selected Vue/browser and Node/server graphs; and generated-consumer tests exercise those real configurations. Published source never imports private app code, core stays host-independent, and browser/server remain disjoint. Do not add a second parser or source-language analyzer to duplicate TypeScript, Oxlint, Vue, HTML, CSS, or Vite.
|
|
34
|
+
- `tsconfig.json`, `vite.config.ts`, and each `*/types.ts` are their respective sources of truth.
|
|
35
|
+
- Use only the environments the project needs; do not delete structural files merely because they are empty.
|
|
36
|
+
|
|
37
|
+
## Non-negotiable rules
|
|
38
|
+
|
|
39
|
+
- **NEVER** use `any`; accept `unknown` and narrow with guards.
|
|
40
|
+
- **NEVER** use non-null assertions (`!`) or type assertions (`as`); narrow or validate.
|
|
41
|
+
- **NEVER** use `@ts-nocheck`, `@ts-ignore`, `@ts-expect-error`, or `eslint-disable`; fix the cause.
|
|
42
|
+
- **NEVER** add an npm package unless the user explicitly requests it; prefer native APIs.
|
|
43
|
+
- **NEVER** remove a symbol to silence lint. Implement it or annotate `// TODO: [Feature] Brief purpose`.
|
|
44
|
+
- **NEVER** undo user edits in `*/types.ts`.
|
|
45
|
+
- **NEVER** put `readonly` on parameters.
|
|
46
|
+
- **NEVER** use TypeScript `private`; use runtime-enforced `#` fields.
|
|
47
|
+
- **NEVER** use default exports except where a framework requires them, such as Vue SFCs or config files.
|
|
48
|
+
- **NEVER** use mocks, behavioral fakes, module replacement, framework spies, or fake clocks to simulate project-owned behavior. Use real implementations, recorders, temporary resources, protocol-faithful fixture servers, and inert customizable data stubs.
|
|
49
|
+
- **ALWAYS** make interface properties and public return collections readonly.
|
|
50
|
+
- **ALWAYS** define reusable/public types in `*/types.ts` before implementation.
|
|
51
|
+
- **ALWAYS** inspect the exact declared/installed `@orkestrel/*` capabilities before implementing overlapping logic; reuse a primitive when its semantics match and do not wrap it merely to rename it.
|
|
52
|
+
- **ALWAYS** finish the requested implementation: no empty stubs, deferred logic, or concealed follow-up work.
|
|
53
|
+
- **ALWAYS** follow the repository's naming, placement, export, and dependency-direction rules exactly.
|
|
54
|
+
|
|
55
|
+
## Design laws
|
|
56
|
+
|
|
57
|
+
- **Types first.** Public contracts precede implementation.
|
|
58
|
+
- **Single-word entity APIs.** Properties, methods, option keys, and events use one descriptive word. If one word is insufficient, change the shape: group options, extract a sub-entity/manager, or split behaviors.
|
|
59
|
+
- **Self-describing helpers.** Module-scope helpers normally use `{verb}{Noun}` because they lack entity context.
|
|
60
|
+
- **One concept, one term.** Do not alternate synonyms; lifecycle verbs have fixed meanings.
|
|
61
|
+
- **Boolean behavior.** A binary behavioral switch is a boolean, not a two-literal union.
|
|
62
|
+
- **Real domain states only.** Literal unions represent irreducible modes, phases, discriminants, or external values—not decorative labels for already-represented facts.
|
|
63
|
+
- **Absence is `undefined`.** Never invent sentinels such as `'none'`, `'unset'`, `'unknown'`, `''`, or `-1`. Use `null` only when an external format distinguishes it from omission.
|
|
64
|
+
- **Derive state.** Compute facts from existing fields; do not store a second flag or label that can drift.
|
|
65
|
+
- **Named discriminants.** Name the axis that varies (`relationship`, `command`, `category`), never `kind` or `type`.
|
|
66
|
+
- **Centralize by kind.** Types, constants, helpers, validators, parsers, factories, errors, and similar declarations live in their designated centralized files. Implementation files contain one class plus imports.
|
|
67
|
+
- **Export and test reusable logic.** No hidden module helpers or declarations; fold trivial one-use logic into its caller or export it from the correct centralized module and test it.
|
|
68
|
+
- **No nested functions.** Extract function declarations/assignments from bodies. Anonymous callbacks passed directly as arguments and anonymous functions returned directly as results are the sole exceptions.
|
|
69
|
+
- **Functional core, imperative shell.** Export pure leaves; retain stateful or defining orchestration as class methods. Classes must compose behavior, not forward 1:1 to helpers.
|
|
70
|
+
- **No superfluous wrappers.** A wrapper must add a boundary, invariant, composition, translation, lifecycle, or materially narrower contract. Otherwise use or rename the real symbol and update every consumer.
|
|
71
|
+
- **Minimal public API.** Add capability with its real consumer; do not speculate. Prefer one minimal interface and one shared engine, allowing native backend overrides only for genuine faster paths.
|
|
72
|
+
- **No compatibility shims.** This is greenfield: update every consumer in the same change.
|
|
73
|
+
- **Mechanism, not product policy.** Framework code supplies reusable mechanisms and stops before application decisions.
|
|
74
|
+
- **No polling architecture.** Park idle work on events/abort signals; yield long work cooperatively.
|
|
75
|
+
|
|
76
|
+
## TTTDD: Types Then Tests Driven Development
|
|
77
|
+
|
|
78
|
+
1. **Types:** define or revise the contract in `*/types.ts`; entity-scoped names must satisfy the naming rules and properties must be readonly.
|
|
79
|
+
2. **Implementation:** conform exactly to the types; place every declaration in its prescribed file.
|
|
80
|
+
3. **Consolidation:** remove duplication and route repeated behavior through one shared implementation.
|
|
81
|
+
4. **Tests:** cover happy paths, edge cases, failures, and boundary values with targeted deterministic tests.
|
|
82
|
+
5. **Documentation:** update the matching guide/spec and parity coverage.
|
|
83
|
+
|
|
84
|
+
If the user changes a type mid-task, treat it as immediately authoritative. Type failures identify implementation that has not caught up.
|
|
85
|
+
|
|
86
|
+
For comprehensive hardening, research, centralization, contract adoption, real-service integration, or cross-package alignment, follow the applicable repository skill. No current-scope requirement may end as a TODO, skipped test, deferred row, or hidden follow-up.
|
|
87
|
+
|
|
88
|
+
## Work process
|
|
89
|
+
|
|
90
|
+
1. **Understand:** clarify scope and entities; read `*/types.ts`, the rule files, the applicable skill, and the matching guide before editing.
|
|
91
|
+
2. **Research:** when requested or externally material, verify current primary sources and build a capability/defect matrix before changing the API.
|
|
92
|
+
3. **Design:** change types first and typecheck the proposed contract.
|
|
93
|
+
4. **Implement:** match the interface, reuse declared ecosystem primitives, extract centralized logic, and update the sole barrel.
|
|
94
|
+
5. **Consolidate:** remove duplication, nested declarations, and superfluous wrappers without expanding the API.
|
|
95
|
+
6. **Test:** mirror source structure, challenge the applicable seams with real implementations, and run the narrowest relevant project.
|
|
96
|
+
7. **Document:** update the guide, examples, and parity contract.
|
|
97
|
+
8. **Verify:** audit discovery/deferrals/package contents as applicable, run the required gates, and read their actual output before claiming success.
|
|
98
|
+
|
|
99
|
+
Quality gates before commit, in order:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
npm run format → npm run lint → npm run check → npm run build → npm test
|
|
659
103
|
```
|
|
660
104
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
Browser tests run in a real browser. Treat that as a feature, not something to fake away.
|
|
666
|
-
|
|
667
|
-
- Do not mock or replace browser APIs such as DOM events, storage, observers, viewports, layout methods, or pointer/drag primitives unless the browser truly lacks the API.
|
|
668
|
-
- Prefer real DOM nodes, real events, real layout styles, and real browser observers.
|
|
669
|
-
- Centralize browser event factories: `createPointerEvent`, `createDragEvent`, `typeInput`, `fireTransitionEnd`.
|
|
670
|
-
- Centralize DOM fixture builders: `createButtonElement`, `createDropdownElements`, `createModalElement`, etc.
|
|
671
|
-
- Assert observable behavior: DOM state, emitted events, callback records, focus, classes, attributes, and public API state.
|
|
672
|
-
- Avoid asserting implementation details such as internal timers, private state, or framework scheduler internals.
|
|
673
|
-
|
|
674
|
-
Use fake timers only for deterministic timer-driven behavior where waiting in real time would make the suite slower or flaky. Do not use fake timers as a substitute for testing real browser interactions.
|
|
675
|
-
|
|
676
|
-
### 16.3 Test runner configuration
|
|
677
|
-
|
|
678
|
-
Keep test runner configuration boring and minimal. Add special provider, browser, teardown, timeout, parallelism, cache, or launch settings only when there is a measured problem and a targeted setting fixes it.
|
|
679
|
-
|
|
680
|
-
- Prefer defaults until there is evidence they are wrong.
|
|
681
|
-
- Keep provider setup in one helper or one shared config block.
|
|
682
|
-
- Avoid long launch-flag lists unless each flag has a current, verified purpose.
|
|
683
|
-
- Avoid persistent browser contexts unless a test genuinely needs persisted browser state.
|
|
684
|
-
- If browser teardown is slow, first check test cleanup, open handles, file parallelism, and per-file browser context churn before adding launch flags.
|
|
685
|
-
- Remove exploratory configuration once the real cause is fixed.
|
|
686
|
-
- Comments in config should explain the current reason for a setting, not the history of failed attempts.
|
|
687
|
-
|
|
688
|
-
---
|
|
689
|
-
|
|
690
|
-
## 17. Workspace & multi-environment layout
|
|
691
|
-
|
|
692
|
-
A workspace has two top-level surfaces — a **library** (`src/`) and an **application** (`app/`) — each split into the environments it needs: `core`, `browser`, `server`, and (for `src/`) an optional `styles` bundle. A project uses only the subset it needs; the structure is identical regardless.
|
|
693
|
-
|
|
694
|
-
### 17.1 Surfaces
|
|
695
|
-
|
|
696
|
-
- `src/core/` — shared library code for browser and server consumers.
|
|
697
|
-
- `src/browser/` — browser-only library code.
|
|
698
|
-
- `src/server/` — server-only library code.
|
|
699
|
-
- `src/styles/` — optional SCSS bundle compiled to a shippable `index.css` (entry `index.ts` is a side-effect `import './index.scss'`).
|
|
700
|
-
- `app/core/` — shared application logic used by browser and server entrypoints; has an `index.ts` barrel.
|
|
701
|
-
- `app/browser/` — browser entry + browser-specific code; `main.ts` is the entrypoint (not a barrel). Typically grows `components/`, `pages/`, `composables.ts`, `controllers/`, `services/`, `stores/`.
|
|
702
|
-
- `app/server/` — server entry + server-specific code; `main.ts` is the entrypoint. Typically grows `handlers/`, `middlewares.ts`, `routes.ts`.
|
|
703
|
-
|
|
704
|
-
**Dependency direction:** browser and server import from core; core never imports from browser or server. Application surfaces import from library surfaces.
|
|
705
|
-
|
|
706
|
-
### 17.2 Alias policy
|
|
707
|
-
|
|
708
|
-
- `@src/core` → `src/core/index.ts`
|
|
709
|
-
- `@src/browser` → `src/browser/index.ts`
|
|
710
|
-
- `@src/server` → `src/server/index.ts`
|
|
711
|
-
- `@src/styles` → `src/styles/index.ts`
|
|
712
|
-
- `@app/core` → `app/core/index.ts`
|
|
713
|
-
- `@app/browser` → `app/browser/index.ts`
|
|
714
|
-
- `@app/server` → `app/server/index.ts`
|
|
715
|
-
|
|
716
|
-
Keep aliases aligned between `tsconfig.json` and `vite.config.ts` — `vite.config.ts` derives its aliases from `tsconfig.json` `compilerOptions.paths`, so **add a new alias to `tsconfig.json` first**.
|
|
717
|
-
|
|
718
|
-
### 17.3 Source of truth & config wrappers
|
|
719
|
-
|
|
720
|
-
- `tsconfig.json` is the shared TypeScript source of truth.
|
|
721
|
-
- `vite.config.ts` is the shared Vite source of truth (build, test projects, environment loading, aliases).
|
|
722
|
-
- `*/types.ts` files are the source of truth for every public API.
|
|
723
|
-
- Files under `configs/src/`, `configs/app/` (and `configs/bin/` if present) are **thin** target-specific wrappers around the root configs — keep root config generic and reusable; do not move shared logic out of the root files.
|
|
724
|
-
|
|
725
|
-
### 17.4 Environment rules
|
|
726
|
-
|
|
727
|
-
- `vite.config.ts` owns environment loading and mapping.
|
|
728
|
-
- Introduce new shared environment variables in `vite.config.ts` first.
|
|
729
|
-
- Prefer a minimal plain variable set (e.g. `APP_NAME`, `APP_API_PATH`, `APP_HOST`, `APP_PORT`).
|
|
730
|
-
- Only expose additional browser runtime values when there is a concrete need.
|
|
731
|
-
|
|
732
|
-
### 17.5 Build outputs
|
|
733
|
-
|
|
734
|
-
| Output | Holds | Format |
|
|
735
|
-
| ------------------ | ---------------------------------- | ------ |
|
|
736
|
-
| `dist/src/core` | core library | `cjs` |
|
|
737
|
-
| `dist/src/browser` | browser library | `es` |
|
|
738
|
-
| `dist/src/server` | server library | `cjs` |
|
|
739
|
-
| `dist/src/styles` | compiled SCSS bundle (`index.css`) | `es` |
|
|
740
|
-
| `dist/app/browser` | browser application build | — |
|
|
741
|
-
| `dist/app/server` | server application build | `cjs` |
|
|
742
|
-
| `dist/showcase` | single-file demo (`index.html`) | — |
|
|
743
|
-
|
|
744
|
-
Declaration emit for the TS library surfaces is handled by `tsc` via the tsconfigs under `configs/src/`, chained after each `vite build` step. The **styles** surface ships CSS, not declarations. The optional **showcase** (`appShowcase` → `configs/app/vite.showcase.config.ts`) bundles `app/browser` into one self-contained `dist/showcase/index.html` via `vite-plugin-singlefile` — it opens straight from `file://`, JS minified by oxc and CSS by Lightning CSS (both bundled with Vite). It is a distributable demo kept out of the default `build`; a build stamp (an injected `build-id` meta tag) makes rebuilds cache-bust under `file://`.
|
|
745
|
-
|
|
746
|
-
### 17.6 Testing structure
|
|
747
|
-
|
|
748
|
-
- `tests/setup.ts` — shared, environment-agnostic setup entry.
|
|
749
|
-
- `tests/setupBrowser.ts` — browser-only setup (wires `./setup.css`; DOM/Vue helpers).
|
|
750
|
-
- `tests/setupServer.ts` — server-only setup (`node:fs` loaders, `WORKSPACE_ROOT`).
|
|
751
|
-
- `tests/setupStyles.ts` — style setup (loads `./setup.css` + the compiled cascade; CSS assertion primitives).
|
|
752
|
-
- `tests/setup.css` — declares the cascade-layer order before `@import 'tailwindcss'` (+ `@source`).
|
|
753
|
-
- Prefer test filenames that follow the source entrypoint (`index.test.ts` for `index.ts`, `main.test.ts` for `main.ts`).
|
|
754
|
-
|
|
755
|
-
Vitest splits into one project per surface×environment, configured in `vite.config.ts` (`node` for core/server, real Chromium via `@vitest/browser-playwright` for browser/styles):
|
|
756
|
-
|
|
757
|
-
| Project | Files | Environment | Setup files |
|
|
758
|
-
| ------------- | ---------------------- | ------------------------------- | ------------------------------------------------- |
|
|
759
|
-
| `src:core` | `tests/src/core/**` | `node` | `setup.ts` |
|
|
760
|
-
| `src:browser` | `tests/src/browser/**` | Playwright (Chromium, headless) | `setup.ts` + `setupBrowser.ts` |
|
|
761
|
-
| `src:server` | `tests/src/server/**` | `node` | `setup.ts` + `setupServer.ts` |
|
|
762
|
-
| `src:styles` | `tests/src/styles/**` | Playwright (Chromium, headless) | `setup.ts` + `setupBrowser.ts` + `setupStyles.ts` |
|
|
763
|
-
| `app:core` | `tests/app/core/**` | `node` | `setup.ts` |
|
|
764
|
-
| `app:browser` | `tests/app/browser/**` | Playwright (Chromium, headless) | `setup.ts` + `setupBrowser.ts` |
|
|
765
|
-
| `app:server` | `tests/app/server/**` | `node` | `setup.ts` + `setupServer.ts` |
|
|
766
|
-
|
|
767
|
-
Scope tests by surface/environment — `npm run test:src`, `npm run test:src:core`, `npm run test:app`, `npm run test:app:server`, etc.
|
|
768
|
-
|
|
769
|
-
### 17.7 Scoped checks & type-isolation
|
|
770
|
-
|
|
771
|
-
`check` is the single **comprehensive** typecheck — `vue-tsc --noEmit --project tsconfig.json`, one pass over the root all-types tsconfig that type-checks the **whole tree** (src, app, tests, `configs/**`, `vite.config.ts`) and gives IDE parity. It uses `vue-tsc` because the tree spans `app/browser/*.vue`. That single root pass IS `check`. Linting is a **separate gate** (`lint`, §17.8): `oxlint` is not a type-checker and `tsc` does not enforce the house rules, so the two are complementary and run as distinct steps, not nested.
|
|
772
|
-
|
|
773
|
-
Alongside `check`, a `check:<scope>` script family mirrors the `test:*` projects as **separate, on-demand isolation checks** — NOT part of `check`. You run them (e.g. `npm run check:src && npm run check:app`, or a single granular scope) to enforce that each surface stays within its environment's strict `lib`/`types` contract:
|
|
774
|
-
|
|
775
|
-
- `check:src` — runs `check:src:{core,browser,server,styles}` (the strict per-env source scopes).
|
|
776
|
-
- `check:app` — runs `check:app:{core,browser,server}` (the strict per-env app scopes).
|
|
777
|
-
|
|
778
|
-
These scoped checks are the **isolation tool**, distinct from the everyday comprehensive `check`.
|
|
779
|
-
|
|
780
|
-
**Tooling per scope — `tsc` for pure-TS scopes, `vue-tsc` only where `.vue` is type-checked.** Only `app/browser` contains `.vue` files, so the whole-tree `check` (the root pass) and the scoped `check:app:browser` use `vue-tsc`; every other scope (`check:src:{core,browser,server,styles}`, `check:app:core`, `check:app:server`) uses plain `tsc`. Rationale: plain `tsc` treats a `*.vue` import as the opaque shim declared in `app/browser/env.d.ts` and silently skips the `.vue` internals (templates, `<script setup>`); `vue-tsc` type-checks inside them. So `vue-tsc` runs exactly where `.vue` is checked and nowhere else — which also matches the build, where the src faces are already emitted with plain `tsc`.
|
|
781
|
-
|
|
782
|
-
Each scoped check enforces **environment type-isolation** via a per-env `lib`/`types` contract, so a surface can only reach the host globals its environment actually has:
|
|
783
|
-
|
|
784
|
-
| Check scope | `lib` | `types` | Host globals available |
|
|
785
|
-
| ------------------------------------ | --------------------------------- | ----------------- | -------------------------------------------------------------------------------- |
|
|
786
|
-
| `check:src:core` / `:app:core` | `["ESNext"]` | `[]` | **none** — pure ECMAScript: no DOM, no Node (not even `crypto`/`console`/timers) |
|
|
787
|
-
| `check:src:browser` / `:app:browser` | `["ESNext","DOM","DOM.Iterable"]` | — | DOM globals; no Node |
|
|
788
|
-
| `check:src:server` / `:app:server` | `["ESNext"]` | `["node"]` | Node globals; no DOM |
|
|
789
|
-
| `check:src:styles` | `["ESNext"]` | `["vite/client"]` | `vite/client` only (for the `*.scss` module decl); no DOM, no Node |
|
|
790
|
-
|
|
791
|
-
**Strict core is the load-bearing rule.** `core` and `app:core` are **pure ECMAScript** — a host-dependent utility cannot live there. A helper that needs a host global belongs in the environment face that has it: e.g. `generateId` (needs the host `crypto` global) lives in the **server** face (`src/server/helpers.ts`), not `@src/core`.
|
|
792
|
-
|
|
793
|
-
The per-env contracts **unify with the build**: `configs/src/tsconfig.{core,browser,server}.json` (build/emit) double as the scoped-check configs (`tsc --noEmit -p`); the net-new check-only configs are `configs/src/tsconfig.styles.json` and `configs/app/tsconfig.core.json` (`configs/app/tsconfig.{browser,server}.json` were already check-only). The root `tsconfig.json` keeps **all** libs/types so the IDE stays happy — and the comprehensive `check` runs against it; the per-env scoped checks are what tighten each surface on top.
|
|
794
|
-
|
|
795
|
-
### 17.8 Script intent
|
|
796
|
-
|
|
797
|
-
- `npm run dev` — browser development entrypoint.
|
|
798
|
-
- `npm run build` — build all configured library and application targets.
|
|
799
|
-
- `npm run serve` / `serve:build` — run the built server / build all then run it.
|
|
800
|
-
- `npm run showcase` — dev server for the single-file showcase.
|
|
801
|
-
- `npm run build:showcase` — build the single-file showcase to `dist/showcase`.
|
|
802
|
-
- `npm run show` — build the showcase and copy it to `demo/showcase.html`. Run it AFTER the `format` gate, never before: `demo/showcase.html` is committed in its generated, minified form, and `npm run format` would otherwise expand the single file's inlined bundle into tens of thousands of lines (oxfmt has no ignore mechanism to exempt it).
|
|
803
|
-
- `npm run lint` — `oxlint --config .oxlintrc.json --fix .`: the whole-tree house-rules linter (bans `any`/non-null `!`/default exports/unused vars, enforces `import type`, etc.). Its own gate — complementary to, and separate from, `check` (§17.7).
|
|
804
|
-
- `npm run check` — the comprehensive typecheck: `vue-tsc --noEmit --project tsconfig.json`, one root pass over the whole tree (§17.7). For on-demand environment isolation, run the separate scoped checks — `check:src`, `check:src:core`, `check:app`, `check:app:server`, etc. (§17.7).
|
|
805
|
-
- `npm run format` — format all files.
|
|
806
|
-
- `npm run clean` — remove the `dist/` directory.
|
|
807
|
-
- `npm run copy <from> <to>` — copy a file (creates parent dirs).
|
|
808
|
-
- `npm run tmp:txt` — rename non-markdown files in `tmp/` to `.txt`.
|
|
809
|
-
- `npm run prepublishOnly` — the full gate sequence before publishing: `format` → `lint` → `check` → `build` → `test`.
|
|
810
|
-
|
|
811
|
-
### 17.9 Tooling
|
|
812
|
-
|
|
813
|
-
- **Type checker:** `vue-tsc` (wraps `tsc` with Vue SFC support) only where `.vue` is checked (the whole-tree `check` root pass, and the scoped `check:app:browser`); plain `tsc` everywhere else (§17.7).
|
|
814
|
-
- **Linter:** `oxlint` (`.oxlintrc.json`, run with `--fix`) — its own gate, not part of `check`.
|
|
815
|
-
- **Formatter:** `oxfmt` (`.oxfmtrc.json`).
|
|
816
|
-
- **Bundler:** Vite.
|
|
817
|
-
- **Test runner:** Vitest with `@vitest/browser-playwright` for browser tests.
|
|
818
|
-
- **Node target:** `node24` for core and server builds.
|
|
819
|
-
- **Browser framework:** Vue 3 (when a browser app is present).
|
|
820
|
-
|
|
821
|
-
---
|
|
822
|
-
|
|
823
|
-
## 18. Vue / browser rules
|
|
824
|
-
|
|
825
|
-
Applies when an `app/browser/` surface uses Vue.
|
|
826
|
-
|
|
827
|
-
- **No `$emit`.** Use props, controllers, stores, services, and composables for all reactivity — they must be reactive without `$emit`. A composable returns **readonly refs** + methods (e.g. `useTheme(): { mode: Readonly<Ref<ThemeMode>>; toggle(): void }`), so consumers can't mutate the ref directly.
|
|
828
|
-
- **Layout via utilities, theme via tokens.** Use the CSS framework's utility classes for layout (flex, gap, spacing, max-width) and the framework's token-driven classes (`.surface`, `.button`, `.muted`, `.accent`) for color, so a `[data-theme]` flip re-themes everything. Only add custom SCSS when no utility exists, and hook into the framework's SASS variables/functions rather than hand-rolling values. Never hand-roll a literal color.
|
|
829
|
-
- Prefer native platform APIs (Popover, `<dialog>`, `hashchange` routing) over reinvented widgets or extra libraries.
|
|
830
|
-
|
|
831
|
-
---
|
|
832
|
-
|
|
833
|
-
## 19. SCSS conventions
|
|
834
|
-
|
|
835
|
-
When a project ships SCSS, the styles layer mirrors the TypeScript centralization principles (§5) — read the parallel TS section first. The concrete token vocabulary is project-specific (a Bootstrap project uses `--bs-*`, a Tailwind project uses `--set-*`, the reference template uses `--app-*`); the structural rules below are universal.
|
|
836
|
-
|
|
837
|
-
### 19.1 Centralized files (the §5 analogue for SCSS)
|
|
838
|
-
|
|
839
|
-
| TypeScript | SCSS |
|
|
840
|
-
| ----------------------------- | ------------------------------------------------------------------------- |
|
|
841
|
-
| `helpers.ts` — pure functions | `_mixins.scss` — `@function` (returns a value) and `@mixin` (emits CSS) |
|
|
842
|
-
| `constants.ts` — UPPER_SNAKE | `_tokens.scss` — `:root` custom-property tokens + cascade-layer order |
|
|
843
|
-
| _(theme overrides)_ | `_theme.scss` — retunes tokens under theme selectors (`[data-theme='…']`) |
|
|
844
|
-
| `*/index.ts` — sole barrel | `index.scss` — sole compilation barrel (`@use 'tokens'`, `'theme'`, …) |
|
|
845
|
-
|
|
846
|
-
`_mixins.scss` is the **mixin / function registry**. It emits no top-level CSS — every consumer reaches it with `@use '../mixins' as *` and calls its mixins / reads its lists. **Never** `@use 'mixins'` from `index.scss`.
|
|
847
|
-
|
|
848
|
-
`_tokens.scss` is the **token source of truth** and declares the cascade-layer order. Token names are part of the **public API** — adding a token is allowed; renaming or removing one is a breaking change. `_theme.scss` retunes tokens under theme selectors; partials never override tokens.
|
|
849
|
-
|
|
850
|
-
### 19.2 Three tools, three roles
|
|
851
|
-
|
|
852
|
-
- **`@function`** — when you need a _value_ to plug into a property. Pure, no side effects, returns one CSS value.
|
|
853
|
-
- **`@mixin`** — when you need to _emit declarations_. May take `@content`. Owns boilerplate like the transition + `prefers-reduced-motion` pair.
|
|
854
|
-
- **`%placeholder`** — only for sharing _within a single partial_. Placeholders are NOT reachable across `@use` boundaries; cross-file sharing flows through `@mixin`.
|
|
855
|
-
|
|
856
|
-
### 19.3 What partials must never do (mirrors §21)
|
|
857
|
-
|
|
858
|
-
- **Never invent a new token** without checking `_tokens.scss` first. New tokens belong in `_tokens.scss` (global) or on the component/element selector (component-scoped), not buried in an unrelated partial.
|
|
859
|
-
- **Never write a literal color.** Every color flows through a `var(--token)` reference (or a `color-mix()` of those).
|
|
860
|
-
- **Never write a hand-rolled per-color/per-variant block** when the structure is shared — collapse it into a single `@each` loop over a shared list.
|
|
861
|
-
- **Never duplicate logic across partials.** If a pattern appears in ≥2 partials, lift it to `_mixins.scss`. A pattern used by only one partial stays inline — adding a mixin for a single caller is over-abstraction.
|
|
862
|
-
- **Never `@extend` across partials.** Sass `@extend` collapses selectors at compile time; sharing flows through tokens and mixins, not Sass inheritance.
|
|
863
|
-
- **Never pair a `transition:` without a `prefers-reduced-motion: reduce` opt-out.** Use the project's `transition` mixin, which emits both. Animations use `@include reduced-motion { animation: none }`.
|
|
864
|
-
- **Never wrap rules in a foreign cascade layer.** Each partial wraps its rules in its own folder's `@layer`, so utilities reliably win. Declare the layer order once in the consumer entry, before `@import 'tailwindcss'`.
|
|
865
|
-
|
|
866
|
-
### 19.4 Naming
|
|
867
|
-
|
|
868
|
-
| Kind | Casing | Pattern | Examples |
|
|
869
|
-
| ---------------- | --------------------- | --------------------------------- | ------------------------------- |
|
|
870
|
-
| Function | lowercase, kebab-case | `{verb}` or `{noun}` | `tint`, `clamp` |
|
|
871
|
-
| Mixin | lowercase, kebab-case | verb / verb-noun | `reduced-motion`, `transition` |
|
|
872
|
-
| Sass `$variable` | lowercase, kebab-case | `!default` if overridable | `$variants`, `$breakpoints` |
|
|
873
|
-
| Custom property | project token scheme | `--{scope}-{property}[-modifier]` | `--app-surface`, `--bs-...` |
|
|
874
|
-
| Modifier class | bare adjective/noun | `.{name}` | `.surface`, `.muted`, `.accent` |
|
|
875
|
-
| State class | bare adjective | §10 lifecycle vocabulary | `.active`, `.disabled` |
|
|
876
|
-
|
|
877
|
-
State class names use the §10 lifecycle vocabulary. The composable owns interactivity state (which class is on the element, when transitions fire, what `aria-*` is set); the partial owns visual chrome. They meet at the **class name** and the **transition token** — both stable contracts.
|
|
878
|
-
|
|
879
|
-
---
|
|
880
|
-
|
|
881
|
-
## 20. Workflow & recovery
|
|
882
|
-
|
|
883
|
-
**Development process:**
|
|
884
|
-
|
|
885
|
-
1. **Understand** — clarify scope, identify entities (the core nouns), read `*/types.ts` first.
|
|
886
|
-
2. **Design types (ALWAYS FIRST)** — open `*/types.ts`; apply §4.1 (every entity-scoped name single-word; if you can't, split per §4.2). Helpers follow §4.3. Mark all properties `readonly`. Typecheck to validate.
|
|
887
|
-
3. **Implement** — create the file in its domain folder, import types from `../types.js`, implement the class to match the interface exactly with `#` fields, extract helpers/constants/factories to centralized files, add `export *` to `index.ts`.
|
|
888
|
-
4. **Consolidate** — extract duplicated logic; simplify internals while preserving the public API.
|
|
889
|
-
5. **Test** — mirror structure, import via `.js` extensions, cover happy path + edges + errors, run the targeted project.
|
|
890
|
-
6. **Document** — update the relevant guide with new types, methods, and behavior.
|
|
891
|
-
|
|
892
|
-
**Quality gates (all pass before commit, in order):** `npm run format` · `npm run lint` (house rules) · `npm run check` (typecheck) · `npm run build` · `npm test` (targeted).
|
|
893
|
-
|
|
894
|
-
**Recovery:**
|
|
895
|
-
|
|
896
|
-
- _Type error_ → read the full message; check the implementation against `*/types.ts` (usual fixes: missing `readonly`, wrong return type, missing method); re-run typecheck after each fix.
|
|
897
|
-
- _Unused-symbol lint warning_ → **STOP**, do NOT delete. Is it defined in `*/types.ts`? If so, implement it; if blocked, annotate `// TODO: [Feature] Brief purpose`. Never remove a symbol just to satisfy the linter.
|
|
898
|
-
- _A compound name appears on an entity member_ → **STOP**; it's a design signal, not a naming problem. Apply §4.2 (nested options, sub-entity property, or separate functions), updating `*/types.ts` first. This does NOT apply to standalone helpers in `helpers.ts` — those use `{verb}{Noun}` (§4.3).
|
|
899
|
-
|
|
900
|
-
---
|
|
901
|
-
|
|
902
|
-
## 21. Architecture rules
|
|
903
|
-
|
|
904
|
-
- `*/types.ts` is the source of truth for the public API — never contradict it; implementation matches it exactly.
|
|
905
|
-
- Entity members (properties, methods, options, events) are single-word per §4.1; if you can't, you split per §4.2 — there is no third option. Standalone helpers use `{verb}{Noun}` per §4.3 (no entity context).
|
|
906
|
-
- Do not expand the public API without concrete multi-site need.
|
|
907
|
-
- Build a capability with its real consumer, not speculatively — defer a piece until the thing that needs it exists, then build it against that concrete consumer.
|
|
908
|
-
- Centralize shared logic — if two surfaces need it, it belongs in core. Deduplicate aggressively — if a pattern appears twice, extract it.
|
|
909
|
-
- **Minimal interface, one engine, native overrides.** When several backends satisfy one capability, keep the interface the smallest set of primitives the capability needs and put the shared logic (querying, paging, aggregation, …) in **one engine over those primitives**, so a new backend implements only the primitives and inherits the rest. A backend may override a generic operation with a **native** one where it has a faster path (an indexed count, a key-range scan), falling back to the engine otherwise — but never re-implements the whole engine per backend.
|
|
910
|
-
- Keep everything generic and reusable. Do not bring in logic from unrelated projects.
|
|
911
|
-
- Do not remove structural files just because they are currently empty.
|
|
912
|
-
- Prefer the smallest valid implementation that preserves the intended architecture.
|
|
913
|
-
- **No backwards-compatibility shims.** Greenfield: change types, rename symbols, restructure freely, and update every consumer in the same change. Never carry deprecation aliases or "for backwards-compat" branches.
|
|
914
|
-
- **No busy-loops, no recursive microtasks as architecture.** An idle loop **parks** on an abort / event wakeup rather than polling; long work yields cooperative quanta rather than recursing microtasks.
|
|
915
|
-
- **Mechanism, never policy.** A server / framework helper ships the reusable mechanism (HMAC, CORS, body parsing, sessions, cookies, CSRF, static serving, …) and stops at the line where application / product decisions begin (user models, login flows, authorization).
|
|
105
|
+
- Use scoped checks/tests during development; do not run the whole suite casually.
|
|
106
|
+
- Type error: read the complete diagnostic, compare implementation with `*/types.ts`, fix one cause, and rerun the relevant check.
|
|
107
|
+
- Unused contract symbol: stop; implement it or leave the prescribed TODO—never delete it for lint.
|
|
108
|
+
- Compound entity member: stop; correct the API shape before implementation. This does not apply to descriptive module helpers.
|
|
916
109
|
|
|
917
|
-
|
|
110
|
+
## Rule map
|
|
918
111
|
|
|
919
|
-
|
|
112
|
+
All files below are normative extensions of this root. Read every rule relevant to the files or concepts you touch; path frontmatter only controls Claude's automatic loading.
|
|
920
113
|
|
|
921
|
-
|
|
114
|
+
| Rule | Governs |
|
|
115
|
+
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
116
|
+
| `.claude/rules/names.md` | Identifiers, API shape, acronyms, lifecycle vocabulary, files/folders |
|
|
117
|
+
| `.claude/rules/typescript.md` | TypeScript syntax, imports, immutability, errors, TSDoc |
|
|
118
|
+
| `.claude/rules/architecture.md` | Centralized files, exports, classes, modules, extension points, stores |
|
|
119
|
+
| `.claude/rules/patterns.md` | Options, managers, emitters, guards, parsers, contracts |
|
|
120
|
+
| `.claude/rules/tests.md` | Test behavior, helpers, browser tests, Vitest configuration |
|
|
121
|
+
| `.claude/rules/workspace.md` | Src/app environments, aliases, isolation, builds, scripts, tooling |
|
|
122
|
+
| `.claude/rules/application.md` | App composition, entries, manifest safety, lifecycle, integration |
|
|
123
|
+
| `.claude/rules/browser.md` | Vue/browser architecture and platform usage |
|
|
124
|
+
| `.claude/rules/styles.md` | SCSS/CSS centralization, tokens, mixins, layers, naming |
|
|
125
|
+
| `.claude/rules/documentation.md` | Guides, parity, roadmap, showcase, examples |
|
|
126
|
+
| `.claude/rules/quality.md` | Research, dependency reuse, hardening, completion, package inspection |
|
|
922
127
|
|
|
923
|
-
|
|
924
|
-
- **Guides are parity-tested.** Every backticked API in a guide must resolve to a real export, and every public export should be documented. TS ↔ SCSS ↔ Markdown drift fails CI — that is the system working, not a test to suppress.
|
|
925
|
-
- **Parity reaches public methods, not just exports.** For class- and interface-heavy surfaces, a guide also documents each behavioral interface's public methods — a `## Methods` section, one table per interface keyed by its backticked name. Parity then asserts the documented methods are exactly the interface's real call-signature members (its `readonly` data members stay in the `## Surface` row), and that each implementing class exposes exactly its interface's methods — no extra public surface.
|
|
926
|
-
- **A guide scopes parity to its concept's source.** Usually that is one module's directory; a layer concept (the database layer, say) may span its core module and the backend-driver modules that implement it. A surface root's cross-cutting helpers are covered by behavioral tests rather than a guide.
|
|
927
|
-
- **`AGENTS.md` is the single source of truth** for conventions, for both humans and AI. Keep instructions here, not duplicated elsewhere.
|
|
928
|
-
- **`guides/README.md` is the map** — a dual-axis index (by-concept: spec ↔ source ↔ tests ↔ showcase; and by-directory). Read it first to build a mental model.
|
|
929
|
-
- **`ROADMAP.md` is the plan of record** — upcoming work as sequenced chunks, each gated green before the next; read it to see where the project is headed and why.
|
|
930
|
-
- **A showcase, when present, is proof.** If the library ships a public API, the app demonstrates it in real markup; a missing demo means a missing feature, caught by parity tests.
|
|
128
|
+
## Documentation contract
|
|
931
129
|
|
|
932
|
-
|
|
130
|
+
- Specs precede code. Read the matching guide, form the intended design, then compare the implementation.
|
|
131
|
+
- Public exports and behavioral methods must remain in guide parity; TypeScript, SCSS, Markdown, tests, and showcase must agree.
|
|
132
|
+
- Never suppress a parity failure. Correct the drift.
|
|
933
133
|
|
|
934
|
-
##
|
|
134
|
+
## Communication
|
|
935
135
|
|
|
936
|
-
-
|
|
937
|
-
-
|
|
938
|
-
-
|
|
939
|
-
- Verify before claiming done: run the gate, read the output, then report.
|
|
136
|
+
- Do the obvious work without asking for ceremonial permission.
|
|
137
|
+
- Keep chat summaries short; show exact changes as diffs when useful.
|
|
138
|
+
- Never claim a gate passed until you ran it and read the result.
|