@metaobjectsdev/sdk 0.21.5 → 0.22.0-rc.1
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/agent-context/README.md +1 -1
- package/agent-context/skills/metaobjects-audit/references/requirements.md +60 -0
- package/agent-context/skills/metaobjects-authoring/SKILL.md +16 -2
- package/agent-context/skills/metaobjects-authoring/references/requirements.md +69 -0
- package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +29 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +7 -0
- package/agent-context/skills/metaobjects-verify/references/migration.md +5 -0
- package/agent-context/skills/metaobjects-verify/references/requirements.md +54 -0
- package/dist/agent-context/assemble.d.ts.map +1 -1
- package/dist/agent-context/assemble.js +13 -10
- package/dist/agent-context/assemble.js.map +1 -1
- package/dist/agent-context/resolve.d.ts +8 -2
- package/dist/agent-context/resolve.d.ts.map +1 -1
- package/dist/agent-context/resolve.js +13 -3
- package/dist/agent-context/resolve.js.map +1 -1
- package/dist/agent-context/types.d.ts +10 -1
- package/dist/agent-context/types.d.ts.map +1 -1
- package/dist/agent-context/types.js +7 -0
- package/dist/agent-context/types.js.map +1 -1
- package/dist/agent-docs/body.d.ts +1 -1
- package/dist/agent-docs/body.d.ts.map +1 -1
- package/dist/agent-docs/body.js +4 -2
- package/dist/agent-docs/body.js.map +1 -1
- package/package.json +2 -2
- package/scripts/regen-agent-context-conformance.ts +2 -1
- package/src/agent-context/assemble.ts +15 -10
- package/src/agent-context/resolve.ts +20 -4
- package/src/agent-context/types.ts +11 -1
- package/src/agent-docs/body.ts +4 -2
package/agent-context/README.md
CHANGED
|
@@ -9,7 +9,7 @@ the slim always-on Markdown (`.metaobjects/AGENTS.md` + `CLAUDE.md`) and the six
|
|
|
9
9
|
- `servers/<lang>.meta.json` — per-server install + codegen command (drives the always-on).
|
|
10
10
|
- `templates/always-on.md.mustache` — the slim always-on body (`{{stackLine}}`, `{{codegenCommand}}`).
|
|
11
11
|
- `skills/<skill>/SKILL.md` — universal skill body.
|
|
12
|
-
- `skills/<skill>/references/<token>.md` — language fragment; installed iff `<token>` is in the stack.
|
|
12
|
+
- `skills/<skill>/references/<token>.md` — language/framework or concern fragment; installed iff `<token>` is in the stack (`Stack.tokens`; concern tokens are OBSERVED from project state, e.g. `requirements` for a project declaring `requirement.*` nodes — see `SDK`'s `CONCERN_TOKENS`).
|
|
13
13
|
|
|
14
14
|
The **scaffolded** set is exactly the six skills in the SDK's `SKILL_NAMES`
|
|
15
15
|
(`src/agent-context/types.ts`) — the assembler emits only those. `skills/`
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Requirements — the audit dimension
|
|
2
|
+
|
|
3
|
+
This project declares `requirement.*` nodes, so the ledger is an auditable surface. Audit it
|
|
4
|
+
for **truthfulness**, not volume. A large ledger that lies is worse than a small one that
|
|
5
|
+
does not, because every later reader trusts it.
|
|
6
|
+
|
|
7
|
+
Run `meta verify` first. It settles referential integrity mechanically — do not spend audit
|
|
8
|
+
effort re-deriving what a green run already proves.
|
|
9
|
+
|
|
10
|
+
## What verify has already proven (do not re-check by hand)
|
|
11
|
+
|
|
12
|
+
Links sit at or below the L4 floor, nesting agrees with levels, `@status` values are legal,
|
|
13
|
+
references resolve (with dangling allowed on `abandoned`/`superseded`), and named
|
|
14
|
+
`@verifiedBy` tests exist and are not skipped.
|
|
15
|
+
|
|
16
|
+
## What only a human or an agent reading the code can catch
|
|
17
|
+
|
|
18
|
+
**1. Statuses that are false.** The highest-value finding in the whole dimension. A
|
|
19
|
+
requirement marked `live` whose implementation was gutted; one marked `partial` that is now
|
|
20
|
+
complete; one marked `abandoned` whose code is demonstrably still running. Sample the claims
|
|
21
|
+
and read the nodes. `status` is the only payload with controlled evidence behind it — the
|
|
22
|
+
resurrection protection rides entirely on it being true.
|
|
23
|
+
|
|
24
|
+
**2. Non-violable statements.** Every requirement must state what breaking it looks like.
|
|
25
|
+
*"Every entity has a uuid primary key"* is violable — point at one with a composite key.
|
|
26
|
+
*"Things are persisted"* is not; it is a description wearing a requirement's shape. Flag
|
|
27
|
+
these: they inflate the ledger while proving nothing.
|
|
28
|
+
|
|
29
|
+
**3. Claim-padding.** An entity appended to an existing unrelated requirement's
|
|
30
|
+
`implementedBy` purely to silence the coverage warning. The claim resolves, so verify is
|
|
31
|
+
happy, but it carries no information about what the entity is *for*. Look for entities whose
|
|
32
|
+
only claim is a high-fan-out architectural rule when they plainly have product meaning.
|
|
33
|
+
|
|
34
|
+
**4. Misfiled kind.** `functional` is checked by EXISTENCE, `architectural` by UNIVERSALITY —
|
|
35
|
+
opposite polarity, and nothing mechanical catches a misfile. The discriminator: did this
|
|
36
|
+
exist because someone asked for something (functional), or because every entity here looks
|
|
37
|
+
like this (architectural)? An architectural entry with one claimant is usually a misfiled
|
|
38
|
+
functional one.
|
|
39
|
+
|
|
40
|
+
**5. Levels used as decoration.** L1–L3 are organisational and must never reference the
|
|
41
|
+
model. If the tree is flat, or every entry is L3, the levels are carrying no information and
|
|
42
|
+
should be simplified rather than defended.
|
|
43
|
+
|
|
44
|
+
## Scope — do NOT flag these as defects
|
|
45
|
+
|
|
46
|
+
- **Unclaimed `object.value` / `object.projection`.** Exempt by design: a value is a shape, a
|
|
47
|
+
projection derives from a claimable entity.
|
|
48
|
+
- **Fields, views, validators and identities without their own requirement.** Member-grain
|
|
49
|
+
coverage is explicitly rejected — plumbing members are covered by architectural
|
|
50
|
+
requirements with high fan-out. L5 exists so a member claim *can* be made where it means
|
|
51
|
+
something, never so every member must carry one.
|
|
52
|
+
- **Dangling links on `abandoned` / `superseded`.** Correct, and load-bearing.
|
|
53
|
+
- **A project with no requirements at all.** The feature is opt-in by declaration; absence is
|
|
54
|
+
not a finding.
|
|
55
|
+
|
|
56
|
+
## Reporting
|
|
57
|
+
|
|
58
|
+
Report findings as claims to verify, not as a score. "These 4 requirements are marked `live`
|
|
59
|
+
and I could not find their implementation" is actionable; "ledger maturity: 62%" is not, and
|
|
60
|
+
invites optimising the number.
|
|
@@ -548,8 +548,13 @@ The `[]` key-suffix declares an array field: `field.long[]: weekIds` lowers to
|
|
|
548
548
|
canonical JSON. `@enforce` on a reference (default `true`) controls whether the
|
|
549
549
|
backend physically enforces it (a SQL FK constraint); set `false` for a logical
|
|
550
550
|
reference for navigation/typing/codegen only. Referential actions
|
|
551
|
-
(`@onDelete`/`@onUpdate`)
|
|
552
|
-
|
|
551
|
+
(`@onDelete`/`@onUpdate`) normally live on the `relationship.*` node (see
|
|
552
|
+
Relationships below — the subtype carries the default), but `identity.reference`
|
|
553
|
+
also registers them as the **explicit per-FK override** (ADR-0047): use them for
|
|
554
|
+
a reference-only FK with no relationship, an M:N junction's FK sides (no
|
|
555
|
+
relationship ever correlates with a junction FK), or a single FK that must
|
|
556
|
+
deviate from its relationship's action. A reference-level action always wins
|
|
557
|
+
over the correlated relationship's.
|
|
553
558
|
|
|
554
559
|
`@references` resolves cross-package by **fully-qualified name**
|
|
555
560
|
(`@references: "shared::billing::Account"`), the same rule as `extends`; a bare
|
|
@@ -658,6 +663,11 @@ The `PostTag` junction supplies the FK direction via its two references:
|
|
|
658
663
|
] } }
|
|
659
664
|
```
|
|
660
665
|
|
|
666
|
+
A junction's FK actions are declared on its `identity.reference` children
|
|
667
|
+
directly (`"@onDelete": "cascade"` on `postRef`/`tagRef` above) — the M:N
|
|
668
|
+
relationship's `@objectRef` names the far side, never the junction, so no
|
|
669
|
+
relationship ever correlates with a junction FK (ADR-0047).
|
|
670
|
+
|
|
661
671
|
**Adoption footgun — pin BOTH actions.** `@onDelete` defaults *per subtype* (above)
|
|
662
672
|
and `@onUpdate` defaults to `cascade` — but a plain SQL foreign key is `NO ACTION` on
|
|
663
673
|
both. If you're adopting an existing database (matching metadata to a live schema),
|
|
@@ -964,6 +974,10 @@ mapping).
|
|
|
964
974
|
- field.int: { name: quantity, required: true }
|
|
965
975
|
```
|
|
966
976
|
|
|
977
|
+
## Requirements — capability ledger (opt-in)
|
|
978
|
+
|
|
979
|
+
If this project declares `requirement.functional` / `requirement.architectural` nodes, read `references/requirements.md` for the full authoring rules.
|
|
980
|
+
|
|
967
981
|
---
|
|
968
982
|
|
|
969
983
|
For non-trivial schema design, use `/superpowers:brainstorming` if installed;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Requirements — `requirement.functional` / `requirement.architectural`
|
|
2
|
+
|
|
3
|
+
Capabilities are **metadata**, declared in `metaobjects/` beside the entities they
|
|
4
|
+
describe. Read the existing requirement nodes before designing anything. Two rules matter
|
|
5
|
+
more than the rest.
|
|
6
|
+
|
|
7
|
+
**1. When you retire something, record it — at that moment.** Set the requirement's
|
|
8
|
+
`status` to `abandoned` (built, then deliberately dropped) or `superseded` (something else
|
|
9
|
+
does it now — name it in `supersededBy`), in the same change that removes the code.
|
|
10
|
+
|
|
11
|
+
This is the one thing a requirement does that the rest of the model cannot. Given a brief
|
|
12
|
+
matching a retired feature, agents reading only the model proposed **reviving** it 24 times
|
|
13
|
+
out of 24, each believing it was reusing. A retired feature is *more* attractive than a
|
|
14
|
+
live one: purpose-built for exactly the request, never complicated by production.
|
|
15
|
+
|
|
16
|
+
Leaving a dangling `implementedBy` on an `abandoned` or `superseded` requirement is
|
|
17
|
+
**correct** — those nodes are supposed to be gone, and `verify` allows it deliberately. On
|
|
18
|
+
`live` or `partial` the same dangling reference is an error: the model moved and the
|
|
19
|
+
requirement went stale.
|
|
20
|
+
|
|
21
|
+
**2. When you add an entity, claim it.** Every `object.entity` should appear in some
|
|
22
|
+
requirement's `implementedBy`, or `verify` says so.
|
|
23
|
+
|
|
24
|
+
**Every requirement states its violation.** One sentence: what breaking it looks like.
|
|
25
|
+
*"Every entity has a uuid primary key"* is violable — point at one with a composite string
|
|
26
|
+
key. *"Things are persisted"* is not, and is a description rather than a requirement. Same
|
|
27
|
+
rule kills *"the system is reliable"*. If you cannot say what breaking it looks like,
|
|
28
|
+
delete it.
|
|
29
|
+
|
|
30
|
+
**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment (an
|
|
31
|
+
application or library), L3 service — these never reference the model. **L4** binds an
|
|
32
|
+
object, **L5** binds a field, view or identity. `implementedBy` above L4 is an error.
|
|
33
|
+
Regrouping *moves* a node; it does not edit a parent string.
|
|
34
|
+
|
|
35
|
+
**Architectural requirements are the other kind.** `requirement.architectural` carries no
|
|
36
|
+
level — a uuid-PK rule, change attribution, tenant scoping. Its check is *universality*
|
|
37
|
+
rather than existence, so one that is `live` and claimed by nothing fails: a policy
|
|
38
|
+
declared and applied to nothing.
|
|
39
|
+
|
|
40
|
+
`@status` is a closed enum enforced by the loader, so a typo fails the load rather than
|
|
41
|
+
silently disabling the entry.
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
- requirement.functional:
|
|
45
|
+
name: Pacing
|
|
46
|
+
level: 3
|
|
47
|
+
status: live
|
|
48
|
+
statement: "Scene pacing follows story beats"
|
|
49
|
+
violation: "A scene that advances on a clock rather than on the story"
|
|
50
|
+
children:
|
|
51
|
+
- requirement.functional:
|
|
52
|
+
name: TurnTimer
|
|
53
|
+
level: 4
|
|
54
|
+
status: abandoned # retired deliberately -- do NOT revive
|
|
55
|
+
statement: "Pacing was driven by a per-turn wall-clock timer"
|
|
56
|
+
violation: "Pacing driven by elapsed time instead of beat completion"
|
|
57
|
+
supersededBy: BeatProgression
|
|
58
|
+
implementedBy: ["game::turn::TurnTimer"] # gone, and that is the point
|
|
59
|
+
|
|
60
|
+
- requirement.architectural:
|
|
61
|
+
name: UuidPrimaryKeys
|
|
62
|
+
status: live
|
|
63
|
+
statement: "Every entity has a uuid primary key"
|
|
64
|
+
violation: "An entity keyed by a composite string"
|
|
65
|
+
implementedBy: ["game::turn::Turn", "game::world::Location"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
There is **no `satisfies:` on a field or entity** — links live on the requirement node, not
|
|
69
|
+
on the nodes it claims. Full reference: the repo's `spec/capability-ledger.md`.
|
|
@@ -13,6 +13,35 @@ truth. Its sibling is `metaobjects-audit` (POST-adoption; scaffolded). Design +
|
|
|
13
13
|
retro-test validation: `docs/superpowers/specs/2026-07-12-metaobjects-fit-assessment-design.md`.
|
|
14
14
|
How to run it: point a high-end LLM at a target repo + this file (e.g. paste it, or
|
|
15
15
|
fetch it from GitHub / metaobjects.dev) and let it produce the report below.
|
|
16
|
+
|
|
17
|
+
DEFERRED, ON PURPOSE — capability requirements (`requirement.functional` /
|
|
18
|
+
`requirement.architectural`). This skill deliberately says NOTHING about them, and that is a
|
|
19
|
+
decision, not an omission.
|
|
20
|
+
|
|
21
|
+
Why not now: (1) the grounding rule above requires every capability claim be verifiable
|
|
22
|
+
against the CURRENT RELEASE, and `requirement.*` is not in one yet. (2) The signal->feature
|
|
23
|
+
mapping is unvalidated against this skill's own kill criterion: the controlled evidence
|
|
24
|
+
(0/24 model-only revivals) measures POST-adoption model-reading, and the feature's premise --
|
|
25
|
+
that the disproof lives nowhere in the model -- cuts against pre-adoption detectability. If
|
|
26
|
+
the reasoning was never written down, there is nothing for an evidence-cited assessment to
|
|
27
|
+
cite. (3) Whether anyone fills the ledger in is itself untested; advertising it pre-adoption
|
|
28
|
+
is the brochure failure this skill exists to avoid.
|
|
29
|
+
|
|
30
|
+
TRIGGER to revisit — the release carrying `requirement.*` has shipped, AND either:
|
|
31
|
+
Arm A a dogfooded project's `abandoned`/`superseded` entries are shown, retrospectively,
|
|
32
|
+
to have been discoverable PRE-adoption from repo evidence at file:line standard
|
|
33
|
+
(removal commit, dead flag, do-not-reintroduce comment);
|
|
34
|
+
Arm B a team that adopted via this assessment hits a resurrection the ledger would have
|
|
35
|
+
caught, or asks why the assessment never mentioned it.
|
|
36
|
+
|
|
37
|
+
ANTI-TRIGGER (defer -> never): if dogfooded entries trace only to tribal knowledge with no
|
|
38
|
+
repo artifact, this assessment structurally cannot speak to the feature as a finding.
|
|
39
|
+
|
|
40
|
+
SHAPE, pre-committed so it is not re-litigated as new machinery: ONE P7 hunt line in the
|
|
41
|
+
existing grammar (observable signal -> vocabulary + metadata_sketch), gated like every other
|
|
42
|
+
P7 line by "if a hunt line has hits, it must appear" -- so a repo with no retirement scar
|
|
43
|
+
tissue produces zero requirements content. NOT a fifth pillar, NOT a P4 rubric row, NOT an
|
|
44
|
+
R0 verdict line: no verdict may turn on it.
|
|
16
45
|
-->
|
|
17
46
|
|
|
18
47
|
# MetaObjects Fit & Migration Assessment
|
|
@@ -66,6 +66,13 @@ paired with the advisory anti-pattern pass above, never all three subverbs. Trea
|
|
|
66
66
|
bare run as a smoke test: the real done-check is running the subverbs your project
|
|
67
67
|
uses explicitly — `verify --codegen`, and, where a DB exists, `verify --db <url>`.
|
|
68
68
|
|
|
69
|
+
## Requirements are checked on every run
|
|
70
|
+
|
|
71
|
+
If this project declares `requirement.functional` / `requirement.architectural` nodes, read
|
|
72
|
+
`references/requirements.md`: requirements are metadata, so they are checked on **every**
|
|
73
|
+
`meta verify` — there is no subverb — and the severity of a broken link depends on the
|
|
74
|
+
requirement's `@status`, which is the part that surprises people reading a failure.
|
|
75
|
+
|
|
69
76
|
## The `verify` subverbs
|
|
70
77
|
|
|
71
78
|
`verify` has three drift checks. Run them in CI.
|
|
@@ -245,6 +245,11 @@ schema without a rewrite:
|
|
|
245
245
|
- **`identity.reference @constraintName`** pins a foreign-key constraint name so the
|
|
246
246
|
metadata can match an existing DB's naming convention without a destructive
|
|
247
247
|
rename.
|
|
248
|
+
- **`identity.reference @onDelete` / `@onUpdate`** pin a foreign key's referential
|
|
249
|
+
actions directly on the FK (ADR-0047) — the way to match an adopted DB's existing
|
|
250
|
+
`ON DELETE`/`ON UPDATE` behavior when the model declares no `relationship.*` for
|
|
251
|
+
that FK (a relationship's explicit action or subtype default otherwise supplies
|
|
252
|
+
it, whichever side of the FK declares the relationship).
|
|
248
253
|
|
|
249
254
|
## Not yet shipped
|
|
250
255
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Requirements — what `meta verify` checks
|
|
2
|
+
|
|
3
|
+
This project declares `requirement.*` nodes, so `verify` checks them. **There is no
|
|
4
|
+
subverb**: requirements are metadata, so they are checked on *every* `meta verify` run.
|
|
5
|
+
|
|
6
|
+
## The split, and why it matters when you read a failure
|
|
7
|
+
|
|
8
|
+
| | owns |
|
|
9
|
+
|---|---|
|
|
10
|
+
| **loader** | `@status` enum, required attrs, child rules, levels — unconditional |
|
|
11
|
+
| **`verify`** | `@implementedBy` / `@verifiedBy` resolution — **severity depends on `@status`** |
|
|
12
|
+
|
|
13
|
+
A typo'd `@status` fails the **load** ("failed to load metadata"), before verify runs. If you
|
|
14
|
+
see that, no other diagnostic in the run is trustworthy — fix it first and re-run.
|
|
15
|
+
|
|
16
|
+
## The status asymmetry — the one that surprises people
|
|
17
|
+
|
|
18
|
+
The **same** unresolved `@implementedBy` reference is:
|
|
19
|
+
|
|
20
|
+
- an **error** on `live` / `partial` — the model moved and the requirement is stale;
|
|
21
|
+
- **allowed** on `abandoned` / `superseded` — those nodes are *supposed* to be gone. That is
|
|
22
|
+
the entry doing its job, not drift.
|
|
23
|
+
|
|
24
|
+
So do not "fix" a dangling reference on an abandoned requirement by deleting it. Deleting it
|
|
25
|
+
destroys the record that something was deliberately retired, which is the single thing this
|
|
26
|
+
mechanism exists to preserve.
|
|
27
|
+
|
|
28
|
+
## Exit codes
|
|
29
|
+
|
|
30
|
+
| situation | exit |
|
|
31
|
+
|---|---|
|
|
32
|
+
| clean tree, or no `requirement.*` nodes at all | 0 |
|
|
33
|
+
| dangling `@implementedBy` on `live`/`partial` | 1 |
|
|
34
|
+
| the same reference on `abandoned`/`superseded` | 0 |
|
|
35
|
+
| `@implementedBy` above the L4 link floor | 1 |
|
|
36
|
+
| live `requirement.architectural` claimed by nothing | 1 |
|
|
37
|
+
| `@verifiedBy` naming a test that exists nowhere | 1 |
|
|
38
|
+
| `@verifiedBy` naming a test that is **skipped** | 0 (warning) |
|
|
39
|
+
| an entity no requirement claims | 0 (warning) |
|
|
40
|
+
|
|
41
|
+
## What a green run does NOT prove
|
|
42
|
+
|
|
43
|
+
It proves **referential integrity**: statuses parse, levels are in range, links sit at or
|
|
44
|
+
below the floor, references resolve, named tests exist and are not skipped.
|
|
45
|
+
|
|
46
|
+
It cannot prove a status is **true**, or that a node genuinely implements the requirement
|
|
47
|
+
claiming it. No test can. That judgement is yours.
|
|
48
|
+
|
|
49
|
+
Coverage is also narrower than the name suggests: it is checked at **entity grain only** —
|
|
50
|
+
`object.value` and `object.projection` are exempt, and fields, views, validators and
|
|
51
|
+
identities are never required to be claimed. Green means "every entity is claimed by
|
|
52
|
+
something", not "every node is described".
|
|
53
|
+
|
|
54
|
+
Full reference: the repo's `spec/capability-ledger.md`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,EACyD,KAAK,aAAa,EAAE,KAAK,KAAK,EAC7F,MAAM,YAAY,CAAC;AAuCpB,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,aAAa,EAAE,CA0CrF"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { SKILL_NAMES, SERVER_LANGS, CLIENT_FRAMEWORKS } from "./types.js";
|
|
4
|
-
// Language/framework reference fragments are stack-scoped; any other
|
|
5
|
-
// (e.g. capability-checklist) is universal and always installs.
|
|
6
|
-
|
|
3
|
+
import { SKILL_NAMES, SERVER_LANGS, CLIENT_FRAMEWORKS, CONCERN_TOKENS, } from "./types.js";
|
|
4
|
+
// Language/framework/concern reference fragments are stack-scoped; any other
|
|
5
|
+
// fragment (e.g. capability-checklist) is universal and always installs. Adding a
|
|
6
|
+
// future concern token to CONCERN_TOKENS (types.ts) is all that's needed to gate
|
|
7
|
+
// its matching references/<token>.md fragment here.
|
|
8
|
+
const SCOPED_FRAGMENT_TOKENS = new Set([...SERVER_LANGS, ...CLIENT_FRAMEWORKS, ...CONCERN_TOKENS]);
|
|
7
9
|
function readServerMeta(contentRoot, server) {
|
|
8
10
|
const p = join(contentRoot, "servers", `${server}.meta.json`);
|
|
9
11
|
if (!existsSync(p))
|
|
@@ -49,17 +51,18 @@ export function assemble(opts) {
|
|
|
49
51
|
out.push({ path: `.claude/skills/${skill}/SKILL.md`, contents: body });
|
|
50
52
|
const refDir = join(skillDir, "references");
|
|
51
53
|
if (existsSync(refDir) && statSync(refDir).isDirectory()) {
|
|
52
|
-
// Stack-scoped: a language/framework fragment installs only when its
|
|
53
|
-
// is in the resolved stack (Stack.tokens = servers ∪ clients ∪
|
|
54
|
-
// Non-
|
|
55
|
-
// install. Keeps each skill's references/ to what the
|
|
56
|
-
// so the SKILL.md "read every references/*.md" footer
|
|
54
|
+
// Stack-scoped: a language/framework/concern fragment installs only when its
|
|
55
|
+
// token is in the resolved stack (Stack.tokens = servers ∪ clients ∪ concerns
|
|
56
|
+
// ∪ {migration}). Non-scoped fragments (e.g. capability-checklist) are
|
|
57
|
+
// universal and always install. Keeps each skill's references/ to what the
|
|
58
|
+
// project actually uses, so the SKILL.md "read every references/*.md" footer
|
|
59
|
+
// stays accurate.
|
|
57
60
|
const refs = readdirSync(refDir)
|
|
58
61
|
.filter((f) => f.endsWith(".md"))
|
|
59
62
|
.map((f) => f.replace(/\.md$/, ""))
|
|
60
63
|
.sort();
|
|
61
64
|
for (const token of refs) {
|
|
62
|
-
if (
|
|
65
|
+
if (SCOPED_FRAGMENT_TOKENS.has(token) && !stack.tokens.has(token))
|
|
63
66
|
continue;
|
|
64
67
|
out.push({
|
|
65
68
|
path: `.claude/skills/${skill}/references/${token}.md`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,GAC7D,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,kFAAkF;AAClF,iFAAiF;AACjF,oDAAoD;AACpD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,YAAY,EAAE,GAAG,iBAAiB,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC;AAI3G,SAAS,cAAc,CAAC,WAAmB,EAAE,MAAc;IACzD,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,YAAY,CAAC,CAAC;IAC9D,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACrC,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAe,CAAC;AAC3D,CAAC;AAED,SAAS,SAAS,CAAC,WAAmB,EAAE,KAAY;IAClD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;IAC7F,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;IAC7F,OAAO;QACL,4EAA4E;QAC5E,yEAAyE;QACzE,6EAA6E;QAC7E,6EAA6E;QAC7E,2DAA2D;QAC3D,IAAI,EAAE,UAAU,UAAU,KAAK,UAAU,GAAG;QAC5C,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,UAAU;KACxD,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,GAAW,EAAE,IAA4B;IAC9D,OAAO,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,EAAE,EAAE,CAAS,EAAE,EAAE;QACrD,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,IAAI,CAAC,CAAC;QACvF,OAAO,IAAI,CAAC,CAAC,CAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,IAA2C;IAClE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACpC,MAAM,GAAG,GAAoB,EAAE,CAAC;IAEhC,4DAA4D;IAC5D,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,uBAAuB,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1F,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;IACzE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IACjE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEjE,qEAAqE;IACrE,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,KAAK,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAEvE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC5C,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACzD,6EAA6E;YAC7E,8EAA8E;YAC9E,uEAAuE;YACvE,2EAA2E;YAC3E,6EAA6E;YAC7E,kBAAkB;YAClB,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC;iBAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;iBAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;iBAClC,IAAI,EAAE,CAAC;YACV,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;gBACzB,IAAI,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;oBAAE,SAAS;gBAC5E,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,kBAAkB,KAAK,eAAe,KAAK,KAAK;oBACtD,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,KAAK,CAAC,EAAE,MAAM,CAAC;iBAC5D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { type ServerLang, type ClientFramework, type Stack } from "./types.js";
|
|
1
|
+
import { type ServerLang, type ClientFramework, type ConcernToken, type Stack } from "./types.js";
|
|
2
2
|
export interface ProjectProbe {
|
|
3
3
|
hasDep(name: string): boolean;
|
|
4
4
|
hasFileMatching(pattern: RegExp): boolean;
|
|
5
|
+
/** True if the project's declared metadata contains at least one `requirement.*` node. */
|
|
6
|
+
hasRequirementNodes(): boolean;
|
|
5
7
|
}
|
|
6
|
-
export declare function makeStack(servers: ServerLang[], clients: ClientFramework[]): Stack;
|
|
8
|
+
export declare function makeStack(servers: ServerLang[], clients: ClientFramework[], concerns?: ConcernToken[]): Stack;
|
|
7
9
|
/** Best-effort detection from a project probe. Always overridable; a wrong guess
|
|
8
10
|
* writes an extra fragment, never a wrong one (callers confirm before scaffolding). */
|
|
9
11
|
export declare function detectStack(probe: ProjectProbe): {
|
|
10
12
|
servers: ServerLang[];
|
|
11
13
|
clients: ClientFramework[];
|
|
12
14
|
};
|
|
15
|
+
/** Best-effort concern detection — OBSERVED project state, never a config flag.
|
|
16
|
+
* Independent of servers/clients: a project's use of a capability doesn't depend
|
|
17
|
+
* on which server language or client framework it runs. */
|
|
18
|
+
export declare function detectConcerns(probe: ProjectProbe): ConcernToken[];
|
|
13
19
|
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/agent-context/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,KAAK,
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/agent-context/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,KAAK,EACrE,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1C,0FAA0F;IAC1F,mBAAmB,IAAI,OAAO,CAAC;CAChC;AAED,wBAAgB,SAAS,CACvB,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,eAAe,EAAE,EAC1B,QAAQ,GAAE,YAAY,EAAO,GAC5B,KAAK,CAKP;AAED;uFACuF;AACvF,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,eAAe,EAAE,CAAA;CAAE,CActG;AAED;;2DAE2D;AAC3D,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,EAAE,CAIlE"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { SERVER_LANGS, CLIENT_FRAMEWORKS, MIGRATION_TOKEN, } from "./types.js";
|
|
2
|
-
export function makeStack(servers, clients) {
|
|
1
|
+
import { SERVER_LANGS, CLIENT_FRAMEWORKS, CONCERN_TOKENS, MIGRATION_TOKEN, } from "./types.js";
|
|
2
|
+
export function makeStack(servers, clients, concerns = []) {
|
|
3
3
|
const s = SERVER_LANGS.filter((x) => servers.includes(x));
|
|
4
4
|
const c = CLIENT_FRAMEWORKS.filter((x) => clients.includes(x));
|
|
5
|
-
|
|
5
|
+
const k = CONCERN_TOKENS.filter((x) => concerns.includes(x));
|
|
6
|
+
return { servers: s, clients: c, concerns: k, tokens: new Set([...s, ...c, ...k, MIGRATION_TOKEN]) };
|
|
6
7
|
}
|
|
7
8
|
/** Best-effort detection from a project probe. Always overridable; a wrong guess
|
|
8
9
|
* writes an extra fragment, never a wrong one (callers confirm before scaffolding). */
|
|
@@ -27,4 +28,13 @@ export function detectStack(probe) {
|
|
|
27
28
|
clients.push("angular");
|
|
28
29
|
return { servers, clients };
|
|
29
30
|
}
|
|
31
|
+
/** Best-effort concern detection — OBSERVED project state, never a config flag.
|
|
32
|
+
* Independent of servers/clients: a project's use of a capability doesn't depend
|
|
33
|
+
* on which server language or client framework it runs. */
|
|
34
|
+
export function detectConcerns(probe) {
|
|
35
|
+
const concerns = [];
|
|
36
|
+
if (probe.hasRequirementNodes())
|
|
37
|
+
concerns.push("requirements");
|
|
38
|
+
return concerns;
|
|
39
|
+
}
|
|
30
40
|
//# sourceMappingURL=resolve.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/agent-context/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAAE,iBAAiB,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/agent-context/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,GAEjE,MAAM,YAAY,CAAC;AASpB,MAAM,UAAU,SAAS,CACvB,OAAqB,EACrB,OAA0B,EAC1B,WAA2B,EAAE;IAE7B,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,CAAC,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,GAAG,CAAS,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC;AAC/G,CAAC;AAED;uFACuF;AACvF,MAAM,UAAU,WAAW,CAAC,KAAmB;IAC7C,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,IAAI,KAAK,CAAC,MAAM,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,4BAA4B,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAClH,IAAI,KAAK,CAAC,eAAe,CAAC,8BAA8B,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClF,IAAI,KAAK,CAAC,eAAe,CAAC,iBAAiB,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,KAAK,CAAC,eAAe,CAAC,WAAW,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,KAAK,CAAC,eAAe,CAAC,oCAAoC,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAExF,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,CAAC,uBAAuB,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjE,IAAI,KAAK,CAAC,MAAM,CAAC,0BAA0B,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvE,IAAI,KAAK,CAAC,MAAM,CAAC,yBAAyB,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAErE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC;AAED;;2DAE2D;AAC3D,MAAM,UAAU,cAAc,CAAC,KAAmB;IAChD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,IAAI,KAAK,CAAC,mBAAmB,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC/D,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -2,6 +2,14 @@ export declare const SERVER_LANGS: readonly ["typescript", "java", "kotlin", "cs
|
|
|
2
2
|
export type ServerLang = (typeof SERVER_LANGS)[number];
|
|
3
3
|
export declare const CLIENT_FRAMEWORKS: readonly ["react", "tanstack", "angular"];
|
|
4
4
|
export type ClientFramework = (typeof CLIENT_FRAMEWORKS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in capability concerns, detected from OBSERVED project state (never a config
|
|
7
|
+
* flag — a flag goes stale, observed metadata cannot). Each token gates a
|
|
8
|
+
* `references/<token>.md` fragment the same way a language/framework token does.
|
|
9
|
+
* Add a new concern here; nothing else in the assembler needs to change.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CONCERN_TOKENS: readonly ["requirements"];
|
|
12
|
+
export type ConcernToken = (typeof CONCERN_TOKENS)[number];
|
|
5
13
|
/** Always-present token: schema migrations are TS-owned for every port (ADR-0015). */
|
|
6
14
|
export declare const MIGRATION_TOKEN = "migration";
|
|
7
15
|
export declare const SKILL_NAMES: readonly ["metaobjects-authoring", "metaobjects-codegen", "metaobjects-runtime-ui", "metaobjects-prompts", "metaobjects-verify", "metaobjects-audit"];
|
|
@@ -10,7 +18,8 @@ export type SkillName = (typeof SKILL_NAMES)[number];
|
|
|
10
18
|
export interface Stack {
|
|
11
19
|
servers: ServerLang[];
|
|
12
20
|
clients: ClientFramework[];
|
|
13
|
-
|
|
21
|
+
concerns: ConcernToken[];
|
|
22
|
+
/** servers ∪ clients ∪ concerns ∪ {"migration"} — the install-selection set for reference fragments. */
|
|
14
23
|
tokens: ReadonlySet<string>;
|
|
15
24
|
}
|
|
16
25
|
/** A file the assembler emits, path relative to the consumer project root. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agent-context/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,+DAAgE,CAAC;AAC1F,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,eAAO,MAAM,iBAAiB,2CAA4C,CAAC;AAC3E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,sFAAsF;AACtF,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C,eAAO,MAAM,WAAW,uJAOd,CAAC;AACX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,qDAAqD;AACrD,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agent-context/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,+DAAgE,CAAC;AAC1F,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,eAAO,MAAM,iBAAiB,2CAA4C,CAAC;AAC3E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,2BAA4B,CAAC;AACxD,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,sFAAsF;AACtF,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C,eAAO,MAAM,WAAW,uJAOd,CAAC;AACX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,qDAAqD;AACrD,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,wGAAwG;IACxG,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export const SERVER_LANGS = ["typescript", "java", "kotlin", "csharp", "python"];
|
|
2
2
|
export const CLIENT_FRAMEWORKS = ["react", "tanstack", "angular"];
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in capability concerns, detected from OBSERVED project state (never a config
|
|
5
|
+
* flag — a flag goes stale, observed metadata cannot). Each token gates a
|
|
6
|
+
* `references/<token>.md` fragment the same way a language/framework token does.
|
|
7
|
+
* Add a new concern here; nothing else in the assembler needs to change.
|
|
8
|
+
*/
|
|
9
|
+
export const CONCERN_TOKENS = ["requirements"];
|
|
3
10
|
/** Always-present token: schema migrations are TS-owned for every port (ADR-0015). */
|
|
4
11
|
export const MIGRATION_TOKEN = "migration";
|
|
5
12
|
export const SKILL_NAMES = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agent-context/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAC;AAG1F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,CAAU,CAAC;AAG3E,sFAAsF;AACtF,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,uBAAuB;IACvB,qBAAqB;IACrB,wBAAwB;IACxB,qBAAqB;IACrB,oBAAoB;IACpB,mBAAmB;CACX,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agent-context/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAC;AAG1F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,CAAU,CAAC;AAG3E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,cAAc,CAAU,CAAC;AAGxD,sFAAsF;AACtF,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,uBAAuB;IACvB,qBAAqB;IACrB,wBAAwB;IACxB,qBAAqB;IACrB,oBAAoB;IACpB,mBAAmB;CACX,CAAC"}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* @deprecated The single-blob agent doc is replaced by the assembled agent-context
|
|
3
3
|
* (see `@metaobjectsdev/sdk/agent-context`). Kept only for back-compat; not scaffolded by `meta init`.
|
|
4
4
|
*/
|
|
5
|
-
export declare const AGENT_DOCS_BODY = "# Meta Forge \u2014 agent reference\n\nThis file is scaffolded by `meta init` and lives alongside your `metaobjects/` records. It teaches AI coding assistants (Claude Code, Codex, etc.) how to read and modify MetaObjects metadata correctly. Refresh after CLI updates with `meta init --refresh-docs`.\n\n## Five working principles (read first)\n\nThese shape every interaction with a metaobjects-driven project. Follow them when you author metadata, write hand-coded business logic, or review someone else's work.\n\n### 1. If it's pattern-derivable from metadata, generate it. Never hand-write boilerplate.\n\nThe metaobjects raison d'\u00EAtre is that anything the metadata fully describes \u2014 schemas, FK references, basic CRUD, query helpers, Zod validators, route handlers, RHF rules, form fields \u2014 should be produced by codegen, not hand-typed. If you find yourself hand-writing something the metadata already knows about, stop and use the generated artifact.\n\nThe first version of the trainer website's database layer had hand-written Drizzle schemas, Zod schemas, and CRUD endpoints. Every one of those is now generated. The hand-written code that remains is real business logic (Stripe webhooks, Loops integration, custom auth flows) \u2014 things the generator genuinely cannot derive.\n\nWhen you're about to add a new field or entity: edit `metaobjects/*.json` and re-run `meta gen`. Don't reach for the generated file directly.\n\n### 2. Use the generated constants. Never use magic strings that touch metadata.\n\nAfter `meta gen`, each entity file exports a rich metadata-constants block. Each non-dollar-prefixed key is a per-field object that carries everything a consumer might need (name, label, view, html input type, placeholder, RHF validation rules):\n\n```ts\nexport const Subscriber = {\n $entity: \"Subscriber\", // entity name string\n $table: \"subscribers\", // SQL table name\n $path: \"/subscribers\", // REST resource path\n\n email: {\n name: \"email\", // field name string (use for filters, register())\n label: \"Email Address\", // humanized fallback or @label override\n view: \"text\", // MetaView subtype\n htmlType: \"email\", // optional; maps view \u2192 HTML <input type=>\n placeholder: \"you@example.com\", // optional; only when @placeholder is set on the view\n helpText: \"We never share this.\", // optional; only when @helpText is set\n rules: { // optional; derived from validator children\n required: \"Email is required\",\n maxLength: { value: 255, message: \"Too long\" },\n pattern: { value: /.../, message: \"Invalid email\" },\n },\n },\n firstName: { name: \"firstName\", label: \"First Name\", view: \"text\", htmlType: \"text\", rules: { required: \"First Name is required\" } },\n // ...\n} as const;\n```\n\n**Use them everywhere \u2014 in both generated AND hand-written code:**\n\n```tsx\n// \u2717 Don't:\n<input name=\"email\" type=\"email\" placeholder=\"Email\" />\n\n// \u2713 Do:\n<input\n type={Subscriber.email.htmlType}\n name={Subscriber.email.name}\n placeholder={Subscriber.email.placeholder}\n aria-label={Subscriber.email.label}\n/>\n```\n\nRename a field in `metaobjects/` and re-gen \u2014 TypeScript catches every stale reference.\n\n**Special case \u2014 Drizzle column access:** when you're already inside Drizzle's typed builder, just use the column properties directly. Drizzle's table-const types are themselves derived from metadata, so `weeks.programId` is already TS-safe:\n\n```ts\n// \u2713 Use Drizzle's typed accessor directly \u2014 no constants needed here:\ndb.select().from(weeks).where(eq(weeks.programId, X))\n\n// \u2717 Don't do this \u2014 it's redundant indirection:\ndb.select().from(weeks).where(eq(weeks[Week.programId.name], X))\n```\n\nUse the constants when you need a STRING (filter object keys, registration arguments, REST paths, labels). Use Drizzle properties directly when the type system already does the work.\n\n### 3. Forms: spread `form.input.<field>` from useEntityForm. One line per input.\n\nFor React forms, use `useEntityForm` from `@metaobjectsdev/react`. It returns the standard React Hook Form surface plus a pre-bound `.input` accessor \u2014 one entry per field, ready to spread onto an `<input>`:\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema } from './generated/Subscriber';\n\nconst form = useEntityForm(Subscriber, SubscriberInsertSchema);\n\n<label>{Subscriber.email.label}</label>\n<input {...form.input.email} /> // \u2190 type, placeholder, name, rules, aria-label all spread automatically\n```\n\nFor non-`<input>` controls (textarea, select), the `type` attr is omitted from `form.input.X` \u2014 pick the right element yourself.\n\nThe same Zod schema (`SubscriberInsertSchema`) validates on the server (in Fastify routes) and on the client (via the resolver). One schema, two surfaces, zero drift.\n\n### 4. Routes: use the generated `<Entity>.routes.ts` for stock CRUD. Hand-write only what's custom.\n\n`meta gen` emits a per-entity routes file that mounts the 5 standard verbs via `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify`. The runtime is plain Drizzle + Zod \u2014 no extra ORM.\n\nFor custom flows (Stripe webhooks, side effects, auth-gated actions), hand-write the route \u2014 but import the entity constants + generated Zod schema. The boilerplate (CRUD, validation, 404 mapping, pagination) lives in the helper; your hand-written code is just the business logic.\n\n**Auth pattern:** install a plugin-level Fastify `preHandler` hook at the top of your route plugin. The hook applies to every route registered after it \u2014 both hand-written handlers AND metaobjects-generated routes via the `routeOptions` field. Beats sprinkling `if (!auth(...)) return;` at the top of every handler.\n\n### 5. Hand-coded code is always available, but coexists with generated code.\n\nGenerated code does the boilerplate. Hand-coded code does the business logic. They live in the same project, the same package, sometimes the same file. The hand-coded code consumes the generated constants and generated Zod schemas \u2014 it never duplicates schema, never hard-codes paths, never declares its own validators that metadata could declare.\n\nConcrete pattern from the trainer website:\n- Generated `Subscriber.routes.ts` registers GET / GET-by-id / POST / PATCH / DELETE on `/api/subscribers`.\n- Hand-written `apps/api/src/routes/subscribers.ts` keeps `POST /subscribe` \u2014 the custom endpoint with the Loops side-effect.\n- Both registered with `fastify.register()`. Both validate via `SubscriberInsertSchema`. Both use `Subscriber.email.name` / etc. Neither knows the other exists.\n\n## Metaobjects metamodel \u2014 quick rules\n\nThe format used by `metaobjects/*.json` is **metaobjects metadata**, a cross-language standard. Eight base types:\n\n| Type | Purpose |\n|---|---|\n| `metadata` | Root document wrapper |\n| `object` | An entity (table/record) |\n| `field` | A property on an object |\n| `attr` | Named scalar/array decoration on any parent |\n| `validator` | A validation rule |\n| `view` | A UI control kind |\n| `identity` | A primary/secondary key |\n| `relationship` | An association between objects |\n\n### Two most-violated rules\n\n1. **Attribute uniqueness.** Within a single parent metadata node, all attribute names must be unique. You cannot have two `attr` children both named `alternative`. For multi-value, use a single `stringarray` attr: `\"@alternatives\": [\"a\", \"b\", \"c\"]`.\n\n2. **Inline `@<name>` and `attr` child are the same thing.** `\"@maxLength\": 50` is shorthand for `{\"attr\": {\"name\": \"maxLength\", \"subType\": \"int\", \"value\": \"50\"}}`. The parser converts inline form into attr children. Don't use both forms for the same attribute name on the same parent.\n\n### Object subtypes (v0.3)\n\n- `base` \u2014 abstract template (no runtime semantics)\n- `entity` \u2014 persistent record; should have a primary identity\n- `value` \u2014 value-object; equality by content; must NOT have a primary identity\n\nJava-runtime strategies (pojo / map / proxy) belong on `@javaRuntime`, not in `subType`.\n\n### Reserved structural keys (NOT attributes)\n\n`name`, `subType`, `package`, `extends`, `isAbstract`, `children`, `merge`, `value`.\n\nThe v0.2 keys (`super`, `overlay`, `override`, `isInterface`, `implements`) are **gone**. The current parser will reject them. Use:\n- `extends:` instead of `super:` for the supertype reference\n- `merge: true` instead of `overlay: true` / `override: true` for in-place modification\n- `@isAbstract: true` instead of `isInterface: true` (multiple inheritance is not supported)\n\n### Package paths and inheritance\n\n- Package segments separated by `::` \u2014 `acme::common::id`\n- Relative references in `extends:` \u2014 `..::common::id` means \"go up to parent package, descend into `common::id`\". Relative forms (`..::` parent-relative, leading `::` root-absolute) are a **YAML-authoring affordance only**; canonical JSON must be fully-qualified (a relative ref in JSON is rejected with `ERR_RELATIVE_REF_IN_CANONICAL`).\n- Cross-file resolution works as long as all files are passed to Loader (or live in the same `metaobjects/` directory)\n\n### Two special intercepted attrs (parser-routed)\n\n- `@isArray` \u2192 marks a field as a collection\n- `@isAbstract` \u2192 marks a node as abstract (inheritable but not instantiable)\n\n## Validators \u2014 two layers\n\nValidators can attach in two places, and they compose:\n\n**Field-level validators** describe what makes the *stored value* valid. They survive across UI, API, batch import, manual SQL \u2014 anywhere data enters the system. The generated Zod `<Entity>InsertSchema` encodes these.\n\n```json\n{\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"required\"}},\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^[^@]+@[^@]+\\\\.[^@]+$\"}}\n ]\n}}\n```\n\n**View-level validators** describe what makes user *input* valid in a specific UI surface \u2014 possibly stricter, possibly with different messages, possibly format-specific. They run client-side in generated forms. They do NOT necessarily reject the stored value if it's already in the DB.\n\n```json\n{\"field\": {\"name\": \"phone\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^\\\\+?[0-9]+$\"}},\n {\"view\": {\"subType\": \"text-input\", \"@label\": \"Phone\",\n \"children\": [\n {\"validator\": {\"subType\": \"length\", \"@min\": 7, \"@max\": 20,\n \"@message\": \"Phone must be 7-20 digits\"}}\n ]\n }}\n ]\n}}\n```\n\nRule of thumb: rules that protect data integrity \u2192 field. Rules that improve input UX \u2192 view.\n\n## metaobjects.config.ts \u2014 generator wiring (project root)\n\n`meta gen` reads `metaobjects.config.ts` at the project root. This is where you declare which generators run and their options. It is TypeScript, type-checked, and imported via `jiti` at run time.\n\n```ts\nimport { defineConfig } from \"@metaobjectsdev/cli\";\nimport {\n entityFile, queriesFile, routesFile, /* formFile, */ barrel,\n} from \"@metaobjectsdev/codegen-ts/generators\";\n\nexport default defineConfig({\n outDir: \"packages/database/src/generated\",\n extStyle: \"none\",\n dbImport: \"../index\",\n dialect: \"sqlite\",\n generators: [\n entityFile(),\n queriesFile(),\n routesFile(),\n // formFile(), // opt-in: emits stock React forms per entity\n barrel(),\n ],\n});\n```\n\n3rd-party generator example: `import { tanstackQuery } from \"@metaobjectsdev/codegen-ts-tanstack\"; // then add tanstackQuery({ ... }) to the generators array`\n\nFilters live on the generator entry: `routesFile({ filter: e => e.name !== \"AuditLog\" })`\n\n`.metaobjects/config.json` is unchanged \u2014 it still holds static project state (schema_version, pending_in_git, confidence_thresholds). Generator wiring belongs in `metaobjects.config.ts` so TypeScript can type-check the imports.\n\n## Generated hooks + grids (TanStack)\n\nWhen `tanstackQuery()` is in your `metaobjects.config.ts`, every entity gets `<Entity>.hooks.ts` with a query-key factory + `useEntity`, `useEntities`, `useCreate`, `useUpdate`, `useDelete` hooks. When `tanstackGrid()` is in the config, entities with a `layout[dataGrid]` child also get `<Entity>.columns.tsx`.\n\n```tsx\nimport { usePrograms, useCreateProgram } from \"@your-pkg/database/generated/Program.hooks\";\nimport { programDefaultColumns, programDefaultGrid } from \"@your-pkg/database/generated/Program.columns\";\nimport { EntityGrid } from \"@metaobjectsdev/tanstack\";\n\nconst { data, isLoading } = usePrograms();\nconst create = useCreateProgram({ onSuccess: () => navigate(\"/programs\") });\n\n<EntityGrid\n columns={programDefaultColumns}\n grid={programDefaultGrid}\n data={data ?? []}\n isLoading={isLoading}\n onRowClick={(row) => navigate(`/admin/programs/${row.id}`)}\n/>\n```\n\n**Provider setup.** Wrap your app with `<EntityFetcherProvider value={fetcher}>` (supplies the HTTP fetcher to all generated hooks). For an admin subtree with different auth, wrap a second time inside: `<EntityFetcherProvider value={adminFetch}>...</EntityFetcherProvider>` overrides the outer one.\n\n**Metadata layer \u2014 grid definition:**\n\n```jsonc\n{ \"layout\": {\n \"subType\": \"dataGrid\",\n \"name\": \"default\",\n \"@pageSize\": 25,\n \"@defaultSortField\": \"createdAt\",\n \"@defaultSortOrder\": \"desc\",\n \"@filterable\": true,\n \"@columns\": [\"email\", \"firstName\", \"subscribed\", \"createdAt\"]\n}}\n```\n\nThe `@columns` attr is a flat string array listing fields to display. Per-column rendering comes from each field's own `view` subtype (the same one that drives forms); sortability comes from the field's `@sortable` attr; width belongs in app CSS. There are no nested per-column children \u2014 just `@columns`.\n\n**Cell renderers.** Field rendering inside grids comes from each field's own `view` subtype (the same one that drives forms). Override defaults app-wide with `<CellRendererProvider value={{ currency: ({ getValue }) => <Money value={getValue()} /> }}>`.\n\n**Per-entity opt-out.** `@emitTanstack: false` on an entity skips both hooks and columns.\n\n## Filtering generated lists\n\nMark filterable fields in metadata with `@filterable: true`:\n\n```jsonc\n{ \"field\": { \"name\": \"email\", \"subType\": \"string\", \"@filterable\": true } }\n```\n\nThe generated `useSubscribers(filter)` hook accepts a typed filter:\n\n```tsx\nconst { data } = useSubscribers({\n email: { like: \"%@example.com\" },\n subscribed: true,\n sort: \"createdAt:desc\",\n limit: 25,\n});\n```\n\nURL sent: `/subscribers?filter[email][like]=%25@example.com&filter[subscribed]=true&sort=createdAt:desc&limit=25`\n\n**Operators by field subtype:**\n- String: `eq, ne, in, like, isNull`\n- Number/date: `eq, ne, gt, gte, lt, lte, in, isNull`\n- Boolean: `eq, isNull`\n\nIllegal combinations like `useSubscribers({ subscribed: { gte: true } })` fail to compile (booleans don't support `gte`).\n\n**Per-grid preset filter** via layout `@filter`:\n\n```jsonc\n{ \"layout\": { \"subType\": \"dataGrid\", \"name\": \"active\",\n \"@filter\": { \"subscribed\": true },\n \"@columns\": [\"email\", \"firstName\", \"subscribed\"] }}\n```\n\nGenerates `subscriberActiveFilter` const consumable in pages. Compose with ad-hoc filters via object spread.\n\n## Projections (read models with joined/aggregated columns)\n\nWhen a list needs computed columns (counts, sums, joined fields), create a **projection** \u2014 an entity that extends a base entity but reads from a SQL view:\n\n```json\n// metaobjects/meta.commerce.json (inline with Program)\n{\n \"object\": {\n \"name\": \"ProgramSummary\",\n \"subType\": \"entity\",\n \"extends\": \"Program\",\n \"children\": [\n { \"source\": { \"subType\": \"rdb\", \"@kind\": \"view\", \"@table\": \"v_program_summary\" } },\n { \"field\": { \"name\": \"weekCount\", \"subType\": \"int\", \"children\": [\n { \"origin\": { \"subType\": \"aggregate\",\n \"@agg\": \"count\", \"@of\": \"Week.id\", \"@via\": \"Program.weeks\" }}\n ]}},\n { \"identity\": { \"subType\": \"primary\", \"name\": \"id\", \"@fields\": \"id\" } }\n ]\n }\n}\n```\n\n`meta gen` produces a read-only `useProgramSummaries(filter)` hook, a SQL view DDL in the migration, and a read-only GET-only route.\n\n**Aggregate vocabulary** (`origin.aggregate @agg`): `count`, `sum`, `avg`, `min`, `max`; plus `any`/`all` (a boolean predicate quantifier over a required `@filter` \u2014 no `@of`; \"did any/every related row match?\"), and `collect` (an array rollup of `@of` \u2014 the field must be `isArray: true`; `@distinct` dedupes, `@orderBy` sets element order).\n\n**Other read-model origins**: `origin.computed` \u2014 a row-level value from the base row's own fields via a structured `@expr` tree (e.g. `{ \"op\": \"isNotNull\", \"arg\": { \"field\": \"payloadJson\" } }` \u2192 a boolean, to avoid shipping a heavy column); `origin.first` \u2014 the single related row picked by `@orderBy` along `@via`, projecting `@of` (e.g. \"the latest child's status\"; the field must not be `@required` \u2014 an empty related set yields null).\n\n**Multi-level via paths** are supported: `@via: \"Program.weeks.workouts\"` builds a 2-level JOIN tree.\n\n**For pages that need a full nested tree** (e.g., Program \u2192 Weeks \u2192 Workouts \u2192 Exercises), use 4 entity hooks with Project D's filter syntax for batched lookups (no projection needed \u2014 flat hooks + client-side stitching is enough):\n\n```tsx\nconst { data: weeks } = useWeeks({ programId, sort: \"weekNumber:asc\" });\nconst weekIds = weeks?.map((w) => w.id) ?? [];\nconst { data: workouts } = useWorkouts(\n weekIds.length ? { weekId: { in: weekIds } } : undefined,\n);\n```\n\n## Currency fields\n\nDeclare a money field with `subType: \"currency\"`:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"USD\" } }\n```\n\nStorage stays as integer minor units (cents for USD). The generated `<Entity>` constants block carries `view`, `currency`, `locale` so admin grids auto-format prices.\n\n**Imports \u2014 use sub-paths in browser code:**\n\n```tsx\nimport { formatCurrency } from \"@metaobjectsdev/runtime-web\";\nimport { CurrencyInput } from \"@metaobjectsdev/react\";\n```\n\n**Display:**\n\n```tsx\n<span>{formatCurrency(program.priceCents)}</span> // $15.00\n<span>{formatCurrency(p.amountCents, \"EUR\", \"de-DE\")}</span> // 15,00 \u20AC\n```\n\n**Form input:**\n\n```tsx\n<CurrencyInput value={priceCents} onChange={setPriceCents} currency=\"USD\" />\n```\n\nUser types `15.99` \u2192 component emits `1599` to `onChange` on blur. Wire format is always integer cents.\n\n**Locale override** via a `view[currency]` child:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"EUR\",\n \"children\": [{ \"view\": { \"subType\": \"currency\", \"@locale\": \"de-DE\" } }]\n}}\n```\n\nCurrency code lives on the field; locale lives on the view.\n\n## Generated artifacts \u2014 what `meta gen` produces\n\nAfter `meta gen`, you get one barrel + per-entity files in your configured `outDir` (default `packages/database/src/generated/`):\n\n| File | What's in it | When to touch by hand |\n|---|---|---|\n| `<Entity>.ts` | Drizzle table, relations(), inferred types, Zod insert/update schemas, and the rich `<Entity>` constants block (per-field objects with name, label, view, htmlType, rules, etc.) | Never. Regenerate. |\n| `<Entity>.queries.ts` | Typed query helpers (`findUserById`, `listUsers`, `createUser`, ...) using prepared statements | Never. Regenerate. |\n| `<Entity>.routes.ts` | Fastify CRUD plugin delegating to `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify` (5 verbs, Zod validation, 404/204 mapping, Drizzle-direct under the hood) | Never. Regenerate. |\n| `<Entity>.form.tsx` | React form using `useEntityForm` + the entity constants. **OPT-IN at project level:** add `formFile()` to `generators` in `metaobjects.config.ts`. Opt out per-entity via `@emitForm: false`. | Never. Regenerate. |\n| `index.ts` | Barrel re-exporting every entity file | Never. Regenerate. |\n\nFor business logic the generator doesn't cover, create a SIBLING file: `<Entity>.extra.ts` for query/route helpers, or any file you like in your apps directory. Import the constants from the generated `<Entity>.ts`.\n\n### Stock route mounting\n\n```ts\nimport { subscriberRoutes } from \"@your-pkg/database/generated/Subscriber.routes\";\nfastify.register(subscriberRoutes, { prefix: \"/api\" });\n```\n\nThat mounts: `GET /api/subscribers`, `GET /api/subscribers/:id`, `POST /api/subscribers`, `PATCH /api/subscribers/:id`, `DELETE /api/subscribers/:id`. Pagination via `?limit=` & `?offset=`. Validation via the generated Zod schemas. Drizzle calls under the hood.\n\n### Mixing custom routes alongside generated\n\n```ts\nimport { db, subscribers } from \"@your-pkg/database\";\nimport { Subscriber, SubscriberInsertSchema } from \"@your-pkg/database/generated/Subscriber\";\nimport { eq } from \"drizzle-orm\";\n\nfastify.post(\"/subscribe\", async (req, reply) => {\n // Generated Zod schema validates the body \u2014 same schema the API route uses.\n const parsed = SubscriberInsertSchema.safeParse(req.body);\n if (!parsed.success) return reply.code(400).send({ issues: parsed.error.issues });\n\n // Drizzle's typed accessors are already TS-safe; no need for indirection.\n const existing = await db.select().from(subscribers).where(eq(subscribers.email, parsed.data.email)).get();\n if (existing) return reply.code(409).send({ error: \"Already subscribed\" });\n\n const [row] = await db.insert(subscribers).values(parsed.data).returning();\n // ... your business logic (analytics, Loops/Mailchimp, navigation, etc.)\n return reply.code(201).send(row);\n});\n```\n\nThe fact that every metadata-derived value flows from `Subscriber` / `SubscriberInsertSchema` / the typed `subscribers` table is what makes rename-the-field-in-metadata-and-regen safe.\n\n### Hand-written form using `useEntityForm`\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema, type Subscriber as Row } from './generated/Subscriber';\n\nexport function SubscribeForm() {\n const form = useEntityForm(Subscriber, SubscriberInsertSchema);\n const { handleSubmit, formState: { errors } } = form;\n\n return (\n <form onSubmit={handleSubmit(/* your onSubmit */)} className=\"your-design-system\">\n <label>{Subscriber.email.label}</label>\n <input {...form.input.email} />\n {errors.email && <span>{errors.email.message}</span>}\n\n <label>{Subscriber.firstName.label}</label>\n <input {...form.input.firstName} />\n {errors.firstName && <span>{errors.firstName.message}</span>}\n\n <button type=\"submit\">Subscribe</button>\n </form>\n );\n}\n```\n\nSpread `form.input.<field>` \u2014 it carries name, type, placeholder, rules, aria-label automatically. No magic strings.\n\n## Meta Forge additions\n\n### `@forge*` attribute namespace\n\nProvenance and confidence concerns expressed as inline attributes on any metadata child. Names use camelCase (no separator).\n\nMost common:\n- `@forgeConfidence` (double 0..1) \u2014 confidence the record is correct\n- `@forgeSource` (string) \u2014 `human` | `claude` | `ts-ast` | `drizzle` | ...\n- `@forgePrimaryLocation` (string) \u2014 file path for an entity\n- `@forgeRationale` (string, decision only) \u2014 why this decision\n- `@forgeAlternatives` (stringarray, decision only) \u2014 alternatives considered\n\nFull inventory in `packages/sdk/FORGE-METADATA.md`.\n\n### New top-level types\n\nRegistered by `@metaobjectsdev/sdk` into the TypeRegistry:\n\n| Type | Purpose |\n|---|---|\n| `decision` | Architectural or design decision |\n| `principle` | Design principle (advisory/enforced) |\n| `convention` | Coding/structural convention |\n| `glossary` | Domain-term definition |\n| `failure` | Recorded failure mode |\n\nThese coexist with `object` children in the same package files. `meta gen` and `meta migrate` only consume `object`; the descriptive types are context for AI tooling and don't drive codegen.\n\n## File layout\n\n```\nmetaobjects/\n\u251C\u2500\u2500 meta.common.json shared base fields/validators (optional)\n\u251C\u2500\u2500 meta.<domain>.json your entity packages(s)\n\u2514\u2500\u2500 _pending/<pkg>.json proposed packages awaiting review\n\n.metaobjects/\n\u251C\u2500\u2500 config.json static project state\n\u251C\u2500\u2500 migrations/ written by meta migrate\n\u2514\u2500\u2500 .gen-state/ codegen merge base (gitignored)\n\nmetaobjects.config.ts generator wiring (committed)\n```\n\n## Worked example\n\n```json\n{\n \"metadata\": {\n \"package\": \"myapp\",\n \"children\": [\n {\n \"object\": {\n \"name\": \"User\",\n \"subType\": \"entity\",\n \"@forgeConfidence\": 0.95,\n \"@forgeSource\": \"human\",\n \"@forgePrimaryLocation\": \"src/db/users.schema.ts\",\n \"children\": [\n {\"field\": {\"name\": \"id\", \"extends\": \"common::id\"}},\n {\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"@column\": \"email_address\",\n \"children\": [{\"validator\": {\"subType\": \"required\"}}]\n }},\n {\"identity\": {\"name\": \"pk\", \"subType\": \"primary\", \"@fields\": [\"id\"], \"@generation\": \"increment\"}}\n ]\n }\n },\n {\n \"decision\": {\n \"name\": \"useTanstackQuery\",\n \"subType\": \"global\",\n \"@forgeConfidence\": 0.9,\n \"@forgeSource\": \"human\",\n \"@forgeRationale\": \"Real-time invalidation matters for live game state.\",\n \"@forgeAlternatives\": [\"swr\", \"redux-toolkit-query\"]\n }\n }\n ]\n }\n}\n```\n\n## Authoring guidance\n\n| Situation | Action |\n|---|---|\n| Adding a field to an existing entity | Edit the `object`'s `children`; append a `field` node, then `meta gen` |\n| New entity in an existing domain | Append an `object` to the appropriate package file, then `meta gen` |\n| Renaming an entity or field | Edit the metadata, regenerate; TS will surface every stale consumer of the constants |\n| New REST resource | Already done \u2014 `meta gen` produced `<Entity>.routes.ts`. Just `fastify.register(...)` it |\n| Custom business logic (Stripe webhook, side-effects, auth flows) | Hand-write a route/handler that imports the generated constants + `om()` |\n| Architectural choice affecting how entities are built | Add a `decision` with `@forgeRationale` + `@forgeAlternatives` |\n| Coding convention | Add a `convention` with `@forgePatternDescription` + `@forgeAppliesTo` |\n| Domain term | Add a `glossary` entry with `@forgeTerm` + `@forgeDefinition` |\n\n## Deeper references\n\n- `packages/metadata/METAMODEL.md` \u2014 full metamodel reference\n- `packages/sdk/FORGE-METADATA.md` \u2014 full `@forge*` inventory + MetaObjects layout details\n- `docs/strategy/2026-05-12-v0.3-ai-first-metadata-loading.md` \u2014 current strategy (v0.3 vocab, packages, AI-first loading)\n";
|
|
5
|
+
export declare const AGENT_DOCS_BODY = "# Meta Forge \u2014 agent reference\n\nThis file is scaffolded by `meta init` and lives alongside your `metaobjects/` records. It teaches AI coding assistants (Claude Code, Codex, etc.) how to read and modify MetaObjects metadata correctly. Refresh after CLI updates with `meta init --refresh-docs`.\n\n## Five working principles (read first)\n\nThese shape every interaction with a metaobjects-driven project. Follow them when you author metadata, write hand-coded business logic, or review someone else's work.\n\n### 1. If it's pattern-derivable from metadata, generate it. Never hand-write boilerplate.\n\nThe metaobjects raison d'\u00EAtre is that anything the metadata fully describes \u2014 schemas, FK references, basic CRUD, query helpers, Zod validators, route handlers, RHF rules, form fields \u2014 should be produced by codegen, not hand-typed. If you find yourself hand-writing something the metadata already knows about, stop and use the generated artifact.\n\nThe first version of the trainer website's database layer had hand-written Drizzle schemas, Zod schemas, and CRUD endpoints. Every one of those is now generated. The hand-written code that remains is real business logic (Stripe webhooks, Loops integration, custom auth flows) \u2014 things the generator genuinely cannot derive.\n\nWhen you're about to add a new field or entity: edit `metaobjects/*.json` and re-run `meta gen`. Don't reach for the generated file directly.\n\n### 2. Use the generated constants. Never use magic strings that touch metadata.\n\nAfter `meta gen`, each entity file exports a rich metadata-constants block. Each non-dollar-prefixed key is a per-field object that carries everything a consumer might need (name, label, view, html input type, placeholder, RHF validation rules):\n\n```ts\nexport const Subscriber = {\n $entity: \"Subscriber\", // entity name string\n $table: \"subscribers\", // SQL table name\n $path: \"/subscribers\", // REST resource path\n\n email: {\n name: \"email\", // field name string (use for filters, register())\n label: \"Email Address\", // humanized fallback or @label override\n view: \"text\", // MetaView subtype\n htmlType: \"email\", // optional; maps view \u2192 HTML <input type=>\n placeholder: \"you@example.com\", // optional; only when @placeholder is set on the view\n helpText: \"We never share this.\", // optional; only when @helpText is set\n rules: { // optional; derived from validator children\n required: \"Email is required\",\n maxLength: { value: 255, message: \"Too long\" },\n pattern: { value: /.../, message: \"Invalid email\" },\n },\n },\n firstName: { name: \"firstName\", label: \"First Name\", view: \"text\", htmlType: \"text\", rules: { required: \"First Name is required\" } },\n // ...\n} as const;\n```\n\n**Use them everywhere \u2014 in both generated AND hand-written code:**\n\n```tsx\n// \u2717 Don't:\n<input name=\"email\" type=\"email\" placeholder=\"Email\" />\n\n// \u2713 Do:\n<input\n type={Subscriber.email.htmlType}\n name={Subscriber.email.name}\n placeholder={Subscriber.email.placeholder}\n aria-label={Subscriber.email.label}\n/>\n```\n\nRename a field in `metaobjects/` and re-gen \u2014 TypeScript catches every stale reference.\n\n**Special case \u2014 Drizzle column access:** when you're already inside Drizzle's typed builder, just use the column properties directly. Drizzle's table-const types are themselves derived from metadata, so `weeks.programId` is already TS-safe:\n\n```ts\n// \u2713 Use Drizzle's typed accessor directly \u2014 no constants needed here:\ndb.select().from(weeks).where(eq(weeks.programId, X))\n\n// \u2717 Don't do this \u2014 it's redundant indirection:\ndb.select().from(weeks).where(eq(weeks[Week.programId.name], X))\n```\n\nUse the constants when you need a STRING (filter object keys, registration arguments, REST paths, labels). Use Drizzle properties directly when the type system already does the work.\n\n### 3. Forms: spread `form.input.<field>` from useEntityForm. One line per input.\n\nFor React forms, use `useEntityForm` from `@metaobjectsdev/react`. It returns the standard React Hook Form surface plus a pre-bound `.input` accessor \u2014 one entry per field, ready to spread onto an `<input>`:\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema } from './generated/Subscriber';\n\nconst form = useEntityForm(Subscriber, SubscriberInsertSchema);\n\n<label>{Subscriber.email.label}</label>\n<input {...form.input.email} /> // \u2190 type, placeholder, name, rules, aria-label all spread automatically\n```\n\nFor non-`<input>` controls (textarea, select), the `type` attr is omitted from `form.input.X` \u2014 pick the right element yourself.\n\nThe same Zod schema (`SubscriberInsertSchema`) validates on the server (in Fastify routes) and on the client (via the resolver). One schema, two surfaces, zero drift.\n\n### 4. Routes: use the generated `<Entity>.routes.ts` for stock CRUD. Hand-write only what's custom.\n\n`meta gen` emits a per-entity routes file that mounts the 5 standard verbs via `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify`. The runtime is plain Drizzle + Zod \u2014 no extra ORM.\n\nFor custom flows (Stripe webhooks, side effects, auth-gated actions), hand-write the route \u2014 but import the entity constants + generated Zod schema. The boilerplate (CRUD, validation, 404 mapping, pagination) lives in the helper; your hand-written code is just the business logic.\n\n**Auth pattern:** install a plugin-level Fastify `preHandler` hook at the top of your route plugin. The hook applies to every route registered after it \u2014 both hand-written handlers AND metaobjects-generated routes via the `routeOptions` field. Beats sprinkling `if (!auth(...)) return;` at the top of every handler.\n\n### 5. Hand-coded code is always available, but coexists with generated code.\n\nGenerated code does the boilerplate. Hand-coded code does the business logic. They live in the same project, the same package, sometimes the same file. The hand-coded code consumes the generated constants and generated Zod schemas \u2014 it never duplicates schema, never hard-codes paths, never declares its own validators that metadata could declare.\n\nConcrete pattern from the trainer website:\n- Generated `Subscriber.routes.ts` registers GET / GET-by-id / POST / PATCH / DELETE on `/api/subscribers`.\n- Hand-written `apps/api/src/routes/subscribers.ts` keeps `POST /subscribe` \u2014 the custom endpoint with the Loops side-effect.\n- Both registered with `fastify.register()`. Both validate via `SubscriberInsertSchema`. Both use `Subscriber.email.name` / etc. Neither knows the other exists.\n\n## Metaobjects metamodel \u2014 quick rules\n\nThe format used by `metaobjects/*.json` is **metaobjects metadata**, a cross-language standard. Eight base types:\n\n| Type | Purpose |\n|---|---|\n| `metadata` | Root document wrapper |\n| `object` | An entity (table/record) |\n| `field` | A property on an object |\n| `attr` | Named scalar/array decoration on any parent |\n| `validator` | A validation rule |\n| `view` | A UI control kind |\n| `identity` | A primary/secondary key |\n| `relationship` | An association between objects |\n\n### Two most-violated rules\n\n1. **Attribute uniqueness.** Within a single parent metadata node, all attribute names must be unique. You cannot have two `attr` children both named `alternative`. For multi-value, use a single `stringarray` attr: `\"@alternatives\": [\"a\", \"b\", \"c\"]`.\n\n2. **Inline `@<name>` and `attr` child are the same thing.** `\"@maxLength\": 50` is shorthand for `{\"attr\": {\"name\": \"maxLength\", \"subType\": \"int\", \"value\": \"50\"}}`. The parser converts inline form into attr children. Don't use both forms for the same attribute name on the same parent.\n\n### Object subtypes (v0.3)\n\n- `base` \u2014 abstract template (no runtime semantics)\n- `entity` \u2014 persistent record; should have a primary identity\n- `value` \u2014 value-object; equality by content; must NOT have a primary identity\n\nJava-runtime strategies (pojo / map / proxy) belong on `@javaRuntime`, not in `subType`.\n\n### Reserved structural keys (NOT attributes)\n\n`name`, `subType`, `package`, `extends`, `isAbstract`, `children`, `merge`, `value`.\n\nThe v0.2 keys (`super`, `overlay`, `override`, `isInterface`, `implements`) are **gone**. The current parser will reject them. Use:\n- `extends:` instead of `super:` for the supertype reference\n- `merge: true` instead of `overlay: true` / `override: true` for in-place modification\n- `@isAbstract: true` instead of `isInterface: true` (multiple inheritance is not supported)\n\n### Package paths and inheritance\n\n- Package segments separated by `::` \u2014 `acme::common::id`\n- Relative references in `extends:` \u2014 `..::common::id` means \"go up to parent package, descend into `common::id`\". Relative forms (`..::` parent-relative, leading `::` root-absolute) are a **YAML-authoring affordance only**; canonical JSON must be fully-qualified (a relative ref in JSON is rejected with `ERR_RELATIVE_REF_IN_CANONICAL`).\n- Cross-file resolution works as long as all files are passed to Loader (or live in the same `metaobjects/` directory)\n\n### Two special intercepted attrs (parser-routed)\n\n- `@isArray` \u2192 marks a field as a collection\n- `@isAbstract` \u2192 marks a node as abstract (inheritable but not instantiable)\n\n## Validators \u2014 two layers\n\nValidators can attach in two places, and they compose:\n\n**Field-level validators** describe what makes the *stored value* valid. They survive across UI, API, batch import, manual SQL \u2014 anywhere data enters the system. The generated Zod `<Entity>InsertSchema` encodes these.\n\n```json\n{\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"required\"}},\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^[^@]+@[^@]+\\\\.[^@]+$\"}}\n ]\n}}\n```\n\n**View-level validators** describe what makes user *input* valid in a specific UI surface \u2014 possibly stricter, possibly with different messages, possibly format-specific. They run client-side in generated forms. They do NOT necessarily reject the stored value if it's already in the DB.\n\n```json\n{\"field\": {\"name\": \"phone\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^\\\\+?[0-9]+$\"}},\n {\"view\": {\"subType\": \"text-input\", \"@label\": \"Phone\",\n \"children\": [\n {\"validator\": {\"subType\": \"length\", \"@min\": 7, \"@max\": 20,\n \"@message\": \"Phone must be 7-20 digits\"}}\n ]\n }}\n ]\n}}\n```\n\nRule of thumb: rules that protect data integrity \u2192 field. Rules that improve input UX \u2192 view.\n\n## metaobjects.config.ts \u2014 generator wiring (project root)\n\n`meta gen` reads `metaobjects.config.ts` at the project root. This is where you declare which generators run and their options. It is TypeScript, type-checked, and imported via `jiti` at run time.\n\n```ts\nimport { defineConfig } from \"@metaobjectsdev/cli\";\nimport {\n entityFile, queriesFile, routesFile, /* formFile, */ barrel,\n} from \"@metaobjectsdev/codegen-ts/generators\";\n\nexport default defineConfig({\n outDir: \"packages/database/src/generated\",\n extStyle: \"none\",\n dbImport: \"../index\",\n dialect: \"sqlite\",\n generators: [\n entityFile(),\n queriesFile(),\n routesFile(),\n // formFile(), // opt-in: emits stock React forms per entity\n barrel(),\n ],\n});\n```\n\n3rd-party generator example: `import { tanstackQuery } from \"@metaobjectsdev/codegen-ts-tanstack\"; // then add tanstackQuery({ ... }) to the generators array`\n\nFilters live on the generator entry: `routesFile({ filter: e => e.name !== \"AuditLog\" })`\n\n`.metaobjects/config.json` is unchanged \u2014 it still holds static project state (schema_version, pending_in_git, confidence_thresholds). Generator wiring belongs in `metaobjects.config.ts` so TypeScript can type-check the imports.\n\n## Generated hooks + grids (TanStack)\n\nWhen `tanstackQuery()` is in your `metaobjects.config.ts`, every entity gets `<Entity>.hooks.ts` with a query-key factory + `useEntity`, `useEntities`, `useCreate`, `useUpdate`, `useDelete` hooks. When `tanstackGrid()` is in the config, entities with a `layout[dataGrid]` child also get `<Entity>.columns.tsx`.\n\n```tsx\nimport { usePrograms, useCreateProgram } from \"@your-pkg/database/generated/Program.hooks\";\nimport { programDefaultColumns, programDefaultGrid } from \"@your-pkg/database/generated/Program.columns\";\nimport { EntityGrid } from \"@metaobjectsdev/tanstack\";\n\nconst { data, isLoading } = usePrograms();\nconst create = useCreateProgram({ onSuccess: () => navigate(\"/programs\") });\n\n<EntityGrid\n columns={programDefaultColumns}\n grid={programDefaultGrid}\n data={data ?? []}\n isLoading={isLoading}\n onRowClick={(row) => navigate(`/admin/programs/${row.id}`)}\n/>\n```\n\n**Provider setup.** Wrap your app with `<EntityFetcherProvider value={fetcher}>` (supplies the HTTP fetcher to all generated hooks). For an admin subtree with different auth, wrap a second time inside: `<EntityFetcherProvider value={adminFetch}>...</EntityFetcherProvider>` overrides the outer one.\n\n**Metadata layer \u2014 grid definition:**\n\n```jsonc\n{ \"layout\": {\n \"subType\": \"dataGrid\",\n \"name\": \"default\",\n \"@pageSize\": 25,\n \"@defaultSortField\": \"createdAt\",\n \"@defaultSortOrder\": \"desc\",\n \"@filterable\": true,\n \"@columns\": [\"email\", \"firstName\", \"subscribed\", \"createdAt\"]\n}}\n```\n\nThe `@columns` attr is a flat string array listing fields to display. Per-column rendering comes from each field's own `view` subtype (the same one that drives forms); sortability comes from the field's `@sortable` attr; width belongs in app CSS. There are no nested per-column children \u2014 just `@columns`.\n\n**Cell renderers.** Field rendering inside grids comes from each field's own `view` subtype (the same one that drives forms). Override defaults app-wide with `<CellRendererProvider value={{ currency: ({ getValue }) => <Money value={getValue()} /> }}>`.\n\n**Per-entity opt-out.** `@emitTanstack: false` on an entity skips both hooks and columns.\n\n## Filtering generated lists\n\nMark filterable fields in metadata with `@filterable: true`:\n\n```jsonc\n{ \"field\": { \"name\": \"email\", \"subType\": \"string\", \"@filterable\": true } }\n```\n\nThe generated `useSubscribers(filter)` hook accepts a typed filter:\n\n```tsx\nconst { data } = useSubscribers({\n email: { like: \"amy@%\" },\n subscribed: true,\n sort: \"createdAt:desc\",\n limit: 25,\n});\n```\n\nURL sent: `/subscribers?filter[email][like]=amy@%25&filter[subscribed]=true&sort=createdAt:desc&limit=25`\n\n**Leading wildcards are rejected by default.** The generated `<Entity>FilterAllowlist` ships `leadingWildcard: false` on every field, so a `like` pattern starting with `%` (e.g. `\"%@example.com\"`) is a 400 `filter.leading_wildcard_disallowed` \u2014 an unanchored LIKE defeats index usage, so it is fail-closed. To opt a field in, hand-edit that field's entry in the generated allowlist to `leadingWildcard: true` (hand edits inside generated files survive regeneration via the three-way merge). This gate is TypeScript-only; other ports' generated APIs do not enforce it.\n\n**Operators by field subtype:**\n- String: `eq, ne, in, like, isNull`\n- Number/date: `eq, ne, gt, gte, lt, lte, in, isNull`\n- Boolean: `eq, isNull`\n\nIllegal combinations like `useSubscribers({ subscribed: { gte: true } })` fail to compile (booleans don't support `gte`).\n\n**Per-grid preset filter** via layout `@filter`:\n\n```jsonc\n{ \"layout\": { \"subType\": \"dataGrid\", \"name\": \"active\",\n \"@filter\": { \"subscribed\": true },\n \"@columns\": [\"email\", \"firstName\", \"subscribed\"] }}\n```\n\nGenerates `subscriberActiveFilter` const consumable in pages. Compose with ad-hoc filters via object spread.\n\n## Projections (read models with joined/aggregated columns)\n\nWhen a list needs computed columns (counts, sums, joined fields), create a **projection** \u2014 an entity that extends a base entity but reads from a SQL view:\n\n```json\n// metaobjects/meta.commerce.json (inline with Program)\n{\n \"object\": {\n \"name\": \"ProgramSummary\",\n \"subType\": \"entity\",\n \"extends\": \"Program\",\n \"children\": [\n { \"source\": { \"subType\": \"rdb\", \"@kind\": \"view\", \"@table\": \"v_program_summary\" } },\n { \"field\": { \"name\": \"weekCount\", \"subType\": \"int\", \"children\": [\n { \"origin\": { \"subType\": \"aggregate\",\n \"@agg\": \"count\", \"@of\": \"Week.id\", \"@via\": \"Program.weeks\" }}\n ]}},\n { \"identity\": { \"subType\": \"primary\", \"name\": \"id\", \"@fields\": \"id\" } }\n ]\n }\n}\n```\n\n`meta gen` produces a read-only `useProgramSummaries(filter)` hook, a SQL view DDL in the migration, and a read-only GET-only route.\n\n**Aggregate vocabulary** (`origin.aggregate @agg`): `count`, `sum`, `avg`, `min`, `max`; plus `any`/`all` (a boolean predicate quantifier over a required `@filter` \u2014 no `@of`; \"did any/every related row match?\"), and `collect` (an array rollup of `@of` \u2014 the field must be `isArray: true`; `@distinct` dedupes, `@orderBy` sets element order).\n\n**Other read-model origins**: `origin.computed` \u2014 a row-level value from the base row's own fields via a structured `@expr` tree (e.g. `{ \"op\": \"isNotNull\", \"arg\": { \"field\": \"payloadJson\" } }` \u2192 a boolean, to avoid shipping a heavy column); `origin.first` \u2014 the single related row picked by `@orderBy` along `@via`, projecting `@of` (e.g. \"the latest child's status\"; the field must not be `@required` \u2014 an empty related set yields null).\n\n**Multi-level via paths** are supported: `@via: \"Program.weeks.workouts\"` builds a 2-level JOIN tree.\n\n**For pages that need a full nested tree** (e.g., Program \u2192 Weeks \u2192 Workouts \u2192 Exercises), use 4 entity hooks with Project D's filter syntax for batched lookups (no projection needed \u2014 flat hooks + client-side stitching is enough):\n\n```tsx\nconst { data: weeks } = useWeeks({ programId, sort: \"weekNumber:asc\" });\nconst weekIds = weeks?.map((w) => w.id) ?? [];\nconst { data: workouts } = useWorkouts(\n weekIds.length ? { weekId: { in: weekIds } } : undefined,\n);\n```\n\n## Currency fields\n\nDeclare a money field with `subType: \"currency\"`:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"USD\" } }\n```\n\nStorage stays as integer minor units (cents for USD). The generated `<Entity>` constants block carries `view`, `currency`, `locale` so admin grids auto-format prices.\n\n**Imports \u2014 use sub-paths in browser code:**\n\n```tsx\nimport { formatCurrency } from \"@metaobjectsdev/runtime-web\";\nimport { CurrencyInput } from \"@metaobjectsdev/react\";\n```\n\n**Display:**\n\n```tsx\n<span>{formatCurrency(program.priceCents)}</span> // $15.00\n<span>{formatCurrency(p.amountCents, \"EUR\", \"de-DE\")}</span> // 15,00 \u20AC\n```\n\n**Form input:**\n\n```tsx\n<CurrencyInput value={priceCents} onChange={setPriceCents} currency=\"USD\" />\n```\n\nUser types `15.99` \u2192 component emits `1599` to `onChange` on blur. Wire format is always integer cents.\n\n**Locale override** via a `view[currency]` child:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"EUR\",\n \"children\": [{ \"view\": { \"subType\": \"currency\", \"@locale\": \"de-DE\" } }]\n}}\n```\n\nCurrency code lives on the field; locale lives on the view.\n\n## Generated artifacts \u2014 what `meta gen` produces\n\nAfter `meta gen`, you get one barrel + per-entity files in your configured `outDir` (default `packages/database/src/generated/`):\n\n| File | What's in it | When to touch by hand |\n|---|---|---|\n| `<Entity>.ts` | Drizzle table, relations(), inferred types, Zod insert/update schemas, and the rich `<Entity>` constants block (per-field objects with name, label, view, htmlType, rules, etc.) | Never. Regenerate. |\n| `<Entity>.queries.ts` | Typed query helpers (`findUserById`, `listUsers`, `createUser`, ...) using prepared statements | Never. Regenerate. |\n| `<Entity>.routes.ts` | Fastify CRUD plugin delegating to `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify` (5 verbs, Zod validation, 404/204 mapping, Drizzle-direct under the hood) | Never. Regenerate. |\n| `<Entity>.form.tsx` | React form using `useEntityForm` + the entity constants. **OPT-IN at project level:** add `formFile()` to `generators` in `metaobjects.config.ts`. Opt out per-entity via `@emitForm: false`. | Never. Regenerate. |\n| `index.ts` | Barrel re-exporting every entity file | Never. Regenerate. |\n\nFor business logic the generator doesn't cover, create a SIBLING file: `<Entity>.extra.ts` for query/route helpers, or any file you like in your apps directory. Import the constants from the generated `<Entity>.ts`.\n\n### Stock route mounting\n\n```ts\nimport { subscriberRoutes } from \"@your-pkg/database/generated/Subscriber.routes\";\nfastify.register(subscriberRoutes, { prefix: \"/api\" });\n```\n\nThat mounts: `GET /api/subscribers`, `GET /api/subscribers/:id`, `POST /api/subscribers`, `PATCH /api/subscribers/:id`, `DELETE /api/subscribers/:id`. Pagination via `?limit=` & `?offset=`. Validation via the generated Zod schemas. Drizzle calls under the hood.\n\n### Mixing custom routes alongside generated\n\n```ts\nimport { db, subscribers } from \"@your-pkg/database\";\nimport { Subscriber, SubscriberInsertSchema } from \"@your-pkg/database/generated/Subscriber\";\nimport { eq } from \"drizzle-orm\";\n\nfastify.post(\"/subscribe\", async (req, reply) => {\n // Generated Zod schema validates the body \u2014 same schema the API route uses.\n const parsed = SubscriberInsertSchema.safeParse(req.body);\n if (!parsed.success) return reply.code(400).send({ issues: parsed.error.issues });\n\n // Drizzle's typed accessors are already TS-safe; no need for indirection.\n const existing = await db.select().from(subscribers).where(eq(subscribers.email, parsed.data.email)).get();\n if (existing) return reply.code(409).send({ error: \"Already subscribed\" });\n\n const [row] = await db.insert(subscribers).values(parsed.data).returning();\n // ... your business logic (analytics, Loops/Mailchimp, navigation, etc.)\n return reply.code(201).send(row);\n});\n```\n\nThe fact that every metadata-derived value flows from `Subscriber` / `SubscriberInsertSchema` / the typed `subscribers` table is what makes rename-the-field-in-metadata-and-regen safe.\n\n### Hand-written form using `useEntityForm`\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema, type Subscriber as Row } from './generated/Subscriber';\n\nexport function SubscribeForm() {\n const form = useEntityForm(Subscriber, SubscriberInsertSchema);\n const { handleSubmit, formState: { errors } } = form;\n\n return (\n <form onSubmit={handleSubmit(/* your onSubmit */)} className=\"your-design-system\">\n <label>{Subscriber.email.label}</label>\n <input {...form.input.email} />\n {errors.email && <span>{errors.email.message}</span>}\n\n <label>{Subscriber.firstName.label}</label>\n <input {...form.input.firstName} />\n {errors.firstName && <span>{errors.firstName.message}</span>}\n\n <button type=\"submit\">Subscribe</button>\n </form>\n );\n}\n```\n\nSpread `form.input.<field>` \u2014 it carries name, type, placeholder, rules, aria-label automatically. No magic strings.\n\n## Meta Forge additions\n\n### `@forge*` attribute namespace\n\nProvenance and confidence concerns expressed as inline attributes on any metadata child. Names use camelCase (no separator).\n\nMost common:\n- `@forgeConfidence` (double 0..1) \u2014 confidence the record is correct\n- `@forgeSource` (string) \u2014 `human` | `claude` | `ts-ast` | `drizzle` | ...\n- `@forgePrimaryLocation` (string) \u2014 file path for an entity\n- `@forgeRationale` (string, decision only) \u2014 why this decision\n- `@forgeAlternatives` (stringarray, decision only) \u2014 alternatives considered\n\nFull inventory in `packages/sdk/FORGE-METADATA.md`.\n\n### New top-level types\n\nRegistered by `@metaobjectsdev/sdk` into the TypeRegistry:\n\n| Type | Purpose |\n|---|---|\n| `decision` | Architectural or design decision |\n| `principle` | Design principle (advisory/enforced) |\n| `convention` | Coding/structural convention |\n| `glossary` | Domain-term definition |\n| `failure` | Recorded failure mode |\n\nThese coexist with `object` children in the same package files. `meta gen` and `meta migrate` only consume `object`; the descriptive types are context for AI tooling and don't drive codegen.\n\n## File layout\n\n```\nmetaobjects/\n\u251C\u2500\u2500 meta.common.json shared base fields/validators (optional)\n\u251C\u2500\u2500 meta.<domain>.json your entity packages(s)\n\u2514\u2500\u2500 _pending/<pkg>.json proposed packages awaiting review\n\n.metaobjects/\n\u251C\u2500\u2500 config.json static project state\n\u251C\u2500\u2500 migrations/ written by meta migrate\n\u2514\u2500\u2500 .gen-state/ codegen merge base (gitignored)\n\nmetaobjects.config.ts generator wiring (committed)\n```\n\n## Worked example\n\n```json\n{\n \"metadata\": {\n \"package\": \"myapp\",\n \"children\": [\n {\n \"object\": {\n \"name\": \"User\",\n \"subType\": \"entity\",\n \"@forgeConfidence\": 0.95,\n \"@forgeSource\": \"human\",\n \"@forgePrimaryLocation\": \"src/db/users.schema.ts\",\n \"children\": [\n {\"field\": {\"name\": \"id\", \"extends\": \"common::id\"}},\n {\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"@column\": \"email_address\",\n \"children\": [{\"validator\": {\"subType\": \"required\"}}]\n }},\n {\"identity\": {\"name\": \"pk\", \"subType\": \"primary\", \"@fields\": [\"id\"], \"@generation\": \"increment\"}}\n ]\n }\n },\n {\n \"decision\": {\n \"name\": \"useTanstackQuery\",\n \"subType\": \"global\",\n \"@forgeConfidence\": 0.9,\n \"@forgeSource\": \"human\",\n \"@forgeRationale\": \"Real-time invalidation matters for live game state.\",\n \"@forgeAlternatives\": [\"swr\", \"redux-toolkit-query\"]\n }\n }\n ]\n }\n}\n```\n\n## Authoring guidance\n\n| Situation | Action |\n|---|---|\n| Adding a field to an existing entity | Edit the `object`'s `children`; append a `field` node, then `meta gen` |\n| New entity in an existing domain | Append an `object` to the appropriate package file, then `meta gen` |\n| Renaming an entity or field | Edit the metadata, regenerate; TS will surface every stale consumer of the constants |\n| New REST resource | Already done \u2014 `meta gen` produced `<Entity>.routes.ts`. Just `fastify.register(...)` it |\n| Custom business logic (Stripe webhook, side-effects, auth flows) | Hand-write a route/handler that imports the generated constants + `om()` |\n| Architectural choice affecting how entities are built | Add a `decision` with `@forgeRationale` + `@forgeAlternatives` |\n| Coding convention | Add a `convention` with `@forgePatternDescription` + `@forgeAppliesTo` |\n| Domain term | Add a `glossary` entry with `@forgeTerm` + `@forgeDefinition` |\n\n## Deeper references\n\n- `packages/metadata/METAMODEL.md` \u2014 full metamodel reference\n- `packages/sdk/FORGE-METADATA.md` \u2014 full `@forge*` inventory + MetaObjects layout details\n- `docs/strategy/2026-05-12-v0.3-ai-first-metadata-loading.md` \u2014 current strategy (v0.3 vocab, packages, AI-first loading)\n";
|
|
6
6
|
//# sourceMappingURL=body.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AACA;;;GAGG;AACH,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AACA;;;GAGG;AACH,eAAO,MAAM,eAAe,su3BAojB3B,CAAC"}
|
package/dist/agent-docs/body.js
CHANGED
|
@@ -279,14 +279,16 @@ The generated \`useSubscribers(filter)\` hook accepts a typed filter:
|
|
|
279
279
|
|
|
280
280
|
\`\`\`tsx
|
|
281
281
|
const { data } = useSubscribers({
|
|
282
|
-
email: { like: "
|
|
282
|
+
email: { like: "amy@%" },
|
|
283
283
|
subscribed: true,
|
|
284
284
|
sort: "createdAt:desc",
|
|
285
285
|
limit: 25,
|
|
286
286
|
});
|
|
287
287
|
\`\`\`
|
|
288
288
|
|
|
289
|
-
URL sent: \`/subscribers?filter[email][like]
|
|
289
|
+
URL sent: \`/subscribers?filter[email][like]=amy@%25&filter[subscribed]=true&sort=createdAt:desc&limit=25\`
|
|
290
|
+
|
|
291
|
+
**Leading wildcards are rejected by default.** The generated \`<Entity>FilterAllowlist\` ships \`leadingWildcard: false\` on every field, so a \`like\` pattern starting with \`%\` (e.g. \`"%@example.com"\`) is a 400 \`filter.leading_wildcard_disallowed\` — an unanchored LIKE defeats index usage, so it is fail-closed. To opt a field in, hand-edit that field's entry in the generated allowlist to \`leadingWildcard: true\` (hand edits inside generated files survive regeneration via the three-way merge). This gate is TypeScript-only; other ports' generated APIs do not enforce it.
|
|
290
292
|
|
|
291
293
|
**Operators by field subtype:**
|
|
292
294
|
- String: \`eq, ne, in, like, isNull\`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body.js","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG
|
|
1
|
+
{"version":3,"file":"body.js","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAojB9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0-rc.1",
|
|
4
4
|
"description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@metaobjectsdev/metadata": "0.
|
|
59
|
+
"@metaobjectsdev/metadata": "0.22.0-rc.1",
|
|
60
60
|
"zod": "^3.23.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
@@ -17,8 +17,9 @@ for (const name of readdirSync(CORPUS)) {
|
|
|
17
17
|
const spec = JSON.parse(readFileSync(join(dir, "stack.json"), "utf8")) as {
|
|
18
18
|
servers: string[];
|
|
19
19
|
clients: string[];
|
|
20
|
+
concerns?: string[];
|
|
20
21
|
};
|
|
21
|
-
const stack = makeStack(spec.servers as never, spec.clients as never);
|
|
22
|
+
const stack = makeStack(spec.servers as never, spec.clients as never, (spec.concerns ?? []) as never);
|
|
22
23
|
const files = assemble({ contentRoot: CONTENT_ROOT, stack });
|
|
23
24
|
const expDir = join(dir, "expected");
|
|
24
25
|
rmSync(expDir, { recursive: true, force: true });
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SKILL_NAMES, SERVER_LANGS, CLIENT_FRAMEWORKS, CONCERN_TOKENS, type AssembledFile, type Stack,
|
|
5
|
+
} from "./types.js";
|
|
4
6
|
|
|
5
|
-
// Language/framework reference fragments are stack-scoped; any other
|
|
6
|
-
// (e.g. capability-checklist) is universal and always installs.
|
|
7
|
-
|
|
7
|
+
// Language/framework/concern reference fragments are stack-scoped; any other
|
|
8
|
+
// fragment (e.g. capability-checklist) is universal and always installs. Adding a
|
|
9
|
+
// future concern token to CONCERN_TOKENS (types.ts) is all that's needed to gate
|
|
10
|
+
// its matching references/<token>.md fragment here.
|
|
11
|
+
const SCOPED_FRAGMENT_TOKENS = new Set<string>([...SERVER_LANGS, ...CLIENT_FRAMEWORKS, ...CONCERN_TOKENS]);
|
|
8
12
|
|
|
9
13
|
interface ServerMeta { displayName: string; install: string; codegenCommand: string; }
|
|
10
14
|
|
|
@@ -57,17 +61,18 @@ export function assemble(opts: { contentRoot: string; stack: Stack }): Assembled
|
|
|
57
61
|
|
|
58
62
|
const refDir = join(skillDir, "references");
|
|
59
63
|
if (existsSync(refDir) && statSync(refDir).isDirectory()) {
|
|
60
|
-
// Stack-scoped: a language/framework fragment installs only when its
|
|
61
|
-
// is in the resolved stack (Stack.tokens = servers ∪ clients ∪
|
|
62
|
-
// Non-
|
|
63
|
-
// install. Keeps each skill's references/ to what the
|
|
64
|
-
// so the SKILL.md "read every references/*.md" footer
|
|
64
|
+
// Stack-scoped: a language/framework/concern fragment installs only when its
|
|
65
|
+
// token is in the resolved stack (Stack.tokens = servers ∪ clients ∪ concerns
|
|
66
|
+
// ∪ {migration}). Non-scoped fragments (e.g. capability-checklist) are
|
|
67
|
+
// universal and always install. Keeps each skill's references/ to what the
|
|
68
|
+
// project actually uses, so the SKILL.md "read every references/*.md" footer
|
|
69
|
+
// stays accurate.
|
|
65
70
|
const refs = readdirSync(refDir)
|
|
66
71
|
.filter((f) => f.endsWith(".md"))
|
|
67
72
|
.map((f) => f.replace(/\.md$/, ""))
|
|
68
73
|
.sort();
|
|
69
74
|
for (const token of refs) {
|
|
70
|
-
if (
|
|
75
|
+
if (SCOPED_FRAGMENT_TOKENS.has(token) && !stack.tokens.has(token)) continue;
|
|
71
76
|
out.push({
|
|
72
77
|
path: `.claude/skills/${skill}/references/${token}.md`,
|
|
73
78
|
contents: readFileSync(join(refDir, `${token}.md`), "utf8"),
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
SERVER_LANGS, CLIENT_FRAMEWORKS, MIGRATION_TOKEN,
|
|
3
|
-
type ServerLang, type ClientFramework, type Stack,
|
|
2
|
+
SERVER_LANGS, CLIENT_FRAMEWORKS, CONCERN_TOKENS, MIGRATION_TOKEN,
|
|
3
|
+
type ServerLang, type ClientFramework, type ConcernToken, type Stack,
|
|
4
4
|
} from "./types.js";
|
|
5
5
|
|
|
6
6
|
export interface ProjectProbe {
|
|
7
7
|
hasDep(name: string): boolean;
|
|
8
8
|
hasFileMatching(pattern: RegExp): boolean;
|
|
9
|
+
/** True if the project's declared metadata contains at least one `requirement.*` node. */
|
|
10
|
+
hasRequirementNodes(): boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function makeStack(
|
|
13
|
+
export function makeStack(
|
|
14
|
+
servers: ServerLang[],
|
|
15
|
+
clients: ClientFramework[],
|
|
16
|
+
concerns: ConcernToken[] = [],
|
|
17
|
+
): Stack {
|
|
12
18
|
const s = SERVER_LANGS.filter((x) => servers.includes(x));
|
|
13
19
|
const c = CLIENT_FRAMEWORKS.filter((x) => clients.includes(x));
|
|
14
|
-
|
|
20
|
+
const k = CONCERN_TOKENS.filter((x) => concerns.includes(x));
|
|
21
|
+
return { servers: s, clients: c, concerns: k, tokens: new Set<string>([...s, ...c, ...k, MIGRATION_TOKEN]) };
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
/** Best-effort detection from a project probe. Always overridable; a wrong guess
|
|
@@ -31,3 +38,12 @@ export function detectStack(probe: ProjectProbe): { servers: ServerLang[]; clien
|
|
|
31
38
|
|
|
32
39
|
return { servers, clients };
|
|
33
40
|
}
|
|
41
|
+
|
|
42
|
+
/** Best-effort concern detection — OBSERVED project state, never a config flag.
|
|
43
|
+
* Independent of servers/clients: a project's use of a capability doesn't depend
|
|
44
|
+
* on which server language or client framework it runs. */
|
|
45
|
+
export function detectConcerns(probe: ProjectProbe): ConcernToken[] {
|
|
46
|
+
const concerns: ConcernToken[] = [];
|
|
47
|
+
if (probe.hasRequirementNodes()) concerns.push("requirements");
|
|
48
|
+
return concerns;
|
|
49
|
+
}
|
|
@@ -4,6 +4,15 @@ export type ServerLang = (typeof SERVER_LANGS)[number];
|
|
|
4
4
|
export const CLIENT_FRAMEWORKS = ["react", "tanstack", "angular"] as const;
|
|
5
5
|
export type ClientFramework = (typeof CLIENT_FRAMEWORKS)[number];
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Opt-in capability concerns, detected from OBSERVED project state (never a config
|
|
9
|
+
* flag — a flag goes stale, observed metadata cannot). Each token gates a
|
|
10
|
+
* `references/<token>.md` fragment the same way a language/framework token does.
|
|
11
|
+
* Add a new concern here; nothing else in the assembler needs to change.
|
|
12
|
+
*/
|
|
13
|
+
export const CONCERN_TOKENS = ["requirements"] as const;
|
|
14
|
+
export type ConcernToken = (typeof CONCERN_TOKENS)[number];
|
|
15
|
+
|
|
7
16
|
/** Always-present token: schema migrations are TS-owned for every port (ADR-0015). */
|
|
8
17
|
export const MIGRATION_TOKEN = "migration";
|
|
9
18
|
|
|
@@ -21,7 +30,8 @@ export type SkillName = (typeof SKILL_NAMES)[number];
|
|
|
21
30
|
export interface Stack {
|
|
22
31
|
servers: ServerLang[]; // deduped, in SERVER_LANGS order
|
|
23
32
|
clients: ClientFramework[]; // deduped, in CLIENT_FRAMEWORKS order
|
|
24
|
-
|
|
33
|
+
concerns: ConcernToken[]; // deduped, in CONCERN_TOKENS order — observed capability usage
|
|
34
|
+
/** servers ∪ clients ∪ concerns ∪ {"migration"} — the install-selection set for reference fragments. */
|
|
25
35
|
tokens: ReadonlySet<string>;
|
|
26
36
|
}
|
|
27
37
|
|
package/src/agent-docs/body.ts
CHANGED
|
@@ -279,14 +279,16 @@ The generated \`useSubscribers(filter)\` hook accepts a typed filter:
|
|
|
279
279
|
|
|
280
280
|
\`\`\`tsx
|
|
281
281
|
const { data } = useSubscribers({
|
|
282
|
-
email: { like: "
|
|
282
|
+
email: { like: "amy@%" },
|
|
283
283
|
subscribed: true,
|
|
284
284
|
sort: "createdAt:desc",
|
|
285
285
|
limit: 25,
|
|
286
286
|
});
|
|
287
287
|
\`\`\`
|
|
288
288
|
|
|
289
|
-
URL sent: \`/subscribers?filter[email][like]
|
|
289
|
+
URL sent: \`/subscribers?filter[email][like]=amy@%25&filter[subscribed]=true&sort=createdAt:desc&limit=25\`
|
|
290
|
+
|
|
291
|
+
**Leading wildcards are rejected by default.** The generated \`<Entity>FilterAllowlist\` ships \`leadingWildcard: false\` on every field, so a \`like\` pattern starting with \`%\` (e.g. \`"%@example.com"\`) is a 400 \`filter.leading_wildcard_disallowed\` — an unanchored LIKE defeats index usage, so it is fail-closed. To opt a field in, hand-edit that field's entry in the generated allowlist to \`leadingWildcard: true\` (hand edits inside generated files survive regeneration via the three-way merge). This gate is TypeScript-only; other ports' generated APIs do not enforce it.
|
|
290
292
|
|
|
291
293
|
**Operators by field subtype:**
|
|
292
294
|
- String: \`eq, ne, in, like, isNull\`
|