@genesislcap/genx 15.23.3 → 15.24.0-FUI-2603.10
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 +33 -0
- package/agent-rules/manifest.json +20 -0
- package/agent-rules/propose-upstream-feature.md +219 -0
- package/agent-rules/report-upstream-bug.md +253 -0
- package/dist/commands/agent-rules.d.ts +6 -0
- package/dist/commands/agent-rules.d.ts.map +1 -0
- package/dist/commands/agent-rules.js +79 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/index.js +17 -0
- package/dist/utils/agent-rules.d.ts +82 -0
- package/dist/utils/agent-rules.d.ts.map +1 -0
- package/dist/utils/agent-rules.js +178 -0
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -26,6 +26,39 @@ To enable this module in your application, follow the steps below.
|
|
|
26
26
|
|
|
27
27
|
## [API Docs](./docs/api/index.md)
|
|
28
28
|
|
|
29
|
+
## Agent rules
|
|
30
|
+
|
|
31
|
+
`genx agent-rules` installs the shared AI agent rules that ship with this package
|
|
32
|
+
([`agent-rules/`](./agent-rules)) into a consumer project, and wires them into whichever assistants
|
|
33
|
+
that project uses.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx genx agent-rules # install and wire up detected assistants
|
|
37
|
+
npx genx agent-rules .. # target another folder (see below)
|
|
38
|
+
npx genx agent-rules --tools claude,cursor,gemini # wire up specific assistants
|
|
39
|
+
npx genx agent-rules --check # CI: fail when installed rules are stale
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`[folder]` defaults to the current directory, and it should be the folder the assistants read -
|
|
43
|
+
usually the repo root. In a Genesis app that is one level **above** `client/`, where `package.json`
|
|
44
|
+
lives, so run `npx genx agent-rules ..` from `client/`. Run bare in a folder with no assistant
|
|
45
|
+
files, the command looks up the tree as far as the repo root and, if it finds the real root, points
|
|
46
|
+
at it and writes nothing rather than leaving an orphaned second copy behind (`--tools` installs in
|
|
47
|
+
the current folder anyway).
|
|
48
|
+
|
|
49
|
+
The rule markdown is copied to `docs/agent-rules/` and referenced from a managed block in
|
|
50
|
+
`CLAUDE.md`, `GEMINI.md`, or `AGENTS.md`, and from a generated `.cursor/rules/<rule>.mdc` for
|
|
51
|
+
Cursor. Content outside the managed block is never touched. Installed copies are managed — re-run
|
|
52
|
+
the command after `genx upgrade` to pick up rule changes, and propose edits in
|
|
53
|
+
[foundation-ui](https://github.com/genesislcap/foundation-ui) rather than locally.
|
|
54
|
+
|
|
55
|
+
Currently shipped:
|
|
56
|
+
|
|
57
|
+
| Rule | Purpose |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `report-upstream-bug.md` | Something **broken** upstream: file a bug in `genesislcap/foundation-ui` — with the human's confirmation — instead of quietly working around it in the consumer project. |
|
|
60
|
+
| `propose-upstream-feature.md` | Something **missing** upstream: propose the capability instead of building the fifth private copy of it. Includes a generality test so app code stays app code. |
|
|
61
|
+
|
|
29
62
|
## Event type codegen
|
|
30
63
|
|
|
31
64
|
When `eventTypes.enabled` is set in `genx.config.*` or `package.json` (`genx` / `genesis` keys), `genx build` and `genx generate event-types` emit typed `EventDetailsMap` from Kotlin handler DTOs and generated table DAOs. See [@genesislcap/event-type-codegen](../event-type-codegen/README.md) for configuration, limitations, and migration.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"targetDir": "docs/agent-rules",
|
|
4
|
+
"rules": [
|
|
5
|
+
{
|
|
6
|
+
"file": "report-upstream-bug.md",
|
|
7
|
+
"name": "report-upstream-bug",
|
|
8
|
+
"title": "Report Foundation UI bugs upstream",
|
|
9
|
+
"description": "File a bug upstream in genesislcap/foundation-ui instead of working around it locally. Use whenever a defect is traced into a @genesislcap/* package - a stack frame in node_modules/@genesislcap, or behaviour contradicting the package's docs or types - or before writing any monkey-patch, patch-package entry, vendored copy, version pin, or cast that exists to route around upstream behaviour.",
|
|
10
|
+
"cursorDescription": "Use when a bug is traced into a @genesislcap package, when weighing a workaround for upstream behaviour, or before patching node_modules or pinning a @genesislcap version."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"file": "propose-upstream-feature.md",
|
|
14
|
+
"name": "propose-upstream-feature",
|
|
15
|
+
"title": "Propose platform features upstream",
|
|
16
|
+
"description": "Propose a missing platform capability in genesislcap/foundation-ui instead of building the fifth private copy of it. Use before building a generic component, formatter, or adapter with no business rules in it, before wrapping or re-implementing a @genesislcap/* component that lacks one option, or when the same thing has already been built in another Genesis app.",
|
|
17
|
+
"cursorDescription": "Use before building a generic component, utility, or adapter that has no business rules in it, when extending a @genesislcap component that lacks an option, or when the same capability already exists privately in another Genesis app."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<!-- Managed by `genx agent-rules` (@genesislcap/genx). Do not edit this copy — it is overwritten on every run. Propose changes in genesislcap/foundation-ui. -->
|
|
2
|
+
|
|
3
|
+
# Propose platform features upstream (don't build the fifth private copy)
|
|
4
|
+
|
|
5
|
+
> **Purpose**: Some of what a Genesis app needs is app-specific, and some of it is platform
|
|
6
|
+
> capability that simply hasn't been built yet. The second kind gets built privately in one app,
|
|
7
|
+
> then again in the next, and every copy has to be maintained against upstream API changes forever.
|
|
8
|
+
> This rule catches that at the moment of writing: if the thing belongs in a `@genesislcap/*`
|
|
9
|
+
> package, it gets proposed in
|
|
10
|
+
> [`genesislcap/foundation-ui`](https://github.com/genesislcap/foundation-ui) — and only then, with
|
|
11
|
+
> approval, built locally as a temporary seam.
|
|
12
|
+
>
|
|
13
|
+
> The sibling rule for defects is `report-upstream-bug.md`. Something that is *broken* upstream is a
|
|
14
|
+
> bug; something that is *missing* upstream is this rule.
|
|
15
|
+
|
|
16
|
+
## When this rule applies
|
|
17
|
+
|
|
18
|
+
Apply it before writing code, as soon as any of these is true:
|
|
19
|
+
|
|
20
|
+
- You are about to build a generic capability — a UI component, a formatter, a datasource or grid
|
|
21
|
+
adapter, an auth/comms/layout concern — with no business rules in it.
|
|
22
|
+
- You are extending an existing `@genesislcap/*` component by wrapping, subclassing, or
|
|
23
|
+
re-implementing it, because it lacks one attribute, slot, event, or option.
|
|
24
|
+
- Someone says "we built this in the other app" or you can see code copied in from another Genesis
|
|
25
|
+
project.
|
|
26
|
+
- The thing would have to track an upstream API to keep working — a private fork of platform
|
|
27
|
+
behaviour rather than a consumer of it.
|
|
28
|
+
- Upstream documents or types the capability partially and you are filling the gap locally.
|
|
29
|
+
|
|
30
|
+
It does **not** apply to app code, and most code is app code:
|
|
31
|
+
|
|
32
|
+
- Business rules, pricing, workflow, entitlements, domain validation.
|
|
33
|
+
- Client-specific branding, copy, layout, or one-off screens.
|
|
34
|
+
- Anything whose API you cannot describe without this app's domain nouns.
|
|
35
|
+
- Spikes and prototypes you intend to delete.
|
|
36
|
+
- Work under a deadline where waiting on upstream is not an option — build it locally now, and
|
|
37
|
+
propose it in parallel (Step 6 covers the seam).
|
|
38
|
+
|
|
39
|
+
## Non-negotiables
|
|
40
|
+
|
|
41
|
+
1. **Apply the generality test in Step 2 before proposing anything.** A proposal for app-specific
|
|
42
|
+
code wastes the platform team's time and teaches them to ignore the next one.
|
|
43
|
+
2. **Never** run `gh issue create` unprompted. Draft the proposal, show it to the human, wait for a
|
|
44
|
+
clear yes. Filing is an outward-facing action on a shared repo.
|
|
45
|
+
3. **Never** fork or vendor an upstream component to change a few lines without approval — propose
|
|
46
|
+
the option upstream and wrap locally instead.
|
|
47
|
+
4. **Building the local version needs explicit human approval** (Step 6), like any workaround.
|
|
48
|
+
5. **Don't overclaim demand.** "Two apps need this" is only true if you can name them.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Step 1 — Check it doesn't already exist
|
|
53
|
+
|
|
54
|
+
Platform surface is large and under-documented; assume it might already be there.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Search the installed packages' public types for the capability.
|
|
58
|
+
grep -rl "<CapabilityName>" node_modules/@genesislcap/*/dist/dts 2>/dev/null | head
|
|
59
|
+
|
|
60
|
+
# What does the package actually export?
|
|
61
|
+
cat node_modules/@genesislcap/<pkg>/dist/dts/index.d.ts | grep -i "<keyword>"
|
|
62
|
+
|
|
63
|
+
# Newer versions may already have it.
|
|
64
|
+
npm view @genesislcap/<pkg> version
|
|
65
|
+
npm view @genesislcap/<pkg> dist-tags
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Also check the package's `README.md` and `docs/api/` inside `node_modules`, and the showcase apps
|
|
69
|
+
if you have the monorepo to hand. If it exists, use it and stop here.
|
|
70
|
+
|
|
71
|
+
## Step 2 — Apply the generality test
|
|
72
|
+
|
|
73
|
+
Answer all four. Any "no" means it is app code: build it locally and stop.
|
|
74
|
+
|
|
75
|
+
1. **Name two independent consumers.** Two real projects or teams that would use this, not two
|
|
76
|
+
screens in this app.
|
|
77
|
+
2. **Describe the API without this app's domain nouns.** If the signature needs
|
|
78
|
+
`Trade`, `Counterparty`, or your client's name to make sense, it is not platform capability.
|
|
79
|
+
3. **No app-specific dependencies.** It must not reach into this app's services, routes, config
|
|
80
|
+
shape, or backend contract.
|
|
81
|
+
4. **The behaviour is stable, not still being discovered.** If the design is likely to change twice
|
|
82
|
+
more this sprint, it is not ready to be someone else's API.
|
|
83
|
+
|
|
84
|
+
Borderline cases exist. When it half-passes, the honest proposal is usually smaller than the thing
|
|
85
|
+
you were about to build: a new attribute, slot, event, or option on an existing component rather
|
|
86
|
+
than a new component.
|
|
87
|
+
|
|
88
|
+
## Step 3 — Check it isn't already proposed
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
gh issue list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
|
|
92
|
+
gh pr list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **An open issue matches** → add your use case and your second consumer to it, after the human
|
|
96
|
+
approves. That is worth more to the platform team than a duplicate: it is evidence of demand.
|
|
97
|
+
- **A closed issue matches** → read why. If it was declined, respect the decision and build locally
|
|
98
|
+
(Step 6); the reasoning usually tells you what shape *would* be accepted.
|
|
99
|
+
- **A merged PR matches** → it may be released already, or available on a pre-release dist-tag.
|
|
100
|
+
|
|
101
|
+
## Step 4 — Decide the shape, smallest first
|
|
102
|
+
|
|
103
|
+
Pick the least invasive option that solves it, and say in the proposal which you chose and why:
|
|
104
|
+
|
|
105
|
+
| Shape | Use when | Cost to upstream |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| New attribute / slot / event / option on an existing component | The component does 90% of it | Lowest — usually additive and non-breaking |
|
|
108
|
+
| New export from an existing package | It is a utility, formatter, or type that fits a package's remit | Low |
|
|
109
|
+
| New component in an existing package | It is a peer of what that package already ships | Medium |
|
|
110
|
+
| New package | Nothing existing has the remit | Highest — expect to justify it |
|
|
111
|
+
|
|
112
|
+
Additive and backward-compatible beats elegant-but-breaking. If your idea only works as a breaking
|
|
113
|
+
change, say so explicitly and expect it to be scheduled rather than accepted outright.
|
|
114
|
+
|
|
115
|
+
## Step 5 — Draft the proposal, then file it once the human confirms
|
|
116
|
+
|
|
117
|
+
Title: what the capability is, in the platform's vocabulary — not your app's.
|
|
118
|
+
|
|
119
|
+
- Good: `grid-pro: expose a column-level formatter hook for cell values`
|
|
120
|
+
- Bad: `Need to format trade dates in the blotter`
|
|
121
|
+
|
|
122
|
+
Body:
|
|
123
|
+
|
|
124
|
+
````markdown
|
|
125
|
+
## What's missing
|
|
126
|
+
|
|
127
|
+
One paragraph: the capability, and where it would live (`@genesislcap/<pkg>`, ideally the exact
|
|
128
|
+
component or module).
|
|
129
|
+
|
|
130
|
+
## Who needs it
|
|
131
|
+
|
|
132
|
+
The projects or teams that would use this, named. If it has already been built privately more
|
|
133
|
+
than once, say where — a duplicated implementation is the strongest argument for adopting it.
|
|
134
|
+
|
|
135
|
+
## What we do today
|
|
136
|
+
|
|
137
|
+
The local implementation or workaround, with its cost: lines of code, what it forks, what breaks
|
|
138
|
+
when upstream changes.
|
|
139
|
+
|
|
140
|
+
## Proposed API
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// The smallest shape that solves it — attribute, option, export, or signature.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Include the default, and confirm existing behaviour is unchanged when the new option is absent.
|
|
147
|
+
|
|
148
|
+
## Alternatives considered
|
|
149
|
+
|
|
150
|
+
Including "keep it app-side", and why that loses.
|
|
151
|
+
|
|
152
|
+
## Breaking change?
|
|
153
|
+
|
|
154
|
+
No / yes, with the migration if yes.
|
|
155
|
+
|
|
156
|
+
## Can we contribute it?
|
|
157
|
+
|
|
158
|
+
Whether this team can raise the PR, and by when.
|
|
159
|
+
````
|
|
160
|
+
|
|
161
|
+
Then, after an explicit yes — with the body in a relative file, so this works on Windows too, and
|
|
162
|
+
deleted afterwards rather than committed:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
gh issue create \
|
|
166
|
+
--repo genesislcap/foundation-ui \
|
|
167
|
+
--title "<title>" \
|
|
168
|
+
--body-file temp-upstream-proposal.md \
|
|
169
|
+
--label "Enhancement" \
|
|
170
|
+
--label "<area label>"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use `gh label list --repo genesislcap/foundation-ui --limit 100` for the area label — the same
|
|
174
|
+
areas as the bug rule (`Grids`, `COMMS`, `Design System`, `Forms`, `Layout`,
|
|
175
|
+
`Frontends Compatibility`, …). Add `Next` when it is capability the platform is expected to grow
|
|
176
|
+
into rather than something needed this sprint. Delete the temp file, then report the URL.
|
|
177
|
+
|
|
178
|
+
## Step 6 — Build locally as a seam, with approval
|
|
179
|
+
|
|
180
|
+
A filed proposal does not ship your feature. Once the issue URL exists:
|
|
181
|
+
|
|
182
|
+
1. **Present the options and their costs** — build behind a local seam now, wait for upstream, or
|
|
183
|
+
contribute the PR yourself and consume a pre-release. Include "wait" when it is viable.
|
|
184
|
+
2. **Wait for an explicit choice.** Do not start editing before that.
|
|
185
|
+
3. **Once approved**, build it so it can be deleted rather than untangled:
|
|
186
|
+
- Keep it behind **one boundary** — an adapter, a wrapper component, a single module the app
|
|
187
|
+
imports from. When upstream lands, one file changes, not thirty call sites.
|
|
188
|
+
- Match the proposed upstream API, so adopting the real thing is an import swap.
|
|
189
|
+
- Mark it with the issue and the removal condition:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// UPSTREAM-CANDIDATE(genesislcap/foundation-ui#2501):
|
|
193
|
+
// Column-level value formatter; grid-pro has no hook for this yet.
|
|
194
|
+
// Remove when: the hook ships in @genesislcap/grid-pro and this app adopts it.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
4. **Log it** in the shared tracking file (create it if absent — `docs/upstream-tracking.md`):
|
|
198
|
+
|
|
199
|
+
````markdown
|
|
200
|
+
| Kind | Issue | Package | What we do locally | Remove when |
|
|
201
|
+
| --- | --- | --- | --- | --- |
|
|
202
|
+
| feature | genesislcap/foundation-ui#2501 | grid-pro | local formatter adapter | hook released and adopted |
|
|
203
|
+
````
|
|
204
|
+
|
|
205
|
+
## Step 7 — Offer the PR
|
|
206
|
+
|
|
207
|
+
Proposals with a PR attached land far sooner than proposals without one, and you have already
|
|
208
|
+
written the implementation once. `foundation-ui` accepts contributions from consumer teams — see its
|
|
209
|
+
[CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md) for
|
|
210
|
+
setup, commit format, and the PR template. A pre-release can be cut from the branch so this app can
|
|
211
|
+
adopt it before it merges (`@genesislcap/<pkg>@<dist-tag>`); ask in the issue.
|
|
212
|
+
|
|
213
|
+
## Never
|
|
214
|
+
|
|
215
|
+
- Propose app business logic as platform capability.
|
|
216
|
+
- Fork or vendor an upstream component to change a few lines instead of proposing the option.
|
|
217
|
+
- Claim demand you cannot name, or a use case you have not built.
|
|
218
|
+
- Design an API around this app's backend contract and present it as generic.
|
|
219
|
+
- Leave the local implementation as the only record — the marker must name the issue.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<!-- Managed by `genx agent-rules` (@genesislcap/genx). Do not edit this copy — it is overwritten on every run. Propose changes in genesislcap/foundation-ui. -->
|
|
2
|
+
|
|
3
|
+
# Report Foundation UI bugs upstream (don't silently work around them)
|
|
4
|
+
|
|
5
|
+
> **Purpose**: When a bug in this project turns out to live inside a `@genesislcap/*` package,
|
|
6
|
+
> the fix belongs in [`genesislcap/foundation-ui`](https://github.com/genesislcap/foundation-ui),
|
|
7
|
+
> not in a local patch. A workaround that is never reported keeps every other consumer project
|
|
8
|
+
> broken and quietly becomes permanent. This rule makes "raise it upstream" the default path and
|
|
9
|
+
> a local workaround the deliberate, approved, clearly-marked exception.
|
|
10
|
+
>
|
|
11
|
+
> The sibling rule for missing capability is `propose-upstream-feature.md`. Something that is
|
|
12
|
+
> *broken* upstream is this rule; something that is *missing* upstream is that one.
|
|
13
|
+
|
|
14
|
+
## When this rule applies
|
|
15
|
+
|
|
16
|
+
Apply it as soon as any of these is true:
|
|
17
|
+
|
|
18
|
+
- A stack frame, breakpoint, or console error points into `node_modules/@genesislcap/**`.
|
|
19
|
+
- The behaviour of a `@genesislcap/*` package contradicts its own docs, types, JSDoc, or
|
|
20
|
+
README — including a `CustomEvent<unknown>` where a typed detail is documented.
|
|
21
|
+
- You are about to write, or have caught yourself considering, any of these:
|
|
22
|
+
- a monkey-patch, prototype override, or `patch-package` entry against a `@genesislcap/*` package;
|
|
23
|
+
- a copy of an upstream component/function into this repo so a few lines can be changed;
|
|
24
|
+
- `as any` / `@ts-expect-error` / `// eslint-disable` purely to route around a wrong upstream type;
|
|
25
|
+
- a CSS override fighting an upstream style (`!important` into a shadow part, etc.);
|
|
26
|
+
- pinning or downgrading a `@genesislcap/*` version to dodge a regression;
|
|
27
|
+
- a `setTimeout`, extra re-render, retry, or "nudge" that exists only to compensate for
|
|
28
|
+
upstream timing.
|
|
29
|
+
|
|
30
|
+
It does **not** apply when the cause is on this side: wrong usage of a documented API, a version
|
|
31
|
+
mismatch between `@genesislcap/*` packages, missing peer dependencies, or local configuration.
|
|
32
|
+
Step 1 exists to tell those apart.
|
|
33
|
+
|
|
34
|
+
## Non-negotiables
|
|
35
|
+
|
|
36
|
+
1. **The upstream issue is filed before local code is changed** to route around the bug.
|
|
37
|
+
2. **Never** modify anything under `node_modules/` (or add a `patch-package` patch, or vendor a
|
|
38
|
+
copy of upstream source) without explicit human approval.
|
|
39
|
+
3. **Never** run `gh issue create` unprompted. Draft the issue, show it to the human, wait for a
|
|
40
|
+
clear yes, then file it. Filing an issue is an outward-facing action on a shared repo.
|
|
41
|
+
4. **Any workaround needs explicit human approval** — present the options and the trade-offs, do
|
|
42
|
+
not pick one and start editing (see Step 6).
|
|
43
|
+
5. **Be honest about what you verified.** Separate "confirmed by running it" from "suspected".
|
|
44
|
+
A confidently-wrong root cause wastes more upstream time than no root cause at all.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Step 1 — Rule out the boring causes
|
|
49
|
+
|
|
50
|
+
Do this before writing a single line of the issue.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# What is actually installed (the lockfile, not the range in package.json)?
|
|
54
|
+
npm ls @genesislcap/<package> # or: pnpm why @genesislcap/<package>
|
|
55
|
+
|
|
56
|
+
# Is it the latest? Is there a newer patch or a prerelease dist-tag?
|
|
57
|
+
npm view @genesislcap/<package> version
|
|
58
|
+
npm view @genesislcap/<package> dist-tags
|
|
59
|
+
|
|
60
|
+
# Are the @genesislcap/* packages on matching versions? Mixed majors/minors cause
|
|
61
|
+
# symptoms that look exactly like upstream bugs but are dependency drift.
|
|
62
|
+
npm ls --all 2>/dev/null | grep @genesislcap
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then confirm the calling code follows the documented API — read the package's own `dist/dts/*.d.ts`
|
|
66
|
+
and README rather than assuming. If the answer is "we were holding it wrong", fix it locally and
|
|
67
|
+
stop here; this rule is done.
|
|
68
|
+
|
|
69
|
+
## Step 2 — Check it isn't already known or already fixed
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Open AND closed issues — a closed one may name the release that fixed it.
|
|
73
|
+
gh issue list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
|
|
74
|
+
|
|
75
|
+
# Same for PRs — a fix may be merged but not yet released.
|
|
76
|
+
gh pr list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Outcomes:
|
|
80
|
+
|
|
81
|
+
- **An open issue matches** → don't file a duplicate. Add a comment with your new evidence
|
|
82
|
+
(your version, your repro, your environment) after the human approves, and link it in your report.
|
|
83
|
+
- **A closed issue matches and a newer version exists** → the fix is likely released. Recommend the
|
|
84
|
+
upgrade instead of an issue.
|
|
85
|
+
- **Nothing matches** → continue to Step 3.
|
|
86
|
+
|
|
87
|
+
## Step 3 — Collect the evidence
|
|
88
|
+
|
|
89
|
+
An upstream maintainer needs to reproduce this without access to your app. Gather:
|
|
90
|
+
|
|
91
|
+
| What | How |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| Root-cause package + exact version | The package the broken code lives in, e.g. `@genesislcap/foundation-comms@15.3.1` |
|
|
94
|
+
| Symptom package + exact version | Where it *appears* to break, if different, e.g. `@genesislcap/grid-pro@15.3.1` |
|
|
95
|
+
| The offending code | Real path + quoted snippet from `node_modules/@genesislcap/<pkg>/dist/...` |
|
|
96
|
+
| Minimal reproduction | Numbered steps. Say plainly whether you executed them or are proposing them |
|
|
97
|
+
| Expected vs actual | Both stated as observable behaviour, not as internal state |
|
|
98
|
+
| Environment | Framework + version (React 19, Angular, etc.), bundler, browser, and relevant third-party versions (`ag-grid-community`, `@microsoft/fast-element`, …) |
|
|
99
|
+
| Evidence | Console output, network/websocket trace, screenshots — trimmed to the relevant lines |
|
|
100
|
+
| Workaround status | What unblocks the app today (even if it's "nothing yet") |
|
|
101
|
+
|
|
102
|
+
Redact anything client-confidential: hostnames, credentials, real user data, and business-specific
|
|
103
|
+
resource names. Replace them with placeholders (`SOME_DATASERVER_QUERY`) — a repro does not need
|
|
104
|
+
them to be real.
|
|
105
|
+
|
|
106
|
+
## Step 4 — Draft the issue
|
|
107
|
+
|
|
108
|
+
Title: a specific, searchable one-liner that names the mechanism, not just the symptom. No
|
|
109
|
+
conventional-commit prefix.
|
|
110
|
+
|
|
111
|
+
- Good: `sendForStream() defers DATA_LOGON via DOM.queueUpdate — StrictMode double-mount discards it, leaving every dataserver grid empty`
|
|
112
|
+
- Bad: `Grid is empty`
|
|
113
|
+
|
|
114
|
+
Body — use this structure (it mirrors
|
|
115
|
+
[#2442](https://github.com/genesislcap/foundation-ui/issues/2442), a good reference report):
|
|
116
|
+
|
|
117
|
+
````markdown
|
|
118
|
+
## Package + version
|
|
119
|
+
|
|
120
|
+
Root cause: **`@genesislcap/<pkg>@<version>`**
|
|
121
|
+
Visible symptom in: **`@genesislcap/<pkg>@<version>`** (if different)
|
|
122
|
+
|
|
123
|
+
From the consumer's `package.json`:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
"@genesislcap/<pkg>": "<version>"
|
|
127
|
+
"<relevant third-party dep>": "<version>"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Summary
|
|
131
|
+
|
|
132
|
+
One paragraph: the mechanism and its user-visible consequence.
|
|
133
|
+
|
|
134
|
+
## Reproduction
|
|
135
|
+
|
|
136
|
+
State up front whether this was confirmed end-to-end or is a proposed reduction.
|
|
137
|
+
|
|
138
|
+
1. …
|
|
139
|
+
2. …
|
|
140
|
+
3. Observe: …
|
|
141
|
+
|
|
142
|
+
## Expected vs Actual
|
|
143
|
+
|
|
144
|
+
**Expected**
|
|
145
|
+
- …
|
|
146
|
+
|
|
147
|
+
**Actual**
|
|
148
|
+
- …
|
|
149
|
+
|
|
150
|
+
## Root cause
|
|
151
|
+
|
|
152
|
+
`<pkg>/dist/<path>`:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
// the offending snippet, quoted
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Explain why this is wrong. If parts are inferred rather than observed, say so explicitly.
|
|
159
|
+
|
|
160
|
+
## Impact
|
|
161
|
+
|
|
162
|
+
Who and what is affected — one app, every consumer using this component, dev-only, production.
|
|
163
|
+
|
|
164
|
+
## Workaround in place downstream
|
|
165
|
+
|
|
166
|
+
What the consumer project is doing right now to stay unblocked, or "none".
|
|
167
|
+
|
|
168
|
+
## Environment
|
|
169
|
+
|
|
170
|
+
- Framework: …
|
|
171
|
+
- Browser / Node: …
|
|
172
|
+
- Other relevant versions: …
|
|
173
|
+
````
|
|
174
|
+
|
|
175
|
+
Show the draft to the human. Do not file it yet.
|
|
176
|
+
|
|
177
|
+
## Step 5 — File it (after the human says yes)
|
|
178
|
+
|
|
179
|
+
Write the body to a file in the project first — passing markdown inline mangles backticks and
|
|
180
|
+
special characters. Keep the path relative so this works on Windows as well as macOS and Linux, and
|
|
181
|
+
delete it afterwards rather than committing it:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
gh issue create \
|
|
185
|
+
--repo genesislcap/foundation-ui \
|
|
186
|
+
--title "<title>" \
|
|
187
|
+
--body-file temp-upstream-issue.md \
|
|
188
|
+
--label "Bug" \
|
|
189
|
+
--label "<area label>"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Pick area labels from the repo's own list — `gh label list --repo genesislcap/foundation-ui --limit 100`.
|
|
193
|
+
Common ones:
|
|
194
|
+
|
|
195
|
+
| Area | Label |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| grid-pro, data-grid, rapid-grid-pro | `Grids` |
|
|
198
|
+
| foundation-comms, datasources, websockets | `COMMS`, `Datasource` |
|
|
199
|
+
| design system, FAST, rapid components | `Design System`, `Rapid` |
|
|
200
|
+
| foundation-forms / criteria / filters | `Forms`, `Criteria` |
|
|
201
|
+
| foundation-layout, golden/dynamic layout | `Layout` |
|
|
202
|
+
| foundation-entity-management | `Entity Management` |
|
|
203
|
+
| React/Angular/Vue wrappers, typings | `Frontends Compatibility` |
|
|
204
|
+
| build, bundling, genx/CLI | `Build`, `Rollup & Webpack`, `CLI & GENX` |
|
|
205
|
+
| production-blocking | `High Priority`, or `CRITICAL` if the app is down |
|
|
206
|
+
|
|
207
|
+
Then delete the temp file, and report the issue URL back to the human.
|
|
208
|
+
|
|
209
|
+
## Step 6 — Workaround: only after the issue exists, only with approval
|
|
210
|
+
|
|
211
|
+
Filing the issue does not unblock this app. Once the issue URL exists:
|
|
212
|
+
|
|
213
|
+
1. **Present the options**, with the cost of each — e.g. upgrade/downgrade, use a different
|
|
214
|
+
documented API, local subclass/override, wait for the upstream fix, `patch-package`.
|
|
215
|
+
Include "do nothing yet" when it is viable.
|
|
216
|
+
2. **Wait for an explicit choice.** Do not start editing consumer code before that.
|
|
217
|
+
3. **Once approved**, keep it minimal and make it self-deleting on upgrade:
|
|
218
|
+
- Mark every touched site with a marker comment carrying the issue URL and the removal condition:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// UPSTREAM-WORKAROUND(genesislcap/foundation-ui#2442):
|
|
222
|
+
// foundation-comms defers DATA_LOGON via DOM.queueUpdate, so a StrictMode remount drops it.
|
|
223
|
+
// Remove when: @genesislcap/foundation-comms ships the fix (> 15.3.1) and this app upgrades.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- Prefer wrapping over forking: a subclass, a decorator, or an adapter at one boundary beats a
|
|
227
|
+
vendored copy of upstream source scattered across the codebase.
|
|
228
|
+
- Never leave the workaround as the only record of the bug. The marker must name the issue.
|
|
229
|
+
4. **Log it** in a single tracking file the team can grep at upgrade time, shared with the feature
|
|
230
|
+
rule (create it if absent — `docs/upstream-tracking.md`):
|
|
231
|
+
|
|
232
|
+
````markdown
|
|
233
|
+
| Kind | Issue | Package + version | What we do locally | Remove when |
|
|
234
|
+
| --- | --- | --- | --- | --- |
|
|
235
|
+
| bug | genesislcap/foundation-ui#2442 | foundation-comms@15.3.1 | ... | fix released and adopted |
|
|
236
|
+
````
|
|
237
|
+
|
|
238
|
+
## Step 7 — Offer the upstream fix
|
|
239
|
+
|
|
240
|
+
If the fix is small and you can see it, say so in the issue and offer a PR. `foundation-ui` accepts
|
|
241
|
+
contributions from consumer teams — see its
|
|
242
|
+
[CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md) for
|
|
243
|
+
setup, commit format, and the PR template. A pre-release build can be cut from a branch so this app
|
|
244
|
+
can validate the fix before it lands (`@genesislcap/<pkg>@<dist-tag>`); ask in the issue.
|
|
245
|
+
|
|
246
|
+
## Never
|
|
247
|
+
|
|
248
|
+
- Edit `node_modules/` directly, or add a `patch-package` patch, without approval — and never
|
|
249
|
+
without an issue link in the patch's commit message.
|
|
250
|
+
- Present a suspected root cause as confirmed.
|
|
251
|
+
- Paste client-confidential data (hostnames, credentials, real records) into a shared repo.
|
|
252
|
+
- Delete, skip, or weaken a test to make an upstream bug invisible.
|
|
253
|
+
- Ship a workaround with no marker comment and no issue reference.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/commands/agent-rules.ts"],"names":[],"mappings":"yBAyCE,eAAsB,EACtB,UAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO;AAFnD,wBAkEE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const build_kit_1 = require("@genesislcap/build-kit");
|
|
7
|
+
const consola_1 = tslib_1.__importDefault(require("consola"));
|
|
8
|
+
const agent_rules_1 = require("../utils/agent-rules");
|
|
9
|
+
/** The packaged rules live at the root of this package, alongside `bin` and `dist`. */
|
|
10
|
+
const packagedRulesDir = () => (0, node_path_1.resolve)(__dirname, '../../agent-rules');
|
|
11
|
+
const loadManifest = (rulesDir) => JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, 'manifest.json'), 'utf8'));
|
|
12
|
+
const readRuleContents = (rulesDir, manifest) => manifest.rules.reduce((contents, rule) => {
|
|
13
|
+
contents[rule.file] = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, rule.file), 'utf8');
|
|
14
|
+
return contents;
|
|
15
|
+
}, {});
|
|
16
|
+
const resolveTools = (dir, requested) => {
|
|
17
|
+
if (!requested) {
|
|
18
|
+
return (0, agent_rules_1.detectTools)(dir);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return (0, agent_rules_1.parseTools)(requested);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// A typo in --tools is a user error, not a bug: report it without a stack trace.
|
|
25
|
+
consola_1.default.error((0, agent_rules_1.errorMessage)(error));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0, function* (folder = process.cwd(), options = {}) {
|
|
30
|
+
const dir = (0, node_path_1.resolve)(folder);
|
|
31
|
+
const rulesDir = packagedRulesDir();
|
|
32
|
+
const manifest = loadManifest(rulesDir);
|
|
33
|
+
const ruleContents = readRuleContents(rulesDir, manifest);
|
|
34
|
+
const tools = resolveTools(dir, options.tools);
|
|
35
|
+
if (!tools.length && !options.tools) {
|
|
36
|
+
// Writing rule files into a package directory whose assistant files live higher up leaves an
|
|
37
|
+
// orphaned second copy behind, so point at the real root instead of quietly installing here.
|
|
38
|
+
const assistantRoot = (0, agent_rules_1.findAssistantRoot)(dir);
|
|
39
|
+
if (assistantRoot) {
|
|
40
|
+
consola_1.default.error(`No assistant detected in ${dir}, but ${assistantRoot} has one.`);
|
|
41
|
+
consola_1.default.info(`Run \`genx agent-rules ${assistantRoot}\` to install there instead.`);
|
|
42
|
+
consola_1.default.info('Pass --tools to install in this folder anyway.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
consola_1.default.warn(`No assistant detected in ${dir}. The rule files will be installed, but nothing will be wired up. Pass --tools claude,cursor,gemini,agents to wire one explicitly.`);
|
|
46
|
+
}
|
|
47
|
+
let planned;
|
|
48
|
+
try {
|
|
49
|
+
planned = (0, agent_rules_1.planFiles)(dir, manifest, tools, ruleContents, (path) => (0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// A hand-mangled managed block is the user's to repair - we will not guess and overwrite prose.
|
|
53
|
+
consola_1.default.error((0, agent_rules_1.errorMessage)(error));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const drift = (0, agent_rules_1.findDrift)(dir, planned);
|
|
57
|
+
if (options.check) {
|
|
58
|
+
if (drift.length) {
|
|
59
|
+
consola_1.default.error(`Agent rules are out of date in ${dir}:`);
|
|
60
|
+
drift.forEach((file) => consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)} ${(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, file.path)) ? 'differs' : 'missing'}`)));
|
|
61
|
+
consola_1.default.info('Run `genx agent-rules` to update.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
consola_1.default.success(`Agent rules are up to date (${manifest.rules.length} rule(s)).`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!drift.length) {
|
|
68
|
+
consola_1.default.info('Agent rules already up to date. Nothing to do.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
drift.forEach((file) => {
|
|
72
|
+
const path = (0, node_path_1.join)(dir, file.path);
|
|
73
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
74
|
+
(0, node_fs_1.writeFileSync)(path, file.contents, 'utf8');
|
|
75
|
+
consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)}`));
|
|
76
|
+
});
|
|
77
|
+
consola_1.default.success(`Installed ${manifest.rules.length} agent rule(s)${tools.length ? ` for: ${tools.join(', ')}` : ''}.`);
|
|
78
|
+
consola_1.default.info('Commit the changes so the whole team gets them.');
|
|
79
|
+
});
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const commands: {
|
|
2
|
+
agentRules: () => Promise<(folder?: string, options?: {
|
|
3
|
+
tools?: string;
|
|
4
|
+
check?: boolean;
|
|
5
|
+
}) => Promise<void>>;
|
|
2
6
|
clean: () => Promise<(paths?: string[]) => Promise<void>>;
|
|
3
7
|
generateEventTypes: () => Promise<(folder?: string, options?: {
|
|
4
8
|
fromMetadata?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;aAY2qB,CAAC;aAAe,CAAC;;;;oBAVlsB,CAAA;;;;;;;CAOd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC"}
|
package/dist/commands/index.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.commands = void 0;
|
|
37
37
|
// lazy loading command definitions
|
|
38
38
|
exports.commands = {
|
|
39
|
+
agentRules: () => Promise.resolve().then(() => __importStar(require('./agent-rules'))).then((r) => r.default),
|
|
39
40
|
clean: () => Promise.resolve().then(() => __importStar(require('./clean'))).then((r) => r.default),
|
|
40
41
|
generateEventTypes: () => Promise.resolve().then(() => __importStar(require('./generate-event-types'))).then((r) => r.default),
|
|
41
42
|
run: () => Promise.resolve().then(() => __importStar(require('./run'))).then((r) => r.default),
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,23 @@ cli
|
|
|
42
42
|
consola_1.consola.error(`Unknown generate type "${type}". Supported: event-types`);
|
|
43
43
|
process.exit(1);
|
|
44
44
|
}));
|
|
45
|
+
cli
|
|
46
|
+
.command('agent-rules [folder]', 'Install shared AI agent rules into a project')
|
|
47
|
+
.option('-t, --tools <list>', 'Comma-separated assistants to wire: claude, cursor, gemini, agents (defaults to those detected)')
|
|
48
|
+
.option('-c, --check', 'Verify the installed rules are up to date, exit 1 otherwise (for CI)')
|
|
49
|
+
.action((folder, options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
const action = yield commands_1.commands.agentRules();
|
|
51
|
+
yield action(folder, options);
|
|
52
|
+
})).example(`
|
|
53
|
+
${(0, build_kit_1.bold)('genx agent-rules')}
|
|
54
|
+
Install the shared rules into docs/agent-rules and wire up every assistant detected in the project
|
|
55
|
+
|
|
56
|
+
${(0, build_kit_1.bold)('genx agent-rules --tools claude,cursor')}
|
|
57
|
+
Wire up Claude Code and Cursor explicitly
|
|
58
|
+
|
|
59
|
+
${(0, build_kit_1.bold)('genx agent-rules --check')}
|
|
60
|
+
Fail if the installed rules are missing or out of date (use in CI)
|
|
61
|
+
`);
|
|
45
62
|
cli
|
|
46
63
|
.command('clean [...paths]', 'Delete specified paths (defaults to dist folder)')
|
|
47
64
|
.action((paths) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shareable agent rules are markdown files that ship with this package and are installed into a
|
|
3
|
+
* consumer project by `genx agent-rules`. The rule body is tool-agnostic; per-assistant wiring is
|
|
4
|
+
* generated from the manifest below.
|
|
5
|
+
*/
|
|
6
|
+
export interface AgentRule {
|
|
7
|
+
/** File name of the rule markdown, relative to the packaged `agent-rules` folder. */
|
|
8
|
+
file: string;
|
|
9
|
+
/** Slug used for generated file names, e.g. the Cursor `.mdc`. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Human readable title, used in logs and generated pointers. */
|
|
12
|
+
title: string;
|
|
13
|
+
/** When the rule applies. Used as the Claude skill / pointer description. */
|
|
14
|
+
description: string;
|
|
15
|
+
/** Cursor's trigger phrasing for the `.mdc` front matter. */
|
|
16
|
+
cursorDescription: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AgentRulesManifest {
|
|
19
|
+
version: number;
|
|
20
|
+
/** Where the rule markdown is installed inside the consumer project. */
|
|
21
|
+
targetDir: string;
|
|
22
|
+
rules: AgentRule[];
|
|
23
|
+
}
|
|
24
|
+
export declare const AGENT_TOOLS: readonly ["claude", "cursor", "gemini", "agents"];
|
|
25
|
+
export type AgentTool = (typeof AGENT_TOOLS)[number];
|
|
26
|
+
/** Entry file each assistant reads, for the tools wired via a managed block. */
|
|
27
|
+
export declare const TOOL_ENTRY_FILES: Record<Exclude<AgentTool, 'cursor'>, string>;
|
|
28
|
+
export declare const MANAGED_BEGIN = "<!-- BEGIN genx agent-rules -->";
|
|
29
|
+
export declare const MANAGED_END = "<!-- END genx agent-rules -->";
|
|
30
|
+
export interface PlannedFile {
|
|
31
|
+
/** Path relative to the consumer project root. */
|
|
32
|
+
path: string;
|
|
33
|
+
contents: string;
|
|
34
|
+
}
|
|
35
|
+
export type ExistsFn = (path: string) => boolean;
|
|
36
|
+
export type ReadFn = (path: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* Narrowed message for a caught value. `catch` bindings are only typed loosely because this
|
|
39
|
+
* package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
|
|
40
|
+
*/
|
|
41
|
+
export declare const errorMessage: (error: unknown) => string;
|
|
42
|
+
/**
|
|
43
|
+
* Replaces the genx-managed block in `content`, or appends one when absent. Everything outside the
|
|
44
|
+
* markers is left alone — these files belong to the consumer project, not to us.
|
|
45
|
+
*
|
|
46
|
+
* Throws when the markers are malformed (an unpaired marker, `END` before `BEGIN`, or more than one
|
|
47
|
+
* pair). Every way of guessing what the author meant risks eating their prose: appending a second
|
|
48
|
+
* block leaves the file to be truncated by the *next* run, and treating an unpaired `BEGIN` as
|
|
49
|
+
* running to end-of-file discards whatever they wrote below it. Refusing to write is the only
|
|
50
|
+
* option that cannot lose someone's instructions.
|
|
51
|
+
*/
|
|
52
|
+
export declare const upsertManagedBlock: (content: string, body: string) => string;
|
|
53
|
+
/** Import line understood by Claude Code (`CLAUDE.md`) and Gemini CLI (`GEMINI.md`). */
|
|
54
|
+
export declare const renderImportBlock: (rules: AgentRule[], targetDir: string) => string;
|
|
55
|
+
/** Plain pointer for assistants without an import syntax (AGENTS.md, Copilot, Windsurf, …). */
|
|
56
|
+
export declare const renderPointerBlock: (rules: AgentRule[], targetDir: string) => string;
|
|
57
|
+
/** A fully generated Cursor rule that defers to the shared markdown. */
|
|
58
|
+
export declare const renderCursorRule: (rule: AgentRule, targetDir: string) => string;
|
|
59
|
+
/**
|
|
60
|
+
* Which assistants a project already uses. Wiring an assistant nobody uses is just litter, so we
|
|
61
|
+
* only touch what we can see evidence of.
|
|
62
|
+
*/
|
|
63
|
+
export declare const detectTools: (dir: string, exists?: ExistsFn) => AgentTool[];
|
|
64
|
+
/**
|
|
65
|
+
* The nearest ancestor of `dir` that looks like the project's assistant root, or `null`.
|
|
66
|
+
*
|
|
67
|
+
* Genesis apps keep `package.json` in `client/` while `CLAUDE.md` and friends sit at the repo
|
|
68
|
+
* root, so running the installer from the package directory would wire up nothing and leave a
|
|
69
|
+
* second, orphaned set of rule files behind. Walk up as far as the repo root looking for the
|
|
70
|
+
* directory the assistants actually read.
|
|
71
|
+
*/
|
|
72
|
+
export declare const findAssistantRoot: (dir: string, exists?: ExistsFn) => string | null;
|
|
73
|
+
/** Parses and validates a `--tools` value. Throws on anything unknown rather than guessing. */
|
|
74
|
+
export declare const parseTools: (value: string) => AgentTool[];
|
|
75
|
+
/**
|
|
76
|
+
* Everything the command intends to write, as (path, contents) pairs. Kept separate from the
|
|
77
|
+
* writing itself so `--check` can compare against disk without touching it.
|
|
78
|
+
*/
|
|
79
|
+
export declare const planFiles: (dir: string, manifest: AgentRulesManifest, tools: AgentTool[], ruleContents: Record<string, string>, read: ReadFn, exists?: ExistsFn) => PlannedFile[];
|
|
80
|
+
/** Planned files whose contents differ from what is on disk. */
|
|
81
|
+
export declare const findDrift: (dir: string, planned: PlannedFile[], read?: ReadFn, exists?: ExistsFn) => PlannedFile[];
|
|
82
|
+
//# sourceMappingURL=agent-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/utils/agent-rules.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,mDAAoD,CAAC;AAC7E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAIzE,CAAC;AAEF,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAC/D,eAAO,MAAM,WAAW,kCAAkC,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AACjD,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAK9C;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MACU,CAAC;AAEzD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,MAAM,MAAM,KAAG,MAsBlE,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,iBAAiB,GAAI,OAAO,SAAS,EAAE,EAAE,WAAW,MAAM,KAAG,MAI5D,CAAC;AAEf,+FAA+F;AAC/F,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,EAAE,EAAE,WAAW,MAAM,KAAG,MAI7D,CAAC;AAUf,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,EAAE,WAAW,MAAM,KAAG,MAarE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,SAAQ,QAAqB,KAAG,SAAS,EAQjF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,SAAQ,QAAqB,KAAG,MAAM,GAAG,IAiBvF,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,SAAS,EAYnD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,UAAU,kBAAkB,EAC5B,OAAO,SAAS,EAAE,EAClB,cAAc,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,MAAM,EACZ,SAAQ,QAAqB,KAC5B,WAAW,EAiCb,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,SAAS,WAAW,EAAE,EACtB,OAAM,MAA6C,EACnD,SAAQ,QAAqB,KAC5B,WAAW,EAIV,CAAC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findDrift = exports.planFiles = exports.parseTools = exports.findAssistantRoot = exports.detectTools = exports.renderCursorRule = exports.renderPointerBlock = exports.renderImportBlock = exports.upsertManagedBlock = exports.errorMessage = exports.MANAGED_END = exports.MANAGED_BEGIN = exports.TOOL_ENTRY_FILES = exports.AGENT_TOOLS = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
exports.AGENT_TOOLS = ['claude', 'cursor', 'gemini', 'agents'];
|
|
7
|
+
/** Entry file each assistant reads, for the tools wired via a managed block. */
|
|
8
|
+
exports.TOOL_ENTRY_FILES = {
|
|
9
|
+
claude: 'CLAUDE.md',
|
|
10
|
+
gemini: 'GEMINI.md',
|
|
11
|
+
agents: 'AGENTS.md',
|
|
12
|
+
};
|
|
13
|
+
exports.MANAGED_BEGIN = '<!-- BEGIN genx agent-rules -->';
|
|
14
|
+
exports.MANAGED_END = '<!-- END genx agent-rules -->';
|
|
15
|
+
const countOccurrences = (haystack, needle) => haystack.split(needle).length - 1;
|
|
16
|
+
/**
|
|
17
|
+
* Narrowed message for a caught value. `catch` bindings are only typed loosely because this
|
|
18
|
+
* package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
|
|
19
|
+
*/
|
|
20
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
21
|
+
exports.errorMessage = errorMessage;
|
|
22
|
+
/**
|
|
23
|
+
* Replaces the genx-managed block in `content`, or appends one when absent. Everything outside the
|
|
24
|
+
* markers is left alone — these files belong to the consumer project, not to us.
|
|
25
|
+
*
|
|
26
|
+
* Throws when the markers are malformed (an unpaired marker, `END` before `BEGIN`, or more than one
|
|
27
|
+
* pair). Every way of guessing what the author meant risks eating their prose: appending a second
|
|
28
|
+
* block leaves the file to be truncated by the *next* run, and treating an unpaired `BEGIN` as
|
|
29
|
+
* running to end-of-file discards whatever they wrote below it. Refusing to write is the only
|
|
30
|
+
* option that cannot lose someone's instructions.
|
|
31
|
+
*/
|
|
32
|
+
const upsertManagedBlock = (content, body) => {
|
|
33
|
+
const block = `${exports.MANAGED_BEGIN}\n${body.trim()}\n${exports.MANAGED_END}`;
|
|
34
|
+
const begins = countOccurrences(content, exports.MANAGED_BEGIN);
|
|
35
|
+
const ends = countOccurrences(content, exports.MANAGED_END);
|
|
36
|
+
const begin = content.indexOf(exports.MANAGED_BEGIN);
|
|
37
|
+
const end = content.indexOf(exports.MANAGED_END);
|
|
38
|
+
if (begins === 1 && ends === 1 && end > begin) {
|
|
39
|
+
const before = content.slice(0, begin);
|
|
40
|
+
const after = content.slice(end + exports.MANAGED_END.length);
|
|
41
|
+
return `${before}${block}${after}`;
|
|
42
|
+
}
|
|
43
|
+
if (begins === 0 && ends === 0) {
|
|
44
|
+
return content.trim() ? `${content.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n`;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`the genx agent-rules block is malformed: found ${begins} "${exports.MANAGED_BEGIN}" and ${ends} ` +
|
|
47
|
+
`"${exports.MANAGED_END}" marker(s), expected one matching pair or none. Restore the pair or delete ` +
|
|
48
|
+
`the block by hand, then run again — refusing to guess, so nothing you wrote is lost.`);
|
|
49
|
+
};
|
|
50
|
+
exports.upsertManagedBlock = upsertManagedBlock;
|
|
51
|
+
/** Import line understood by Claude Code (`CLAUDE.md`) and Gemini CLI (`GEMINI.md`). */
|
|
52
|
+
const renderImportBlock = (rules, targetDir) => [
|
|
53
|
+
'<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
|
|
54
|
+
...rules.map((rule) => `@${targetDir}/${rule.file}`),
|
|
55
|
+
].join('\n');
|
|
56
|
+
exports.renderImportBlock = renderImportBlock;
|
|
57
|
+
/** Plain pointer for assistants without an import syntax (AGENTS.md, Copilot, Windsurf, …). */
|
|
58
|
+
const renderPointerBlock = (rules, targetDir) => [
|
|
59
|
+
'<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
|
|
60
|
+
...rules.map((rule) => `- ${rule.description} Read and follow \`${targetDir}/${rule.file}\`.`),
|
|
61
|
+
].join('\n');
|
|
62
|
+
exports.renderPointerBlock = renderPointerBlock;
|
|
63
|
+
/**
|
|
64
|
+
* YAML double-quoted scalar for a rule description. A colon, a quote, or a leading `-` in the
|
|
65
|
+
* manifest would otherwise produce front matter Cursor cannot parse — and the rule would then be
|
|
66
|
+
* silently inert rather than visibly broken.
|
|
67
|
+
*/
|
|
68
|
+
const yamlString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
69
|
+
/** A fully generated Cursor rule that defers to the shared markdown. */
|
|
70
|
+
const renderCursorRule = (rule, targetDir) => `---
|
|
71
|
+
description: ${yamlString(rule.cursorDescription)}
|
|
72
|
+
alwaysApply: false
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
<!-- Installed by \`genx agent-rules\`. Re-run it to update; local edits are lost. -->
|
|
76
|
+
|
|
77
|
+
# ${rule.title}
|
|
78
|
+
|
|
79
|
+
@${targetDir}/${rule.file}
|
|
80
|
+
|
|
81
|
+
If this reference does not resolve, open \`${targetDir}/${rule.file}\` and follow it directly.
|
|
82
|
+
`;
|
|
83
|
+
exports.renderCursorRule = renderCursorRule;
|
|
84
|
+
/**
|
|
85
|
+
* Which assistants a project already uses. Wiring an assistant nobody uses is just litter, so we
|
|
86
|
+
* only touch what we can see evidence of.
|
|
87
|
+
*/
|
|
88
|
+
const detectTools = (dir, exists = node_fs_1.existsSync) => {
|
|
89
|
+
const markers = {
|
|
90
|
+
claude: ['CLAUDE.md', '.claude'],
|
|
91
|
+
cursor: ['.cursor', '.cursorrules'],
|
|
92
|
+
gemini: ['GEMINI.md', '.gemini'],
|
|
93
|
+
agents: ['AGENTS.md'],
|
|
94
|
+
};
|
|
95
|
+
return exports.AGENT_TOOLS.filter((tool) => markers[tool].some((marker) => exists((0, node_path_1.join)(dir, marker))));
|
|
96
|
+
};
|
|
97
|
+
exports.detectTools = detectTools;
|
|
98
|
+
/**
|
|
99
|
+
* The nearest ancestor of `dir` that looks like the project's assistant root, or `null`.
|
|
100
|
+
*
|
|
101
|
+
* Genesis apps keep `package.json` in `client/` while `CLAUDE.md` and friends sit at the repo
|
|
102
|
+
* root, so running the installer from the package directory would wire up nothing and leave a
|
|
103
|
+
* second, orphaned set of rule files behind. Walk up as far as the repo root looking for the
|
|
104
|
+
* directory the assistants actually read.
|
|
105
|
+
*/
|
|
106
|
+
const findAssistantRoot = (dir, exists = node_fs_1.existsSync) => {
|
|
107
|
+
let current = (0, node_path_1.dirname)(dir);
|
|
108
|
+
for (;;) {
|
|
109
|
+
if ((0, exports.detectTools)(current, exists).length) {
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
// The repo root is as far as we go - above it is someone else's project.
|
|
113
|
+
if (exists((0, node_path_1.join)(current, '.git'))) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const parent = (0, node_path_1.dirname)(current);
|
|
117
|
+
if (parent === current) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
current = parent;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
exports.findAssistantRoot = findAssistantRoot;
|
|
124
|
+
/** Parses and validates a `--tools` value. Throws on anything unknown rather than guessing. */
|
|
125
|
+
const parseTools = (value) => {
|
|
126
|
+
const requested = value
|
|
127
|
+
.split(',')
|
|
128
|
+
.map((tool) => tool.trim().toLowerCase())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
const unknown = requested.filter((tool) => !exports.AGENT_TOOLS.includes(tool));
|
|
131
|
+
if (unknown.length) {
|
|
132
|
+
throw new Error(`Unknown agent tool(s): ${unknown.join(', ')}. Supported: ${exports.AGENT_TOOLS.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
return exports.AGENT_TOOLS.filter((tool) => requested.includes(tool));
|
|
135
|
+
};
|
|
136
|
+
exports.parseTools = parseTools;
|
|
137
|
+
/**
|
|
138
|
+
* Everything the command intends to write, as (path, contents) pairs. Kept separate from the
|
|
139
|
+
* writing itself so `--check` can compare against disk without touching it.
|
|
140
|
+
*/
|
|
141
|
+
const planFiles = (dir, manifest, tools, ruleContents, read, exists = node_fs_1.existsSync) => {
|
|
142
|
+
const { targetDir, rules } = manifest;
|
|
143
|
+
const planned = rules.map((rule) => ({
|
|
144
|
+
path: `${targetDir}/${rule.file}`,
|
|
145
|
+
contents: ruleContents[rule.file],
|
|
146
|
+
}));
|
|
147
|
+
tools.forEach((tool) => {
|
|
148
|
+
if (tool === 'cursor') {
|
|
149
|
+
rules.forEach((rule) => {
|
|
150
|
+
planned.push({
|
|
151
|
+
path: `.cursor/rules/${rule.name}.mdc`,
|
|
152
|
+
contents: (0, exports.renderCursorRule)(rule, targetDir),
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const entryFile = exports.TOOL_ENTRY_FILES[tool];
|
|
158
|
+
const entryPath = (0, node_path_1.join)(dir, entryFile);
|
|
159
|
+
const current = exists(entryPath) ? read(entryPath) : '';
|
|
160
|
+
const body = tool === 'agents'
|
|
161
|
+
? (0, exports.renderPointerBlock)(rules, targetDir)
|
|
162
|
+
: (0, exports.renderImportBlock)(rules, targetDir);
|
|
163
|
+
try {
|
|
164
|
+
planned.push({ path: entryFile, contents: (0, exports.upsertManagedBlock)(current, body) });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new Error(`${entryFile}: ${(0, exports.errorMessage)(error)}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return planned;
|
|
171
|
+
};
|
|
172
|
+
exports.planFiles = planFiles;
|
|
173
|
+
/** Planned files whose contents differ from what is on disk. */
|
|
174
|
+
const findDrift = (dir, planned, read = (path) => (0, node_fs_1.readFileSync)(path, 'utf8'), exists = node_fs_1.existsSync) => planned.filter((file) => {
|
|
175
|
+
const path = (0, node_path_1.join)(dir, file.path);
|
|
176
|
+
return !exists(path) || read(path) !== file.contents;
|
|
177
|
+
});
|
|
178
|
+
exports.findDrift = findDrift;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/genx",
|
|
3
3
|
"description": "Genx CLI",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.24.0-FUI-2603.10",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"build": "npm run clean && tsc -b ./tsconfig.json",
|
|
11
11
|
"clean": "rimraf dist temp tsconfig.tsbuildinfo",
|
|
12
12
|
"dev": "tsc -b ./tsconfig.json -w",
|
|
13
|
+
"test": "genx test",
|
|
13
14
|
"lint": "genx lint -l ox",
|
|
14
15
|
"lint:fix": "genx lint -l ox --fix"
|
|
15
16
|
},
|
|
@@ -17,18 +18,21 @@
|
|
|
17
18
|
"genx": "./bin/genx"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@genesislcap/build-kit": "15.
|
|
21
|
-
"@genesislcap/eslint-stylelint-builder": "15.
|
|
22
|
-
"@genesislcap/event-type-codegen": "15.
|
|
23
|
-
"@genesislcap/rollup-builder": "15.
|
|
24
|
-
"@genesislcap/ts-builder": "15.
|
|
25
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
26
|
-
"@genesislcap/vite-builder": "15.
|
|
27
|
-
"@genesislcap/webpack-builder": "15.
|
|
21
|
+
"@genesislcap/build-kit": "15.24.0-FUI-2603.10",
|
|
22
|
+
"@genesislcap/eslint-stylelint-builder": "15.24.0-FUI-2603.10",
|
|
23
|
+
"@genesislcap/event-type-codegen": "15.24.0-FUI-2603.10",
|
|
24
|
+
"@genesislcap/rollup-builder": "15.24.0-FUI-2603.10",
|
|
25
|
+
"@genesislcap/ts-builder": "15.24.0-FUI-2603.10",
|
|
26
|
+
"@genesislcap/uvu-playwright-builder": "15.24.0-FUI-2603.10",
|
|
27
|
+
"@genesislcap/vite-builder": "15.24.0-FUI-2603.10",
|
|
28
|
+
"@genesislcap/webpack-builder": "15.24.0-FUI-2603.10",
|
|
28
29
|
"cac": "^6.7.14",
|
|
29
30
|
"consola": "^3.0.2",
|
|
30
31
|
"serve-handler": "^6.1.5"
|
|
31
32
|
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@genesislcap/foundation-testing": "15.24.0-FUI-2603.10"
|
|
35
|
+
},
|
|
32
36
|
"repository": {
|
|
33
37
|
"type": "git",
|
|
34
38
|
"url": "git+https://github.com/genesislcap/foundation-ui.git",
|