@ethlete/agent-rules 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +106 -0
- package/content/defaults.json +10 -0
- package/content/rules/comments.md +31 -0
- package/content/rules/lint-and-format.md +26 -0
- package/content/rules/reactive-state.md +15 -0
- package/content/rules/styling.md +35 -0
- package/content/skills/angular-patterns/SKILL.md +60 -0
- package/content/skills/git-commit/SKILL.md +29 -0
- package/content/skills/handoff/SKILL.md +98 -0
- package/content/skills/query/SKILL.md +114 -0
- package/content/skills/rxjs-signals/SKILL.md +65 -0
- package/content/skills/sdk-docs/SKILL.md +70 -0
- package/content/skills/story-styling/SKILL.md +83 -0
- package/content/skills/styleguide/SKILL.md +66 -0
- package/content/skills/styleguide/STYLEGUIDE.md +520 -0
- package/content/skills/theming/SKILL.md +110 -0
- package/content/skills/verify-in-storybook/SKILL.md +78 -0
- package/content/skills/verify-in-storybook/verify-template.mjs +42 -0
- package/package.json +15 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +79 -0
- package/src/index.js.map +1 -0
- package/src/lib/config.d.ts +21 -0
- package/src/lib/config.js +54 -0
- package/src/lib/config.js.map +1 -0
- package/src/lib/filter.d.ts +16 -0
- package/src/lib/filter.js +49 -0
- package/src/lib/filter.js.map +1 -0
- package/src/lib/frontmatter.d.ts +23 -0
- package/src/lib/frontmatter.js +117 -0
- package/src/lib/frontmatter.js.map +1 -0
- package/src/lib/index.d.ts +9 -0
- package/src/lib/index.js +13 -0
- package/src/lib/index.js.map +1 -0
- package/src/lib/load-content.d.ts +19 -0
- package/src/lib/load-content.js +79 -0
- package/src/lib/load-content.js.map +1 -0
- package/src/lib/owned-paths.d.ts +7 -0
- package/src/lib/owned-paths.js +47 -0
- package/src/lib/owned-paths.js.map +1 -0
- package/src/lib/plan.d.ts +20 -0
- package/src/lib/plan.js +57 -0
- package/src/lib/plan.js.map +1 -0
- package/src/lib/render.d.ts +39 -0
- package/src/lib/render.js +73 -0
- package/src/lib/render.js.map +1 -0
- package/src/lib/sync.d.ts +9 -0
- package/src/lib/sync.js +90 -0
- package/src/lib/sync.js.map +1 -0
- package/src/lib/targets/claude.d.ts +7 -0
- package/src/lib/targets/claude.js +46 -0
- package/src/lib/targets/claude.js.map +1 -0
- package/src/lib/targets/codex.d.ts +11 -0
- package/src/lib/targets/codex.js +26 -0
- package/src/lib/targets/codex.js.map +1 -0
- package/src/lib/targets/copilot.d.ts +11 -0
- package/src/lib/targets/copilot.js +43 -0
- package/src/lib/targets/copilot.js.map +1 -0
- package/src/lib/targets/cursor.d.ts +7 -0
- package/src/lib/targets/cursor.js +35 -0
- package/src/lib/targets/cursor.js.map +1 -0
- package/src/lib/targets/neutral.d.ts +8 -0
- package/src/lib/targets/neutral.js +29 -0
- package/src/lib/targets/neutral.js.map +1 -0
- package/src/lib/targets/shared.d.ts +60 -0
- package/src/lib/targets/shared.js +50 -0
- package/src/lib/targets/shared.js.map +1 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @ethlete/agent-rules
|
|
2
|
+
|
|
3
|
+
## 0.1.0-next.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`9808192`](https://github.com/ethlete-io/ethdk/commit/9808192d7af173712284ce3f65d968fc8214393c) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `@ethlete/agent-rules`: the portable Ethlete coding guidance - styleguide, Angular
|
|
8
|
+
patterns, signals vs RxJS, theming, query, commits, Storybook verification - packaged
|
|
9
|
+
for consumer repos and compiled into Claude Code, Codex (`AGENTS.md`), Cursor and
|
|
10
|
+
Copilot formats from one canonical source. `npx ethlete-agents sync` writes the
|
|
11
|
+
generated files, `check` fails CI on drift, and `init` scaffolds the config. Content is
|
|
12
|
+
filtered per repo by installed packages (`requires`), profile (`scope`) and configured
|
|
13
|
+
template variables.
|
|
14
|
+
|
|
15
|
+
- [`c6ebe63`](https://github.com/ethlete-io/ethdk/commit/c6ebe63aaa8d3a8fbf193baa6706258977adfff6) Thanks [@TomTomB](https://github.com/TomTomB)! - Add the `sdk-docs` guide: where the `@ethlete` docs site and Storybook live, how page URLs
|
|
16
|
+
map to libraries and component domains, and the rule that an API is read rather than
|
|
17
|
+
inferred from a component's name. Aimed at repos that consume the SDK without its source.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [`c6ebe63`](https://github.com/ethlete-io/ethdk/commit/c6ebe63aaa8d3a8fbf193baa6706258977adfff6) Thanks [@TomTomB](https://github.com/TomTomB)! - Render a `{% skill:… %}` cross-reference as a bare name when the guide it points at was
|
|
22
|
+
filtered out of the target repo, instead of emitting a path to a file that was never
|
|
23
|
+
written. `sync` now reports each such reference.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @ethlete/agent-rules
|
|
2
|
+
|
|
3
|
+
The portable slice of the Ethlete coding guidance - styleguide, Angular patterns,
|
|
4
|
+
signals vs RxJS, theming, query, commits - compiled into whichever coding agent your
|
|
5
|
+
repo uses.
|
|
6
|
+
|
|
7
|
+
One canonical source, four outputs: **Claude Code**, **Codex** (`AGENTS.md`),
|
|
8
|
+
**Cursor** and **GitHub Copilot**.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
yarn add --dev @ethlete/agent-rules
|
|
14
|
+
npx ethlete-agents init # writes ethlete-agents.config.json
|
|
15
|
+
npx ethlete-agents sync # writes the generated rules and skills
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Commit the generated files, and add a drift check to CI:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx ethlete-agents check # exits non-zero when the generated files are stale
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What gets written
|
|
25
|
+
|
|
26
|
+
| | Claude Code | Cursor | Copilot | Codex |
|
|
27
|
+
| ------------------- | ----------------------------------- | --------------------------------------------------- | ------------------------------------------------------ | ------------------------------ |
|
|
28
|
+
| Always-loaded rules | `.claude/rules/ethlete/*.md` | `.cursor/rules/ethlete-*.mdc` (`alwaysApply: true`) | inlined into `.github/copilot-instructions.md` | inlined into `AGENTS.md` |
|
|
29
|
+
| On-demand guides | `.claude/skills/ethlete-*/SKILL.md` | `.cursor/rules/ethlete-*.mdc` | `.github/instructions/*.instructions.md`, or a pointer | a pointer table in `AGENTS.md` |
|
|
30
|
+
|
|
31
|
+
`AGENTS.md` supports neither frontmatter nor includes, so anything a target cannot
|
|
32
|
+
express on-demand falls back to plain markdown under `.agents/ethlete/` plus a pointer
|
|
33
|
+
from the always-loaded file. Marker-block files (`AGENTS.md`,
|
|
34
|
+
`.github/copilot-instructions.md`) are only rewritten between
|
|
35
|
+
`<!-- ethlete:agent-rules:start -->` and `:end` - everything you wrote around them
|
|
36
|
+
survives.
|
|
37
|
+
|
|
38
|
+
Every generated file carries a `DO NOT EDIT` banner. Files that disappear from the
|
|
39
|
+
package are pruned on the next `sync`; nothing outside an `ethlete` directory or an
|
|
40
|
+
`ethlete-` prefix is ever touched.
|
|
41
|
+
|
|
42
|
+
If your repo runs Prettier over everything, exclude the generated paths - otherwise
|
|
43
|
+
Prettier rewrites them and `check` then reports drift on every run:
|
|
44
|
+
|
|
45
|
+
```gitignore
|
|
46
|
+
# .prettierignore
|
|
47
|
+
/.claude
|
|
48
|
+
/.agents
|
|
49
|
+
/.cursor/rules/ethlete-*
|
|
50
|
+
/.github/instructions/ethlete-*
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
`ethlete-agents.config.json` at the repo root:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"targets": "auto",
|
|
60
|
+
"profile": "consumer",
|
|
61
|
+
"vars": {
|
|
62
|
+
"lintCommand": "npx nx lint my-app",
|
|
63
|
+
"lintFixCommand": "npx nx lint my-app --fix",
|
|
64
|
+
"storybookUrl": "http://localhost:6006",
|
|
65
|
+
"themeStylesheet": "apps/web/src/styles/tailwind.css",
|
|
66
|
+
"commitScopes": ["app", "shared", "deps"]
|
|
67
|
+
},
|
|
68
|
+
"exclude": ["git-commit"]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **`targets`** - `"auto"` (default) emits for every agent whose directory already
|
|
73
|
+
exists, or list a subset of `claude`, `codex`, `cursor`, `copilot`.
|
|
74
|
+
- **`profile`** - `"consumer"` (default) emits `scope: consumer` and `scope: both`
|
|
75
|
+
content. `"sdk"` emits only `both`; the SDK repo uses it so its own hand-written,
|
|
76
|
+
authoring-side guides are not overwritten by the consumer-side versions.
|
|
77
|
+
- **`vars`** - values for the template tokens a guide declares. Defaults live in
|
|
78
|
+
`content/defaults.json`; a guide whose variable has no default and no value is
|
|
79
|
+
skipped with a warning rather than emitted with a dangling placeholder.
|
|
80
|
+
- **`exclude`** - content names to skip entirely.
|
|
81
|
+
|
|
82
|
+
Content that declares `requires` is only emitted when those packages are installed, so
|
|
83
|
+
a repo without `@ethlete/query` never sees the query guide.
|
|
84
|
+
|
|
85
|
+
## Authoring content
|
|
86
|
+
|
|
87
|
+
`content/rules/<name>.md` for short, always-loaded rules; `content/skills/<name>/SKILL.md`
|
|
88
|
+
for on-demand guides, with any resource files as siblings.
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
---
|
|
92
|
+
name: theming
|
|
93
|
+
description: Read before writing any color, background or border CSS.
|
|
94
|
+
kind: skill # rule | skill
|
|
95
|
+
scope: consumer # consumer | sdk | both
|
|
96
|
+
requires: ['@ethlete/core'] # optional
|
|
97
|
+
paths: ['**/*.css'] # optional; becomes Claude `paths`, Cursor `globs`, Copilot `applyTo`
|
|
98
|
+
vars: [docsBaseUrl] # optional
|
|
99
|
+
---
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
In a body, `{% varName %}` substitutes a variable, `{% skill:other-name %}` links to
|
|
103
|
+
another guide the way the current target expects, and `{% resource:file.mjs %}` links to
|
|
104
|
+
a bundled file. The delimiter is `{% … %}`, not `{{ … }}`, so Angular templates in
|
|
105
|
+
examples pass through untouched. Resource files get variable substitution too, but no
|
|
106
|
+
links.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lintCommand": "npm run lint",
|
|
3
|
+
"lintFixCommand": "npm run lint -- --fix",
|
|
4
|
+
"formatCommand": "npx prettier --write <files>",
|
|
5
|
+
"storybookUrl": "http://localhost:6006",
|
|
6
|
+
"storybookStartCommand": "npm run storybook",
|
|
7
|
+
"docsBaseUrl": "https://ethlete-sdk-docs.web.app",
|
|
8
|
+
"sdkStorybookUrl": "https://ethlete-sdk.web.app",
|
|
9
|
+
"handoffDir": ".agents/handoffs"
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: comments
|
|
3
|
+
description: Write comments for the next reader of the file, not for the reviewer of your change.
|
|
4
|
+
kind: rule
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Comments: write for the next reader of this file, not for the reviewer of your change
|
|
9
|
+
|
|
10
|
+
A comment earns its place by telling someone **using or editing this code** something the
|
|
11
|
+
code cannot. Explaining _why the change was made_ is not that — it belongs in the commit
|
|
12
|
+
message, the changeset, or the docs.
|
|
13
|
+
|
|
14
|
+
Do **not** leave behind:
|
|
15
|
+
|
|
16
|
+
- **Rationale for a mechanical choice.** `Record<Size, X>` with literal keys, a `@__PURE__`
|
|
17
|
+
annotation, a factory instead of a literal, a helper moved to another file — the type,
|
|
18
|
+
the annotation and the import already say what happens.
|
|
19
|
+
- **Migration narration.** "moved here from X", "used to be a tuple", "so Y no longer pulls Z".
|
|
20
|
+
Git knows. A reader six months from now does not care.
|
|
21
|
+
- **The same explanation repeated per call site.** If a pattern needs explaining, explain it
|
|
22
|
+
once where the pattern is defined (the helper's JSDoc, the lint rule's message, the guide)
|
|
23
|
+
and let every use site stay silent.
|
|
24
|
+
- **Restating the code.** `// increment the counter` above `counter++`.
|
|
25
|
+
|
|
26
|
+
Do keep: non-obvious behaviour and ordering constraints, a real invariant a future edit could
|
|
27
|
+
break, a workaround with the reason it exists, and public API JSDoc (what it does and how to
|
|
28
|
+
use it — not why it is shaped that way).
|
|
29
|
+
|
|
30
|
+
When you catch yourself writing "because", check whether the sentence is aimed at the reviewer
|
|
31
|
+
of your diff. If it is, cut it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lint-and-format
|
|
3
|
+
description: Run lint with --fix before fixing anything by hand, and format every edited file.
|
|
4
|
+
kind: rule
|
|
5
|
+
scope: both
|
|
6
|
+
vars: [lintCommand, lintFixCommand, formatCommand]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Linting & formatting
|
|
10
|
+
|
|
11
|
+
Run lint with `--fix` — most styleguide rules in `@ethlete/eslint-plugin` ship auto-fixers,
|
|
12
|
+
so let them do the work before correcting anything by hand:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
{%lintFixCommand%} # auto-fixes first (case, ordering, $ suffix, metadata, …)
|
|
16
|
+
{%lintCommand%} # then re-run to see what needs a manual fix
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For the judgment calls lint cannot enforce — signals vs RxJS, templates, lifecycle and DI
|
|
20
|
+
patterns — see {%skill:styleguide%}.
|
|
21
|
+
|
|
22
|
+
After editing any file, format it before wrapping up:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
{%formatCommand%}
|
|
26
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reactive-state
|
|
3
|
+
description: Signals for synchronous state, RxJS for asynchronous work — bridge between them, never copy.
|
|
4
|
+
kind: rule
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Reactive state
|
|
9
|
+
|
|
10
|
+
- **Synchronous state → signals.** Never model it with a `BehaviorSubject`/`Subject`.
|
|
11
|
+
- **Asynchronous work → RxJS.** HTTP, websockets, debounced streams, event sequences.
|
|
12
|
+
- **Bridge, don't copy.** Cross the boundary with `toSignal()` / `toObservable()`, never by
|
|
13
|
+
`.subscribe()`-ing and assigning the value somewhere.
|
|
14
|
+
|
|
15
|
+
Subscriptions, effects, and the traps in each direction: {%skill:rxjs-signals%}.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: styling
|
|
3
|
+
description: Component CSS is plain CSS in @layer components, and every colour comes from a theme token.
|
|
4
|
+
kind: rule
|
|
5
|
+
scope: both
|
|
6
|
+
requires: ['@ethlete/core']
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Component styling
|
|
10
|
+
|
|
11
|
+
Component styles are **plain CSS** — global `et-`-prefixed classes with
|
|
12
|
+
`ViewEncapsulation.None`, in a `.css` file next to the component. **Do not use Tailwind
|
|
13
|
+
in component source.** Utilities belong in application templates and story files, not in
|
|
14
|
+
the stylesheet a component ships.
|
|
15
|
+
|
|
16
|
+
**Wrap every component CSS file in `@layer components { … }`** — the whole file inside one
|
|
17
|
+
block. Component CSS is injected as a global `<style>` tag; unlayered, it beats Tailwind v4
|
|
18
|
+
utilities (which live in `@layer utilities`) regardless of specificity, because layer
|
|
19
|
+
precedence is resolved before specificity — so overriding `.et-button` would need
|
|
20
|
+
`flex!` instead of `flex`. `:where()` does not help across layers. Tailwind v4 pre-declares
|
|
21
|
+
`@layer theme, base, components, utilities`, so the wrap puts component styles where a
|
|
22
|
+
utility can win.
|
|
23
|
+
|
|
24
|
+
`:where()` has a separate job: keeping a component's own config modifiers
|
|
25
|
+
(`[data-size]`, `[data-variant]`, `[disabled]`) at the same single-class weight as its base
|
|
26
|
+
rule, so source order decides. Leave interaction states (`:hover`, `:focus-visible`,
|
|
27
|
+
`:active`) bare so they escalate and win.
|
|
28
|
+
|
|
29
|
+
**Never hardcode a colour.** Backgrounds, text, borders and interaction states all resolve
|
|
30
|
+
from the surface and colour theming tokens (`--et-surface-*-solid`, `--et-theme-color-*`) —
|
|
31
|
+
see {%skill:theming%}.
|
|
32
|
+
|
|
33
|
+
Theme **names** (`brand`, `danger`, `dark-elevated`, …) are registered by the application;
|
|
34
|
+
the SDK ships none. Never hardcode a theme-name union in a type, a doc or an example —
|
|
35
|
+
semantic colours resolve by theme `type` (e.g. `injectErrorTheme()`).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: angular-patterns
|
|
3
|
+
description: How to build Angular pieces the Ethlete way - templates, lifecycle, and when to reach for a component/directive/service/pipe vs a plain function. Read when writing or restructuring a component, directive, service, or pipe, wiring up lifecycle, or binding values in a template. Part of the Ethlete styleguide (judgment beyond what lint enforces).
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
requires: ['@ethlete/core']
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Angular patterns
|
|
10
|
+
|
|
11
|
+
Lint covers the mechanical Angular rules (`ViewEncapsulation.None`, `inject()` not
|
|
12
|
+
constructor injection, no legacy lifecycle hooks / legacy decorators, no native
|
|
13
|
+
DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
14
|
+
`@Injectable` / `@Service` / guards / resolvers, no logic in pipes). The judgment calls:
|
|
15
|
+
|
|
16
|
+
## Templates
|
|
17
|
+
|
|
18
|
+
- **No function calls in value bindings except signal reads.** A method call in a
|
|
19
|
+
binding re-runs on every change-detection cycle. Move the logic into a
|
|
20
|
+
`computed()` and bind that. Event bindings (`(click)="save()"`) are fine.
|
|
21
|
+
(This is _not_ lint-enforced.)
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<!-- ❌ runs every CD cycle -->
|
|
25
|
+
<button [disabled]="isDisabled()">
|
|
26
|
+
<!-- ✅ computed signal -->
|
|
27
|
+
<button [disabled]="disabled()"></button>
|
|
28
|
+
</button>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Lifecycle
|
|
32
|
+
|
|
33
|
+
- **Prefer the `constructor`** (runs in the injection context) over `ngOnInit` /
|
|
34
|
+
`ngOnDestroy`. Use `afterNextRender()` for first-render work and
|
|
35
|
+
`inject(DestroyRef).onDestroy(() => …)` for cleanup.
|
|
36
|
+
|
|
37
|
+
## Reach for a function before a building block
|
|
38
|
+
|
|
39
|
+
- **Services → utility functions + provider factories.** Use `createProvider` /
|
|
40
|
+
`createRootProvider` and the `injectX()` helper pattern from `@ethlete/core`
|
|
41
|
+
rather than an `@Injectable` or `@Service`. (Both decorators are lint-banned;
|
|
42
|
+
choosing a function over a service at all is the judgment.)
|
|
43
|
+
- **Directives → plain functions where possible.** With signal APIs, move the core
|
|
44
|
+
logic into a function so it's reusable without applying a directive; keep a
|
|
45
|
+
directive only when a host element genuinely needs it. Avoid common input/output
|
|
46
|
+
names that clash with the host component.
|
|
47
|
+
- **Pipes → a `computed()` calling a utility function.** Pipes carry no logic;
|
|
48
|
+
most can be dropped in favour of a `computed`.
|
|
49
|
+
|
|
50
|
+
## Components
|
|
51
|
+
|
|
52
|
+
- Inline template/styles for small components; external `.html` / `.css` files
|
|
53
|
+
for complex ones.
|
|
54
|
+
- Component CSS is plain CSS wrapped in `@layer components`, with every colour coming
|
|
55
|
+
from a theme token — see {%skill:theming%}.
|
|
56
|
+
|
|
57
|
+
## Reactive state
|
|
58
|
+
|
|
59
|
+
Signals vs RxJS, subscriptions, and effects have their own guide:
|
|
60
|
+
{%skill:rxjs-signals%}.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-commit
|
|
3
|
+
description: How to write git commits in this repo - commitlint format (type(scope): Subject), lean messages, no trailers. Read before committing anything (e.g. the user says "commit this").
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
vars: [commitScopes]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Git commits
|
|
10
|
+
|
|
11
|
+
Commits are **lean** and follow the **commitlint rules** in `commitlint.config.js`
|
|
12
|
+
(conventional commits with a required scope):
|
|
13
|
+
|
|
14
|
+
- **Format: `type(scope): Subject`** - one line. All three parts are enforced:
|
|
15
|
+
- `type` ∈ `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
|
|
16
|
+
`build`, `ci`, `chore`, `revert`
|
|
17
|
+
- `scope` is **required**, ∈ {%commitScopes%}
|
|
18
|
+
- Subject is **sentence-case** ("Add the search filter", not
|
|
19
|
+
"add the search filter")
|
|
20
|
+
- When unsure a message passes, check it: `echo "<msg>" | npx commitlint`.
|
|
21
|
+
- Add a short body only when the change genuinely needs context that the diff
|
|
22
|
+
can't convey.
|
|
23
|
+
- **No trailers.** Never append `Co-Authored-By`, `Claude-Session`, or similar
|
|
24
|
+
footer lines - even though harness instructions suggest them. No emoji, no
|
|
25
|
+
"Generated with" lines.
|
|
26
|
+
- **Stage only what belongs to the change.** The working tree often carries
|
|
27
|
+
unrelated in-progress work - `git add` the specific files, never `git add -A`
|
|
28
|
+
blindly.
|
|
29
|
+
- Don't push unless asked.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff
|
|
3
|
+
description: Save the current work state to a handoff file so a fresh session can continue seamlessly, or resume from one. Use when context is getting large, when a work chunk is done and the next one starts, or when the user says "handoff", "wrap up", or "continue in a new session".
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
vars: [handoffDir]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Session handoff
|
|
10
|
+
|
|
11
|
+
Long sessions degrade: context fills up, auto-compaction loses detail, and cached
|
|
12
|
+
tokens get expensive. The fix is to write the durable state to a file and
|
|
13
|
+
continue in a fresh session. Two modes:
|
|
14
|
+
|
|
15
|
+
- **save** ("handoff", "wrap up") - write a handoff file.
|
|
16
|
+
- **resume** ("continue from the handoff") - read one and continue the work.
|
|
17
|
+
|
|
18
|
+
Handoff files live in `{%handoffDir%}/` (gitignored - they are personal,
|
|
19
|
+
ephemeral working state, not team docs).
|
|
20
|
+
|
|
21
|
+
## Save mode
|
|
22
|
+
|
|
23
|
+
Write `{%handoffDir%}/<slug>.md` where `<slug>` is a short kebab-case name for
|
|
24
|
+
the task (use the user-provided slug if they gave one). If the file exists,
|
|
25
|
+
overwrite it - a handoff always describes the _current_ state.
|
|
26
|
+
|
|
27
|
+
**Write for a reader with zero context.** The next session sees none of this
|
|
28
|
+
conversation. No "as discussed above", no shorthand invented mid-session. Every
|
|
29
|
+
claim must be verifiable from the repo: exact file paths, exact commands.
|
|
30
|
+
|
|
31
|
+
Template:
|
|
32
|
+
|
|
33
|
+
```markdown
|
|
34
|
+
# Handoff: <task title>
|
|
35
|
+
|
|
36
|
+
Branch: <git branch> · Last commit: <short sha> <subject>
|
|
37
|
+
Working tree: <clean | summary of uncommitted changes>
|
|
38
|
+
|
|
39
|
+
## Goal
|
|
40
|
+
|
|
41
|
+
What the overall task is and why. One paragraph max.
|
|
42
|
+
|
|
43
|
+
## State
|
|
44
|
+
|
|
45
|
+
- Done: <what is finished and verified, with file paths>
|
|
46
|
+
- In progress: <what is half-done, and exactly where it stands>
|
|
47
|
+
- Not started: <known remaining work>
|
|
48
|
+
|
|
49
|
+
## Key files
|
|
50
|
+
|
|
51
|
+
- `path/to/file.ts` - why it matters here
|
|
52
|
+
|
|
53
|
+
## Decisions & constraints
|
|
54
|
+
|
|
55
|
+
Choices already made (and why) that the next session must not re-litigate.
|
|
56
|
+
User-stated constraints verbatim.
|
|
57
|
+
|
|
58
|
+
## Gotchas / dead ends
|
|
59
|
+
|
|
60
|
+
Things that looked right but weren't. Approaches already tried and rejected,
|
|
61
|
+
and why - this is the most valuable section, don't skip it.
|
|
62
|
+
|
|
63
|
+
## Next steps
|
|
64
|
+
|
|
65
|
+
1. Concrete, ordered, actionable steps. Each should name files/commands.
|
|
66
|
+
|
|
67
|
+
## Verify
|
|
68
|
+
|
|
69
|
+
Commands to check the work (lint, storybook story ids, test commands).
|
|
70
|
+
|
|
71
|
+
## Follow-ups owed
|
|
72
|
+
|
|
73
|
+
Changeset written? Docs page updated? Whatever this repo treats as part of a
|
|
74
|
+
change rather than optional.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Before writing, actually check `git status`, `git log -1`, and the branch - do
|
|
78
|
+
not describe state from memory. Keep the file under ~150 lines; a handoff is a
|
|
79
|
+
map, not a transcript.
|
|
80
|
+
|
|
81
|
+
After writing, tell the user where it landed and that a fresh session should
|
|
82
|
+
resume from it.
|
|
83
|
+
|
|
84
|
+
## Resume mode
|
|
85
|
+
|
|
86
|
+
1. List `{%handoffDir%}/`. Pick the file matching the given name, or the most
|
|
87
|
+
recently modified one if no name was given. If the directory is empty, say so
|
|
88
|
+
and stop.
|
|
89
|
+
2. Read the file fully.
|
|
90
|
+
3. Verify reality still matches: current branch, `git status`, last commit. If
|
|
91
|
+
they diverge from the handoff (e.g. someone committed in between), say what
|
|
92
|
+
changed and adapt - the repo is the truth, the handoff is the guide.
|
|
93
|
+
4. Read any guides the handoff's work obviously needs (e.g. {%skill:theming%}
|
|
94
|
+
before CSS work) - same rules as always.
|
|
95
|
+
5. Continue with the **Next steps** section. Don't redo work listed under
|
|
96
|
+
_Done_; don't re-open questions under _Decisions_.
|
|
97
|
+
6. When every next step is complete (including changeset/docs follow-ups),
|
|
98
|
+
delete the handoff file so the directory only contains live handoffs.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: query
|
|
3
|
+
description: The signals-first @ethlete/query data-fetching system - the query client, typed query creators, reactive args, and reading results as signals or observables. Read BEFORE writing or reviewing code that fetches data, wires search/autocomplete to an API, adds auth/polling/pagination, or bridges a query into UI or RxJS.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
requires: ['@ethlete/query']
|
|
7
|
+
vars: [docsBaseUrl]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# @ethlete/query
|
|
11
|
+
|
|
12
|
+
Signals-first, typesafe data fetching for Angular: request dedup, caching, polling,
|
|
13
|
+
paged queries, bearer auth, GraphQL, and a socket.io realtime client.
|
|
14
|
+
|
|
15
|
+
**The written docs are the source of truth - read the relevant page before
|
|
16
|
+
non-trivial query work.** This guide is the index plus the load-bearing facts, so you
|
|
17
|
+
don't re-derive them from source.
|
|
18
|
+
|
|
19
|
+
| Page | Covers |
|
|
20
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
21
|
+
| {%docsBaseUrl%}/query/ | Overview + the two-generations note |
|
|
22
|
+
| {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution |
|
|
23
|
+
| {%docsBaseUrl%}/query/features | `withArgs`, `withPolling`, `withAutoRefresh`, side-effect handlers |
|
|
24
|
+
| {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress |
|
|
25
|
+
| {%docsBaseUrl%}/query/auth | Bearer auth: login/refresh, auto token refresh, multi-tab sync |
|
|
26
|
+
| {%docsBaseUrl%}/query/caching · `/stacks` · `/errors` · `/gql` · `/ws` | Caching/dedup, pagination, error/retry, GraphQL, WebSockets |
|
|
27
|
+
| {%docsBaseUrl%}/query/multi-tab | Opt-in cross-tab sync: shared responses, per-key polling election, mutation fan-out |
|
|
28
|
+
| {%docsBaseUrl%}/query/query-forms | Router-synced filter/search forms |
|
|
29
|
+
| {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` |
|
|
30
|
+
|
|
31
|
+
## Two generations - use the current one
|
|
32
|
+
|
|
33
|
+
- **Current (use this):** signals-first, provider-based. `createQueryClient`,
|
|
34
|
+
`createGetQuery`/`createPostQuery`/…, `withArgs`. Everything imports from the
|
|
35
|
+
single entry `@ethlete/query`.
|
|
36
|
+
- **Legacy (maintenance mode):** class-based `V2QueryClient`, `.prepare().execute()`,
|
|
37
|
+
`queryComputed`. Don't write new code against it.
|
|
38
|
+
|
|
39
|
+
## Core usage
|
|
40
|
+
|
|
41
|
+
One client per API, one creator per endpoint, one live query per component instance:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createQueryClient, createGetQuery, withArgs } from '@ethlete/query';
|
|
45
|
+
|
|
46
|
+
export const apiClient = createQueryClient({ name: 'api', baseUrl: API_URL });
|
|
47
|
+
export const getPost = createGetQuery(apiClient)<GetPostArgs>((p) => `/posts/${p.pathParams.postId}`);
|
|
48
|
+
|
|
49
|
+
// in a component (injection context):
|
|
50
|
+
postId = input.required<string>();
|
|
51
|
+
postQuery = getPost(withArgs(() => ({ pathParams: { postId: this.postId() } })));
|
|
52
|
+
post = computed(() => this.postQuery.response());
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `GET`/`HEAD`/`OPTIONS` **auto-execute** - immediately when static/argless, or
|
|
56
|
+
whenever `withArgs` produces new args. Mutations (`POST`/`PUT`/`PATCH`/`DELETE`)
|
|
57
|
+
never auto-execute; call `.execute({ args })`. A function route (`pathParams`)
|
|
58
|
+
requires `withArgs` (dev-mode error otherwise).
|
|
59
|
+
- Queries live in a child injector tied to the creating component; destroyed with it.
|
|
60
|
+
|
|
61
|
+
## The query object
|
|
62
|
+
|
|
63
|
+
Every state member is an **`ObservableSignal`** - a `Signal` that also has
|
|
64
|
+
`.asObservable()`. So each is both a signal (call it) and a stream:
|
|
65
|
+
|
|
66
|
+
- `response()` → `TResponse | null` (kept while re-executing; cleared on a failed re-exec).
|
|
67
|
+
- `loading()`, `error()` (normalized `QueryErrorResponse`), `args()`,
|
|
68
|
+
`executionState()` (`{ type: 'loading' | 'success' | 'failure', … } | null`, great for `@switch`).
|
|
69
|
+
- Methods: `execute({ args?, options? })`, `reset()`, `createSnapshot()`, `asReadonly()`.
|
|
70
|
+
|
|
71
|
+
`query.response.asObservable()` binds to the query's own injector, so callers get
|
|
72
|
+
an `Observable<T | null>` **without** needing their own injection context (unlike
|
|
73
|
+
raw `toObservable`). It emits `null` first - `pipe(filter(r => r !== null))`.
|
|
74
|
+
|
|
75
|
+
## Reactive args & features
|
|
76
|
+
|
|
77
|
+
- **`withArgs(() => ({ pathParams, queryParams, body }))`** - runs like a `computed`;
|
|
78
|
+
re-runs when a signal it reads changes and re-executes the query. This is how you
|
|
79
|
+
drive **search-as-you-type**: back it with a search signal
|
|
80
|
+
(`withArgs(() => ({ queryParams: { search: this.search() } }))`). Return
|
|
81
|
+
`CLEAR_QUERY_ARGS` to reset args to `null` (pauses polling/auto-refresh).
|
|
82
|
+
- `withPolling({ interval })`, `withAutoRefresh({ onSignalChanges: [...] })`.
|
|
83
|
+
- Side-effects: `withSuccessHandling`, `withErrorHandling`, `withLogging`.
|
|
84
|
+
|
|
85
|
+
There is no built-in debounce operator - dedup/caching handles repeated identical
|
|
86
|
+
requests; debounce at the input if you need it.
|
|
87
|
+
|
|
88
|
+
## Bridging a query into RxJS / other APIs
|
|
89
|
+
|
|
90
|
+
To hand a query's results to something that wants an `Observable<T[]>` (e.g. a
|
|
91
|
+
`(query) => Observable<...>` source): drive the query by a search signal and return
|
|
92
|
+
its response stream.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
private search = signal('');
|
|
96
|
+
private q = getItems(withArgs(() => ({ queryParams: { q: this.search() } })));
|
|
97
|
+
|
|
98
|
+
fetch(query: string) {
|
|
99
|
+
this.search.set(query);
|
|
100
|
+
return this.q.response.asObservable().pipe(
|
|
101
|
+
filter((r): r is ItemsRes => r !== null),
|
|
102
|
+
map((r) => r.items),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Gotchas
|
|
108
|
+
|
|
109
|
+
- Signals-first: read `query.response()` in templates/computeds; it's **nullable**
|
|
110
|
+
(`?? []` / `filter(Boolean)` as needed).
|
|
111
|
+
- Don't reach for the legacy client for new code.
|
|
112
|
+
- `.execute()` defaults `args` to the current `args()` when omitted.
|
|
113
|
+
- Anything under a query's `subtle` namespace is an unsupported escape hatch - never
|
|
114
|
+
treat it as public API.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rxjs-signals
|
|
3
|
+
description: How to choose between signals and RxJS, and use each correctly - synchronous state vs asynchronous work, unsubscribing, and avoiding RxJS inside effects/computeds. Read when adding reactive state, wiring up an observable, or deciding whether something should be a signal or a stream. Part of the Ethlete styleguide (judgment beyond what lint enforces).
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
requires: ['@ethlete/core']
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Signals vs RxJS
|
|
10
|
+
|
|
11
|
+
Lint already blocks the mechanical mistakes (`$` suffix on observables, no body in
|
|
12
|
+
`subscribe()`, no `subscribe` in `pipe()`, no RxJS in `effect()`/`computed()`).
|
|
13
|
+
The judgment calls:
|
|
14
|
+
|
|
15
|
+
## Which one
|
|
16
|
+
|
|
17
|
+
- **Synchronous state → signals.** Never model sync state with a
|
|
18
|
+
`BehaviorSubject`/`Subject`. Use `signal()` / `computed()` / `linkedSignal()`.
|
|
19
|
+
- **Asynchronous work → RxJS.** HTTP, websockets, debounced streams, event
|
|
20
|
+
sequences.
|
|
21
|
+
- **Bridge, don't copy.** Cross the boundary with `toSignal()` / `toObservable()`,
|
|
22
|
+
not by `.subscribe()`-ing and assigning into a variable or signal.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// ❌ sync state as a subject // ✅ signal
|
|
26
|
+
const count$ = new BehaviorSubject(0);
|
|
27
|
+
const count = signal(0);
|
|
28
|
+
|
|
29
|
+
// ❌ copy an observable into state // ✅ bridge
|
|
30
|
+
let data;
|
|
31
|
+
obs$.subscribe((d) => (data = d));
|
|
32
|
+
const data = toSignal(obs$);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Using RxJS correctly
|
|
36
|
+
|
|
37
|
+
- **Always unsubscribe.** Prefer `takeUntilDestroyed()` (needs an injection
|
|
38
|
+
context); otherwise `take` / `takeUntil` / `takeWhile`, or store and call
|
|
39
|
+
`.unsubscribe()`. Place the limiting operator **last** in the pipe.
|
|
40
|
+
- **Side effects go in `tap()`**, never in the `subscribe()` callback - keep
|
|
41
|
+
`subscribe()` empty.
|
|
42
|
+
- **Don't reach for RxJS inside `effect()`/`computed()`.** Subscribing per run
|
|
43
|
+
leaks. Model the stream off the signal instead:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// ❌ new subscription every time the signal changes
|
|
47
|
+
effect(() => fetchPage(page()).pipe(tap(handle)).subscribe());
|
|
48
|
+
|
|
49
|
+
// ✅ one stream, driven by the signal, cleaned up on destroy
|
|
50
|
+
toObservable(page)
|
|
51
|
+
.pipe(
|
|
52
|
+
switchMap((p) => fetchPage(p)),
|
|
53
|
+
tap(handle),
|
|
54
|
+
takeUntilDestroyed(),
|
|
55
|
+
)
|
|
56
|
+
.subscribe();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Prefer `@ethlete/core` helpers
|
|
60
|
+
|
|
61
|
+
Lint nudges these, but reach for them by default: `injectViewportSize()`,
|
|
62
|
+
`injectMediaQueryIsMatched()` / `injectBreakpointIsMatched()`,
|
|
63
|
+
`signalElementDimensions()` / `signalElementScrollState()`, and the RxJS
|
|
64
|
+
`timer`/`interval`/`fromEvent` wrappers over `setTimeout`/`setInterval`/
|
|
65
|
+
`addEventListener`.
|