@ethlete/agent-rules 0.1.0-next.15 → 0.1.0-next.17
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 +33 -0
- package/README.md +5 -4
- package/content/rules/app-styling.md +47 -0
- package/content/rules/comments.md +4 -1
- package/content/rules/nx-layout.md +47 -0
- package/content/rules/styling.md +1 -1
- package/content/skills/angular-patterns/SKILL.md +23 -0
- package/content/skills/app-testing/SKILL.md +141 -0
- package/content/skills/design-exploration/SKILL.md +38 -38
- package/content/skills/git-flow/SKILL.md +4 -4
- package/content/skills/query/SKILL.md +148 -81
- package/content/skills/rxjs-signals/SKILL.md +24 -3
- package/content/skills/sdk-docs/SKILL.md +35 -7
- package/content/skills/sdk-update/SKILL.md +12 -7
- package/content/skills/story-styling/SKILL.md +7 -6
- package/content/skills/styleguide/lint-rule-lookup.md +2 -1
- package/content/skills/theming/SKILL.md +3 -4
- package/content/skills/timetrack/SKILL.md +61 -26
- package/content/skills/verify-in-app/SKILL.md +107 -0
- package/migrations/app-styling-utilities.md +114 -0
- package/migrations/list-state-query-form.md +103 -0
- package/migrations/nx-layout.md +23 -0
- package/migrations/sdk-components-over-hand-built-ui.md +69 -0
- package/migrations/search-query-field.md +45 -0
- package/migrations.json +43 -0
- package/package.json +4 -1
- package/src/index.js +5 -4
- package/src/index.js.map +1 -1
- package/src/lib/config.d.ts +2 -0
- package/src/lib/config.js +16 -3
- package/src/lib/config.js.map +1 -1
- package/src/lib/filter.d.ts +2 -0
- package/src/lib/filter.js +4 -4
- package/src/lib/filter.js.map +1 -1
- package/src/lib/package-runner.d.ts +1 -0
- package/src/lib/package-runner.js +34 -0
- package/src/lib/package-runner.js.map +1 -0
- package/src/lib/plan.js +10 -0
- package/src/lib/plan.js.map +1 -1
- package/src/lib/sync.js +19 -12
- package/src/lib/sync.js.map +1 -1
- package/src/lib/timetrack-command.js +98 -1
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack.d.ts +67 -0
- package/src/lib/timetrack.js +16 -1
- package/src/lib/timetrack.js.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @ethlete/agent-rules
|
|
2
2
|
|
|
3
|
+
## 0.1.0-next.17
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `et update` now leaves tasks to fix code written under the earlier guidance: app styling, URL-bound list state, hand-debounced search, hand-built charts, avatars and progress bars, and the Nx layout.
|
|
8
|
+
- `timetrack resync --replace` re-reads a checkout's agent logs and replaces the samples the store already holds for those sessions, so a parser fix reaches stored days.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- The Angular and signals skills cover shared `injectX()` logic, per-row formatting, writing query params and effects that only write a signal; the comment rule says app code rarely has public API.
|
|
13
|
+
- Add the consumer skills `verify-in-app` (drive the served app headlessly with Playwright) and `app-testing` (specs for query, router and overlay code), and link the new query testing docs page from the query skill.
|
|
14
|
+
- The `app-styling` rule and its migration now keep `ViewEncapsulation.None` on app components, as the `require-view-encapsulation-none` lint rule requires, and scope the remaining CSS under the component's host class.
|
|
15
|
+
- Consumer repos get an `app-styling` rule (Tailwind in app templates, 10px rem root) instead of the SDK's component styling rule, and the `query` and `sdk-docs` skills now point to `defineQueryForm` and every component domain.
|
|
16
|
+
- Query skill: bridge a correlated result into RxJS with `executeUntilSettled$`, keeping the Promise form for signal-forms `submit()`.
|
|
17
|
+
- The `list-state-query-form` task binds pagination, page size and a table sort the way that works, and keeps `replaceUrl: true`. The query skill names `observe({ replaceUrl: true })`.
|
|
18
|
+
- The guidance migrations find the call sites they missed in a real app, and generated commands use the repo's package manager.
|
|
19
|
+
- The `app-styling-utilities`, `sdk-components-over-hand-built-ui` and `nx-layout` migration tasks name the real theme utilities, find more call sites, and stop before overriding a repo's own rules or the generated `AGENTS.md` block.
|
|
20
|
+
- Consumer Nx workspaces get an `nx-layout` rule: thin apps, feature code in `libs/domain`, and shared `queries`, `types`, `uikit`, `theme` and `env` libs with `scope:*` tags.
|
|
21
|
+
- The consumer query skill now maps every @ethlete/query capability to its docs page, so agents find APIs like `defineQueryForm` instead of hand-building them.
|
|
22
|
+
- `ethlete-agents` commands run from a subdirectory now use the nearest directory holding `ethlete-agents.config.json` as the repo root, so `check` no longer reports false drift there.
|
|
23
|
+
- The `sdk-docs` skill points consumer agents at the new App setup docs page first: root font size, theme generation and the providers an app needs.
|
|
24
|
+
- `sync` and `check` now name every skill or rule skipped for an unmet `requires` or missing var, and warn when a path var like `themeStylesheet` points at a missing file.
|
|
25
|
+
- `vars.themeStylesheet` may now name a folder of theme files. The `story-styling` skill searches it recursively.
|
|
26
|
+
- `recommendedTs` now uses `ethlete/consistent-type-definitions` instead of `@typescript-eslint/consistent-type-definitions`, so `--fix` no longer turns an interface inside `declare module` or `declare global` (such as the theme-name registry the `@ethlete/core` generators emit) into a type alias that merges into nothing.
|
|
27
|
+
- The `ET100` error now says mutations need `withArgs` too and calls `silenceMissingWithArgsFeatureError` an escape hatch; the query skill recommends `withArgs` for mutations.
|
|
28
|
+
|
|
29
|
+
## 0.1.0-next.16
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Name the `take-until-destroyed-last` lint rule in the styleguide lint lookup and the RxJS skill.
|
|
34
|
+
- A design call now lists its drawings as `variants` in `variant-<key>.ts` files, and `et design check` takes `--variant`; the old `options` key and `--option` flag are gone.
|
|
35
|
+
|
|
3
36
|
## 0.1.0-next.15
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -99,18 +99,19 @@ Prettier rewrites them and `check` then reports drift on every run:
|
|
|
99
99
|
`.agents/skills/` is the cross-tool baseline) and adds `claude`, `cursor` or `copilot`
|
|
100
100
|
when their directory exists; or list an explicit subset.
|
|
101
101
|
- **`profile`** - `"consumer"` (default) emits `scope: consumer` and `scope: both`
|
|
102
|
-
content. `"sdk"` emits
|
|
103
|
-
authoring-side guides are not overwritten by the consumer-side versions.
|
|
102
|
+
content. `"sdk"` emits `scope: sdk` and `scope: both`; the SDK repo uses it so its own
|
|
103
|
+
hand-written, authoring-side guides are not overwritten by the consumer-side versions.
|
|
104
104
|
- **`vars`** - values for the template tokens a guide declares. Defaults live in
|
|
105
105
|
`content/defaults.json`; a guide whose variable has no default and no value is
|
|
106
106
|
skipped with a warning rather than emitted with a dangling placeholder. Some are
|
|
107
107
|
derived from the repo instead of defaulted - the `gitFlow*` ones from the `gitFlow`
|
|
108
|
-
block,
|
|
108
|
+
block, `packageRunner` (`yarn`, `pnpm exec`, `bunx` or `npx`) from the root
|
|
109
|
+
`package.json`'s `packageManager` or the lockfile, and `commitRuleSource` / `commitValidation` from whether a commitlint config
|
|
109
110
|
exists (`commitlint.config.*`, `.commitlintrc*`, or a `commitlint` key in
|
|
110
111
|
`package.json`). Without one, the git-commit guide presents the format as the repo's
|
|
111
112
|
convention and never mentions a `commitlint` run - an agent that goes looking for a
|
|
112
113
|
promised validator and finds nothing reports the discrepancy instead of just
|
|
113
|
-
committing. Setting
|
|
114
|
+
committing. Setting any derived var in `vars` overrides the detection.
|
|
114
115
|
- **`exclude`** - rule or skill names to skip entirely for every configured agent and
|
|
115
116
|
developer. For example, `"exclude": ["git-flow", "handoff"]` prevents those skills
|
|
116
117
|
from being generated; the next `sync` also removes copies generated previously. Unknown
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-styling
|
|
3
|
+
description: App components are styled with Tailwind utilities in their templates; app CSS stays out of @layer components; the SDK needs a 10px rem root; hardcoded colours are never primary values.
|
|
4
|
+
kind: rule
|
|
5
|
+
scope: consumer
|
|
6
|
+
requires: ['@ethlete/core']
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Styling
|
|
10
|
+
|
|
11
|
+
Every component in this workspace (views, pages, shells, shared UI in `libs/`) is laid out
|
|
12
|
+
and styled with **Tailwind utility classes in its template**, including the generated
|
|
13
|
+
theme utilities (`bg-et-surface-bg`, `border-et-surface-border`, `text-et-<theme>`). Do not
|
|
14
|
+
invent a BEM class system, and do not write a `.css` file for layout, spacing or
|
|
15
|
+
typography a utility expresses. Every app component sets `encapsulation: ViewEncapsulation.None`,
|
|
16
|
+
as the `require-view-encapsulation-none` lint rule requires. Its CSS is then global, so give the
|
|
17
|
+
component a host class (`host: { class: 'app-player-card' }`) and scope every selector under it,
|
|
18
|
+
the way SDK components scope theirs under their `et-` classes. Never write a bare element or
|
|
19
|
+
unprefixed class selector in a component stylesheet.
|
|
20
|
+
|
|
21
|
+
Write CSS only for what utilities cannot express. Keep that CSS **unlayered** or in
|
|
22
|
+
`@layer utilities` — never in `@layer components`, and that includes the global stylesheet. SDK component styles are injected into
|
|
23
|
+
`@layer components` at runtime, after your stylesheet, so an app rule in the same layer
|
|
24
|
+
ties on layer and loses on source order.
|
|
25
|
+
|
|
26
|
+
To change an SDK component's look, set its `--et-*` tokens first (documented per
|
|
27
|
+
component), then use a utility class or unlayered CSS on the element — see "Overriding
|
|
28
|
+
component styles" in the docs site's components overview.
|
|
29
|
+
|
|
30
|
+
The SDK sizes everything in `rem` on a **10px root**: the app's global stylesheet must set
|
|
31
|
+
`html { font-size: 62.5%; }`. Without it every SDK control renders 1.6× too large. With it,
|
|
32
|
+
`1rem` is `10px`, so the app's own `rem` values stay easy to read.
|
|
33
|
+
|
|
34
|
+
The 10px root also shrinks every rem-based Tailwind scale to 62.5% of its nominal size (1.6×
|
|
35
|
+
smaller): `p-4` is 10px instead of 16px, and `max-w-3xl` is 480px instead of 768px. Set
|
|
36
|
+
`--spacing: 0.4rem` in the app's `@theme` so the spacing scale keeps its 4px step, as the SDK's
|
|
37
|
+
own Storybook does. Redefine any other rem scale the app uses (`--text-*`, `--container-*`,
|
|
38
|
+
`--radius-*`) the same way, or use px where a size matters. Breakpoints are not affected: a
|
|
39
|
+
media query's `rem` ignores the root font size.
|
|
40
|
+
|
|
41
|
+
**Never use a hardcoded colour as the primary value.** Backgrounds, text, borders and
|
|
42
|
+
interaction states resolve from the surface and colour theming tokens
|
|
43
|
+
(`--et-surface-*-solid`, `--et-theme-color-*`) or their generated utilities. A static
|
|
44
|
+
fallback inside `var(--token, <fallback>)` is permitted, but not required.
|
|
45
|
+
|
|
46
|
+
Theme **names** (`brand`, `danger`, `dark-elevated`, …) are registered by this app; the SDK
|
|
47
|
+
ships none. Semantic colours resolve by theme `type` (e.g. `injectErrorTheme()`).
|
|
@@ -20,6 +20,8 @@ smaller function, or a type; fix that instead of narrating it.
|
|
|
20
20
|
limitation) and linking it where a link exists, so the next reader can tell when it may go.
|
|
21
21
|
4. **Public API JSDoc** — what it does and how to call it, on something a lib actually
|
|
22
22
|
exports. One or two sentences. Not internals, not history, not why it is shaped that way.
|
|
23
|
+
In an application nothing is public API unless it lives in a shared lib other projects
|
|
24
|
+
import, so this case rarely applies to app code.
|
|
23
25
|
|
|
24
26
|
Nothing else qualifies. Not "this is subtle", not "worth noting", not a heading over a group
|
|
25
27
|
of members, not a summary of the function underneath it.
|
|
@@ -43,7 +45,8 @@ years.
|
|
|
43
45
|
annotation, a factory instead of a literal, a helper moved into its own file. The type, the
|
|
44
46
|
annotation and the import already say what happens.
|
|
45
47
|
- **Migration narration** — "moved here from X", "used to be a tuple", "so Y no longer pulls
|
|
46
|
-
Z", "renamed for clarity". Git
|
|
48
|
+
Z", "renamed for clarity", "see ADR-0012". Git and the ADR index know; the next reader does
|
|
49
|
+
not care.
|
|
47
50
|
- **The same explanation at every call site.** Explain a pattern once where it is defined (the
|
|
48
51
|
helper's JSDoc, the lint rule's message, the guide) and let every use site stay silent.
|
|
49
52
|
- **Commented-out code.**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nx-layout
|
|
3
|
+
description: Nx workspace layout - thin apps, feature code in libs grouped by kind, path-mirroring aliases, scope tags.
|
|
4
|
+
kind: rule
|
|
5
|
+
scope: consumer
|
|
6
|
+
requires: ['nx']
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Nx workspace layout
|
|
10
|
+
|
|
11
|
+
Apps are thin shells. All feature code lives in libs, grouped by kind, not by Nx type:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
apps/<app>/src/app/ app.component.ts, app.config.ts, app.routes.ts only
|
|
15
|
+
libs/
|
|
16
|
+
domain/<app>/<feature>/ views, components, services, <feature>.routes.ts
|
|
17
|
+
domain/<app>/shared/ code shared by one app's features (optional)
|
|
18
|
+
queries/ query clients and creators, one folder per backend
|
|
19
|
+
types/ API models and shared types
|
|
20
|
+
uikit/ app-agnostic presentational components
|
|
21
|
+
theme/ surface and colour themes
|
|
22
|
+
env/ environment.ts, environment.production.ts
|
|
23
|
+
assets/ fonts, icons, images
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `app.routes.ts` only lazy-loads domain libs:
|
|
27
|
+
`loadChildren: () => import('@acme/domain/admin/users').then((m) => m.USERS_ROUTES)`.
|
|
28
|
+
Each feature lib exports its own `<FEATURE>_ROUTES`.
|
|
29
|
+
- A small app may use one lib per app (`libs/domain/<app>`) with a folder per feature.
|
|
30
|
+
Never put features, queries, shared UI or theme in the app project.
|
|
31
|
+
- Do not add Nx `feature/ui/data-access/util` folders or `type:*` tags.
|
|
32
|
+
- Every lib has `src/index.ts` and explicit `build` (`@nx/angular:ng-packagr-lite`), `lint`
|
|
33
|
+
and `test` targets in `project.json`. Keep `plugins: []` in `nx.json`.
|
|
34
|
+
- Alias = path with slashes: `@acme/domain/admin/users`, `@acme/queries`, `@acme/types`,
|
|
35
|
+
`@acme/uikit`, `@acme/env`. Project name = path with dashes (`domain-admin-users`).
|
|
36
|
+
Component `prefix` = the repo abbreviation.
|
|
37
|
+
- Tag every project with one `scope:*`: `scope:<app>` for an app and its domain libs,
|
|
38
|
+
`scope:core` for env and types, plus `scope:queries`, `scope:uikit`, `scope:theme`.
|
|
39
|
+
`@nx/enforce-module-boundaries` with `enforceBuildableLibDependency: true` lets
|
|
40
|
+
`scope:<app>` depend on itself and the shared scopes, never on another app.
|
|
41
|
+
- A backend in the same workspace is an app in `apps/` with its libs in
|
|
42
|
+
`libs/domain/<name>-api`. Ban `@angular/*` in backend scopes and `@nestjs/*` in frontend
|
|
43
|
+
scopes with `bannedExternalImports`.
|
|
44
|
+
- Environment config lives only in `libs/env`, never hardcoded in a query client.
|
|
45
|
+
|
|
46
|
+
This is the layout for new workspaces and new code. Do not restructure an existing
|
|
47
|
+
workspace that deviates from it unless the user asks.
|
package/content/rules/styling.md
CHANGED
|
@@ -28,6 +28,16 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
|
28
28
|
<button [disabled]="disabled()"></button>
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
- **Per-row formatting belongs in the data, not in a method per row.** Map the rows once
|
|
32
|
+
in a `computed()` and bind the prepared fields. A pure, module-level formatting function
|
|
33
|
+
is acceptable too; a component method called for each row in `@for` is not.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// ❌ <td>{{ formatDate(row.createdAt) }}</td> - re-runs for every row, every CD cycle
|
|
37
|
+
// ✅
|
|
38
|
+
rows = computed(() => this.items().map((item) => ({ ...item, createdAtLabel: formatDate(item.createdAt) })));
|
|
39
|
+
```
|
|
40
|
+
|
|
31
41
|
## Lifecycle
|
|
32
42
|
|
|
33
43
|
- **Prefer the `constructor`** (runs in the injection context) over `ngOnInit` /
|
|
@@ -40,6 +50,10 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
|
40
50
|
`createRootProvider` and the `injectX()` helper pattern from `@ethlete/core`
|
|
41
51
|
rather than an `@Injectable` or `@Service`. (Both decorators are lint-banned;
|
|
42
52
|
choosing a function over a service at all is the judgment.)
|
|
53
|
+
- **Repeated component logic → one `injectX()` function.** When two components carry the
|
|
54
|
+
same signals, effects or subscriptions, extract them into an `injectX()` that runs in the
|
|
55
|
+
injection context and returns what the components bind. Do not copy the block, and do not
|
|
56
|
+
reach for a base class.
|
|
43
57
|
- **Directives → plain functions where possible.** With signal APIs, move the core
|
|
44
58
|
logic into a function so it's reusable without applying a directive; keep a
|
|
45
59
|
directive only when a host element genuinely needs it. Avoid common input/output
|
|
@@ -47,6 +61,15 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
|
|
|
47
61
|
- **Pipes → a `computed()` calling a utility function.** Pipes carry no logic;
|
|
48
62
|
most can be dropped in favour of a `computed`.
|
|
49
63
|
|
|
64
|
+
## Query params
|
|
65
|
+
|
|
66
|
+
- **Read** them with the `@ethlete/core` signals: `injectQueryParam(key)`,
|
|
67
|
+
`injectQueryParams()`. `ActivatedRoute` and `router.url` are lint-banned.
|
|
68
|
+
- **Write** them with `inject(Router).navigate([], { queryParams, queryParamsHandling: 'merge' })`.
|
|
69
|
+
`merge` keeps the params you do not set; `null` removes a param.
|
|
70
|
+
- **Filter, sort and page state bound to the URL** is a query form: with `@ethlete/query`,
|
|
71
|
+
use `defineQueryForm` instead of writing the params by hand.
|
|
72
|
+
|
|
50
73
|
## Components
|
|
51
74
|
|
|
52
75
|
- Inline template/styles for small components; external `.html` / `.css` files
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-testing
|
|
3
|
+
description: How to unit-test an app built on the Ethlete SDK - views that fetch through @ethlete/query (HttpTestingController and the @ethlete/query/testing helpers), components that open overlays, and views that read the URL with injectQueryParam under provideRouter. Read before writing or fixing a spec for a component, view or service that uses @ethlete/query, @ethlete/core router signals or @ethlete/components overlays.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
requires: ['@ethlete/core']
|
|
7
|
+
vars: [docsBaseUrl]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Testing an Ethlete app
|
|
11
|
+
|
|
12
|
+
Specs run in jsdom under `TestBed`. Test through the public API the view uses - mount the
|
|
13
|
+
component, answer its requests, move the URL - and assert on what it renders or exposes. The full
|
|
14
|
+
reference for the query helpers is {%docsBaseUrl%}/query/testing.
|
|
15
|
+
|
|
16
|
+
## Views that fetch
|
|
17
|
+
|
|
18
|
+
A client made with `createQueryClient` is provided in root, so a spec provides nothing for it. It
|
|
19
|
+
needs Angular's HTTP testing backend, and answers each request by hand:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
23
|
+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
24
|
+
import { TestBed } from '@angular/core/testing';
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => TestBed.inject(HttpTestingController).verify());
|
|
31
|
+
|
|
32
|
+
it('renders the players', async () => {
|
|
33
|
+
const http = TestBed.inject(HttpTestingController);
|
|
34
|
+
const fixture = TestBed.createComponent(PlayersComponent);
|
|
35
|
+
fixture.detectChanges();
|
|
36
|
+
TestBed.tick();
|
|
37
|
+
|
|
38
|
+
http.expectOne((req) => req.url.includes('/players')).flush({ items: ['Müller'] });
|
|
39
|
+
await fixture.whenStable();
|
|
40
|
+
|
|
41
|
+
expect(fixture.nativeElement.textContent).toContain('Müller');
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- A GET query executes once its args resolve, which happens on change detection -
|
|
46
|
+
`TestBed.tick()` before `expectOne`, and again after `flush` before reading the query's
|
|
47
|
+
`response()`, `loading()` or `error()`.
|
|
48
|
+
- `req.url` carries the query string, so `expectOne('https://api.example.com/players?search=mul')`
|
|
49
|
+
matches the full URL. Match with a predicate, as above, when the params are not the point.
|
|
50
|
+
- Fail a request with `flush(body, { status: 422, statusText: 'Unprocessable Entity' })`.
|
|
51
|
+
- Mutations (`POST`, `PUT`, `PATCH`, `DELETE`) never auto-execute - call `.execute({ args })`,
|
|
52
|
+
then expect the request.
|
|
53
|
+
|
|
54
|
+
From `@ethlete/query/testing`:
|
|
55
|
+
|
|
56
|
+
| Export | Use |
|
|
57
|
+
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `expectAndFlush(http, url, body, status?)` | `expectOne(url).flush(body)` in one call. |
|
|
59
|
+
| `expectFlushAndWait(http, url, body, status?)` | The same, then `TestBed.tick()`. |
|
|
60
|
+
| `setupQueryTest(config?)` | A scratch client, `HttpTestingController`, `createGet` … `createDelete` creators - for testing code that takes a creator or client. |
|
|
61
|
+
| `setupAuthTest({ querySetup })` | A bearer auth provider on that scratch client, with `login()` / `refresh()` helpers that answer the requests. |
|
|
62
|
+
| `createFakeQueryPersistenceStore()` | In-memory persistence adapter - jsdom has no IndexedDB. |
|
|
63
|
+
| `installFakeBroadcastChannel()`, `installFakeWebLocks()`, `flushMultiTabSync` | Two clients in one spec behave as two tabs. |
|
|
64
|
+
| `createWebSocketTestDouble()` | A scripted socket.io `io` for `createWebSocketClient({ io })`. |
|
|
65
|
+
|
|
66
|
+
`setupQueryTest` calls `TestBed.configureTestingModule` itself and replaces `ErrorHandler` with a
|
|
67
|
+
no-op (`mockErrorHandler: false` keeps the real one). Call it inside `beforeEach`, and call
|
|
68
|
+
`restoreConsole()` in `afterEach` if the file spies on `console`.
|
|
69
|
+
|
|
70
|
+
## Views that read the URL
|
|
71
|
+
|
|
72
|
+
`injectQueryParam`, `injectQueryParams`, `injectPathParam` and the other router signals from
|
|
73
|
+
`@ethlete/core` read the router, so the spec provides one and navigates it:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { provideRouter, Router } from '@angular/router';
|
|
77
|
+
|
|
78
|
+
TestBed.configureTestingModule({
|
|
79
|
+
providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await TestBed.inject(Router).navigateByUrl('/?search=mul');
|
|
83
|
+
const fixture = TestBed.createComponent(PlayersComponent);
|
|
84
|
+
fixture.detectChanges();
|
|
85
|
+
TestBed.tick();
|
|
86
|
+
|
|
87
|
+
http.expectOne((req) => req.urlWithParams.includes('search=mul')).flush({ items: [] });
|
|
88
|
+
|
|
89
|
+
await TestBed.inject(Router).navigateByUrl('/?search=x');
|
|
90
|
+
TestBed.tick();
|
|
91
|
+
http.expectOne((req) => req.urlWithParams.includes('search=x')).flush({ items: [] });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Navigate to change a param - never set the signal or mock the inject function. A view that also
|
|
95
|
+
reads path params needs the real route in the config (`provideRouter([{ path: 'players/:id',
|
|
96
|
+
component: PlayersComponent }])`) and `RouterTestingHarness` from `@angular/router/testing` to
|
|
97
|
+
render it.
|
|
98
|
+
|
|
99
|
+
## Components that open overlays
|
|
100
|
+
|
|
101
|
+
An overlay mounts at the end of `document.body`, not in the fixture, and it opens and closes over
|
|
102
|
+
animation frames. Query the document, wait two frames, and close what is left after each test:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { injectOverlayManager } from '@ethlete/components';
|
|
106
|
+
|
|
107
|
+
const flushFrames = () =>
|
|
108
|
+
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
109
|
+
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
TestBed.runInInjectionContext(() => injectOverlayManager())
|
|
112
|
+
.openOverlays()
|
|
113
|
+
.forEach((ref) => ref.forceClose());
|
|
114
|
+
await flushFrames();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('opens the edit dialog', async () => {
|
|
118
|
+
const fixture = TestBed.createComponent(PlayerListComponent);
|
|
119
|
+
fixture.detectChanges();
|
|
120
|
+
|
|
121
|
+
fixture.nativeElement.querySelector('button.edit').click();
|
|
122
|
+
await flushFrames();
|
|
123
|
+
|
|
124
|
+
expect(document.querySelector('.player-edit-dialog')).not.toBeNull();
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Count open overlays with `injectOverlayManager().openOverlays().length` (inside
|
|
129
|
+
`TestBed.runInInjectionContext`).
|
|
130
|
+
- The `OverlayRef` that `createOverlayOpener(...).open()` returns has `componentInstance()`,
|
|
131
|
+
`close(result)` and `afterClosed()` - enough to test what the opener does with the result.
|
|
132
|
+
- A query-param overlay (`defineQueryParamOverlay`) opens from the URL: provide the router and
|
|
133
|
+
navigate with the param set, as above.
|
|
134
|
+
- jsdom has no `ResizeObserver`, `IntersectionObserver`, `matchMedia` or `Element.animate`. If a
|
|
135
|
+
spec throws on one of them, stub it once in the test setup file with an inert class or function,
|
|
136
|
+
not per spec.
|
|
137
|
+
|
|
138
|
+
## Before you call it done
|
|
139
|
+
|
|
140
|
+
A spec for a bug fix must fail without the fix - revert it, run the spec, see it fail, restore it.
|
|
141
|
+
To check the same change in the running app, read {%skill:verify-in-app%}.
|
|
@@ -12,12 +12,12 @@ A design exploration is a dialog. **The user is the designer. You find and frame
|
|
|
12
12
|
chooses.** A finding is not a licence to pick the fix.
|
|
13
13
|
|
|
14
14
|
The unit of work is a **call**: one open question, with every answer drawn side by side at
|
|
15
|
-
the same geometry.
|
|
15
|
+
the same geometry. A variant wins or loses only against the other variants of its call.
|
|
16
16
|
|
|
17
17
|
## The rules
|
|
18
18
|
|
|
19
|
-
1. **One open call at a time.** Take one question. Draw its
|
|
20
|
-
2. **Put the
|
|
19
|
+
1. **One open call at a time.** Take one question. Draw its variants. Stop.
|
|
20
|
+
2. **Put the variants in the call, then name your pick.** Two to four variants, drawn for
|
|
21
21
|
real, side by side, labelled, with what each one costs. Your pick is a proposal.
|
|
22
22
|
3. **Commit only when the user says commit.**
|
|
23
23
|
4. **A clear rejection starts the next call.** Settle and record the rejected call, then draw
|
|
@@ -33,31 +33,31 @@ finished while the user is still talking about it.
|
|
|
33
33
|
|
|
34
34
|
- The question, in one sentence.
|
|
35
35
|
- The call slug to look at.
|
|
36
|
-
- The
|
|
36
|
+
- The variants, labelled A, B, C, one line each.
|
|
37
37
|
- Your pick, one line on why.
|
|
38
38
|
- Nothing else. No next step, no second finding.
|
|
39
39
|
|
|
40
40
|
**Do not send a screenshot.** The user keeps the page open. Screenshot only for your own
|
|
41
|
-
check that
|
|
41
|
+
check that a variant renders.
|
|
42
42
|
|
|
43
43
|
If a second defect appears, add it to the open list and give it one line. Do not draw it.
|
|
44
44
|
|
|
45
|
-
## Where the
|
|
45
|
+
## Where the variants are drawn
|
|
46
46
|
|
|
47
47
|
The exploration's plan file names the tool. Two shapes exist.
|
|
48
48
|
|
|
49
|
-
**A repository with a `.ethlete/design` folder** draws a call per page, each
|
|
49
|
+
**A repository with a `.ethlete/design` folder** draws a call per page, each variant in its own
|
|
50
50
|
iframe. The tool is `et design`, from `@ethlete/cli`, so the repository itself needs no design
|
|
51
51
|
tooling. A call argues in the repository it is about:
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
.ethlete/design/
|
|
55
|
-
config.json
|
|
55
|
+
config.json the port, the default call, and one entry per project
|
|
56
56
|
calls/<project>/<group>/<slug>/
|
|
57
|
-
call.ts
|
|
58
|
-
fixture.ts
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
call.ts the eyebrow, the headline, the intro, frameWidth, the variants and the round prose
|
|
58
|
+
fixture.ts the data every variant shares
|
|
59
|
+
variant-a.ts one default-exported drawing per variant
|
|
60
|
+
variant-b.ts
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
The first segment of a slug is the **project**. `config.json` gives each project its own
|
|
@@ -73,16 +73,16 @@ Ethlete Studio carries its own copy of the tool, so it draws a checkout that ins
|
|
|
73
73
|
The machine still needs Node, and the render stage still needs `playwright` where the copy in
|
|
74
74
|
use can reach it.
|
|
75
75
|
|
|
76
|
-
`call.ts` calls `defineCall` from `@design-explore`. Each
|
|
76
|
+
`call.ts` calls `defineCall` from `@design-explore`. Each variant carries a `key`, a `name`,
|
|
77
77
|
a `claim`, a `cost`, an optional `verdict` of `chosen` or `rejected`, and a `load` that
|
|
78
|
-
imports its own module. The host greys a rejected
|
|
78
|
+
imports its own module. The host greys a rejected variant and shows it on hover.
|
|
79
79
|
|
|
80
|
-
In a call that runs past about a dozen
|
|
80
|
+
In a call that runs past about a dozen variants, every variant names the pass that drew it with
|
|
81
81
|
`round: 'r3'`. **That tag is the only thing that makes a round exist.** The host reads the
|
|
82
|
-
|
|
83
|
-
same bands in the sidebar, so a new
|
|
84
|
-
|
|
85
|
-
per rejected
|
|
82
|
+
variants, draws one band per round in the order the variants introduce them, and lists those
|
|
83
|
+
same bands in the sidebar, so a new variant can never leave the menu stale. A round whose
|
|
84
|
+
variants all carry a verdict is **settled**: the page folds it down to its winner plus one row
|
|
85
|
+
per rejected variant, and a link opens it again.
|
|
86
86
|
|
|
87
87
|
A call whose rounds have all ruled is **resolved**. Its winners usually form a chain, each one
|
|
88
88
|
the last plus a change, so the page stops drawing them: it leads with a **Result** band holding
|
|
@@ -92,36 +92,36 @@ rows. A key in the chain opens the round that drew it.
|
|
|
92
92
|
`rounds` is prose only. Each entry gives a `key`, a `title` and a `note` saying what came out
|
|
93
93
|
of that pass, which is how the intro stays short and each pass reads as a reply to the one
|
|
94
94
|
before. An untagged round still draws and still gets a menu row - it says the bare key until
|
|
95
|
-
somebody writes the entry. A `rounds` entry that no
|
|
95
|
+
somebody writes the entry. A `rounds` entry that no variant names is stale prose, and the host
|
|
96
96
|
and the check both report it. Write the note in the same pass that sets the verdicts, never
|
|
97
97
|
before the user rules.
|
|
98
98
|
|
|
99
|
-
A call with one
|
|
99
|
+
A call with one variant and no `claim` is a **view**: one reference picture, drawn full
|
|
100
100
|
width with no verdict tag. Use it for a picture that answers no question.
|
|
101
101
|
|
|
102
102
|
Two views, and a way to compare:
|
|
103
103
|
|
|
104
104
|
- **rounds** is the default. Round headings, the result band, and the folding above.
|
|
105
|
-
- **all N** is a contact sheet: every
|
|
106
|
-
a call of two dozen fits on a screen or two. Use it to find the
|
|
107
|
-
- Clicking
|
|
105
|
+
- **all N** is a contact sheet: every variant in the call at about a third size, in one grid, so
|
|
106
|
+
a call of two dozen fits on a screen or two. Use it to find the variants worth a close look.
|
|
107
|
+
- Clicking a variant's name anywhere - a heading, a folded row, a thumbnail - puts it in the
|
|
108
108
|
**compare overlay** at the top of the page. Two drawings that differ by a few pixels can only
|
|
109
109
|
be told apart in one place, so the overlay stacks every pick in one box at full size and the
|
|
110
110
|
reader switches between them: click the box or press space to blink, and with two picks the
|
|
111
111
|
arrow keys wipe a seam across. Nothing is scaled and nothing moves. The picks live in the URL
|
|
112
112
|
under `pick`, so a comparison is a link you can send.
|
|
113
113
|
|
|
114
|
-
Three constraints the tool puts on
|
|
114
|
+
Three constraints the tool puts on a variant file:
|
|
115
115
|
|
|
116
116
|
- **Never import a package barrel.** The libraries resolve to source, so one barrel makes
|
|
117
117
|
the browser request every module in the library, and the requests fail with
|
|
118
118
|
`ERR_INSUFFICIENT_RESOURCES`. Import the one file you need.
|
|
119
|
-
- **The fixture is shared, and
|
|
119
|
+
- **The fixture is shared, and a variant may not change it.** Variants drawn at three
|
|
120
120
|
geometries cannot be compared.
|
|
121
|
-
- **One file per
|
|
121
|
+
- **One file per variant**, so several agents can draw at once, and a broken variant breaks
|
|
122
122
|
its own frame only.
|
|
123
123
|
|
|
124
|
-
**A repository with a sketch Storybook** draws every
|
|
124
|
+
**A repository with a sketch Storybook** draws every variant in the **same** story, side by
|
|
125
125
|
side, under the real geometry the thing ships in. The default Storybook is
|
|
126
126
|
{%storybookUrl%}; an app with its own sketch Storybook names its port in the plan file.
|
|
127
127
|
|
|
@@ -130,7 +130,7 @@ Either way:
|
|
|
130
130
|
- Sketches take inputs only and stay out of the application's build.
|
|
131
131
|
- Prototype, never refactor. Do not touch the shipped component until the treatment is
|
|
132
132
|
settled.
|
|
133
|
-
- Keep the rejected
|
|
133
|
+
- Keep the rejected variants drawn, marked as rejected.
|
|
134
134
|
|
|
135
135
|
## Check before you look
|
|
136
136
|
|
|
@@ -159,30 +159,30 @@ transpile without type checking, so the render reports `ok` on a file that does
|
|
|
159
159
|
compile. Open it only when the check passes and the drawing still does not appear:
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
et design check --call <slug>
|
|
163
|
-
et design check --call <slug> --
|
|
162
|
+
et design check --call <slug> # every variant of the call
|
|
163
|
+
et design check --call <slug> --variant b # one of them
|
|
164
164
|
node check-story.mjs --tsconfig <path> --story <story-id>
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
## Delegating a call
|
|
168
168
|
|
|
169
|
-
A call has one job per
|
|
169
|
+
A call has one job per variant, so it fans out. Every brief names `model: opus`.
|
|
170
170
|
|
|
171
|
-
1. **One agent per
|
|
171
|
+
1. **One agent per variant.** Give it the call folder, the variant key, the fixture it must
|
|
172
172
|
not change, and the one claim its drawing has to support. Tell it to write its own
|
|
173
|
-
|
|
173
|
+
variant file and nothing else, so two agents never touch one file.
|
|
174
174
|
2. **One check-and-fix agent, after the drawing agents return.** Give it the changed files
|
|
175
175
|
and the call slug. It runs the check above, fixes what it reports, and repeats until the
|
|
176
|
-
check says `ok`. It may not change what
|
|
176
|
+
check says `ok`. It may not change what a variant draws, only what stops it rendering.
|
|
177
177
|
3. **One write-up agent, once the user settles the call.** Give it the verdict in the
|
|
178
178
|
user's own words and the plan file. It records what won, what lost and why, it sets each
|
|
179
|
-
|
|
179
|
+
variant's `verdict` in `call.ts`, and it writes that round's `note`. It runs while you open
|
|
180
180
|
the next call.
|
|
181
181
|
|
|
182
182
|
## Screenshots
|
|
183
183
|
|
|
184
184
|
**Off by default.** The user has the page open and sends you a picture when something looks
|
|
185
|
-
wrong. Take one only when the code cannot tell you whether two
|
|
185
|
+
wrong. Take one only when the code cannot tell you whether two variants really differ, and
|
|
186
186
|
never to put in front of the user. Copy {%resource:shoot-template.mjs%} to the repository
|
|
187
187
|
root.
|
|
188
188
|
|
|
@@ -198,6 +198,6 @@ node shoot.mjs <story-id> 1100 760 out.png
|
|
|
198
198
|
## Writing it down
|
|
199
199
|
|
|
200
200
|
Every settled call goes in the exploration's plan file: what won, what lost, and why. Keep
|
|
201
|
-
an **Open** list for the calls not yet made. A rejected
|
|
201
|
+
an **Open** list for the calls not yet made. A rejected variant written down stops the next
|
|
202
202
|
session from drawing it again. After a clear rejection, use that record to frame and draw the
|
|
203
203
|
next call; do not wait for the user to type “continue”.
|
|
@@ -16,10 +16,10 @@ what gets deployed to a test environment and, once accepted, merged into
|
|
|
16
16
|
**Never name a branch by hand - `start` does it from the grammar:**
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
{%packageRunner%} ethlete-agents git-flow start FIP-2178 # reads the issue, names it, branches off the right base
|
|
20
|
+
{%packageRunner%} ethlete-agents git-flow check # is the current branch conforming?
|
|
21
|
+
{%packageRunner%} ethlete-agents git-flow explain <branch> # what the parser sees, and what it expects
|
|
22
|
+
{%packageRunner%} ethlete-agents git-flow repair <branch> # rename a non-conforming one, retarget its MRs
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
`start` prints its plan (branch, base, MR target) and asks before writing anything; add
|