Skip to content

Commit d33beb7

Browse files
authored
Merge pull request #2 from metaobjectsdev/feat/fr019-cross-port
FR-019: shared + @provided enums (all five ports) + typecheck-gate repair
2 parents 9918874 + 8bff7e4 commit d33beb7

53 files changed

Lines changed: 1520 additions & 135 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.githooks/pre-push

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env bash
2+
#
3+
# metaobjects pre-push gate.
4+
#
5+
# `bun test` transpiles per-file and does NOT typecheck, so type-broken code ships
6+
# green on the test suite while the CI `typecheck` job goes red. CI catches it, but a
7+
# direct push to `main` by an admin BYPASSES branch protection — so red lands anyway.
8+
# This hook closes that hole locally: it runs the SAME `tsc` gate CI runs and BLOCKS
9+
# the push when it is red. Loud by design — nothing red should leave your machine
10+
# quietly.
11+
#
12+
# Scope: only the TypeScript build+typecheck gate (the one local `bun test` skips).
13+
# The Java / C# / Python compile+conformance gates stay CI-only (too heavy + toolchain
14+
# -dependent for a push hook); branch protection still requires them on PRs.
15+
#
16+
# Activate (one-time, per clone): git config core.hooksPath .githooks
17+
# Bypass (emergencies only): git push --no-verify
18+
# SKIP_TS_TYPECHECK=1 git push
19+
#
20+
set -uo pipefail
21+
22+
if [ "${SKIP_TS_TYPECHECK:-}" = "1" ]; then
23+
echo "pre-push: SKIP_TS_TYPECHECK=1 set — skipping the TS typecheck gate (you asked for it)." >&2
24+
exit 0
25+
fi
26+
27+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
28+
ZERO="0000000000000000000000000000000000000000"
29+
30+
# Collect the files changed across every ref being pushed (stdin: <localref> <localsha>
31+
# <remoteref> <remotesha>). A brand-new branch (remote sha = zeros) has no remote base,
32+
# so we fail SAFE and run the gate rather than guess.
33+
changed=""
34+
run_gate=0
35+
while read -r _localref localsha _remoteref remotesha; do
36+
[ -z "${localsha:-}" ] && continue
37+
if [ "$localsha" = "$ZERO" ]; then
38+
continue # branch deletion — nothing to check
39+
fi
40+
if [ "$remotesha" = "$ZERO" ]; then
41+
run_gate=1 # new branch: no base to diff — run the gate
42+
continue
43+
fi
44+
files="$(git diff --name-only "$remotesha..$localsha" 2>/dev/null || true)"
45+
changed="$changed
46+
$files"
47+
done
48+
49+
# Skip the (slow) gate when the push touches no TypeScript sources.
50+
if [ "$run_gate" -ne 1 ]; then
51+
if printf '%s\n' "$changed" | grep -qE '^(server/typescript|client/web)/'; then
52+
run_gate=1
53+
fi
54+
fi
55+
56+
if [ "$run_gate" -ne 1 ]; then
57+
exit 0
58+
fi
59+
60+
echo "pre-push: TypeScript change detected — running the build + typecheck gate (CI parity)…" >&2
61+
echo " (skip in an emergency with: git push --no-verify)" >&2
62+
63+
# typecheck resolves cross-package types through each dep's dist/, so build first —
64+
# exactly what .github/workflows/conformance.yml does before `typecheck`.
65+
if ! ( cd "$ROOT" && bun run --filter '*' build ) >/tmp/metaobjects-prepush-build.log 2>&1; then
66+
echo "" >&2
67+
echo " ✖ pre-push BLOCKED: workspace build failed. Last lines:" >&2
68+
tail -20 /tmp/metaobjects-prepush-build.log | sed 's/^/ /' >&2
69+
echo " full log: /tmp/metaobjects-prepush-build.log" >&2
70+
exit 1
71+
fi
72+
73+
if ! ( cd "$ROOT" && bun run --filter '*' typecheck ) >/tmp/metaobjects-prepush-tsc.log 2>&1; then
74+
echo "" >&2
75+
echo " ✖ pre-push BLOCKED: tsc typecheck is RED. This is the gate CI enforces; do not push red." >&2
76+
echo "" >&2
77+
grep -E "error TS" /tmp/metaobjects-prepush-tsc.log | grep -v node_modules | sed 's/^/ /' | head -40 >&2
78+
echo "" >&2
79+
echo " full log: /tmp/metaobjects-prepush-tsc.log" >&2
80+
echo " fix the errors, or (emergencies only) re-push with --no-verify." >&2
81+
exit 1
82+
fi
83+
84+
echo "pre-push: ✓ TypeScript build + typecheck clean." >&2
85+
exit 0

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ git config hooks.denyListPath /path/to/your/private/denylist.txt
5959

6060
It blocks commits whose added lines match (`git commit --no-verify` bypasses, discouraged); the npm author email is the one allowed exception. Guard a new private name by editing that private denylist (single source of truth) — never add real names to this public repo or the committed hook.
6161

62+
**Pre-push typecheck gate** (`.githooks/pre-push`, same `core.hooksPath`): `bun test`
63+
transpiles per-file and does NOT typecheck, so type-broken code can ship green on the
64+
test suite while the CI `typecheck` job goes red — and a direct admin push to `main`
65+
bypasses branch protection. This hook closes that hole locally: when a push touches
66+
`server/typescript/` or `client/web/`, it runs the same `bun run --filter '*' build &&
67+
… typecheck` gate CI runs and **blocks the push when it is red** (~6s on a clean tree;
68+
skipped entirely for non-TS pushes). Bypass in an emergency with `git push --no-verify`
69+
or `SKIP_TS_TYPECHECK=1 git push`. The Java/C#/Python compile+conformance gates stay
70+
CI-only (still required on PRs via branch protection).
71+
6272
## Monorepo layout
6373

6474
This repo holds all implementations of the standard, organized by deployment target → language/platform → framework integration:

docs/superpowers/specs/2026-06-06-fr-019-shared-and-provided-enums-design.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,27 @@ The corpus is the oracle: TS reference green first, then each port matches.
120120

121121
## Realization status
122122

123-
- **Unimplemented.** This spec + ADR-0026 lock the decision. Implementation order:
124-
(1) loader `@provided` attr + validation + conformance fixtures (TS green);
125-
(2) TS shared-materialize + provided-reference codegen; (3) per-port fan-out
126-
(C#/Java/Kotlin/Python) against the fixtures; (4) retire `@csEnumType`.
123+
- **Shipped — all five ports.** `@provided` is registered on `field.enum` and
124+
graduated into the cross-port `expected-registry.json` (the pilot carve-out is
125+
closed). Shared-materialize + provided-reference codegen ship in TS (reference),
126+
C#, Java (`codegen-spring`), Kotlin (`codegen-kotlin`), and Python, each gated by
127+
the shared `fixtures/codegen-conformance/shared-provided-enum` oracle and its own
128+
idiomatic per-port conformance test. The positive metamodel fixture
129+
`fixtures/conformance/enum-provided-shared` pins `@provided` load + canonical
130+
round-trip cross-port, and the loader error fixture
131+
`fixtures/conformance/error-provided-not-boolean` pins `@provided: "yes"`
132+
`ERR_BAD_ATTR_VALUE` in all five ports. `@csEnumType` was never introduced
133+
(retired in design).
134+
- **Boolean-validation alignment:** adding the error fixture surfaced that the JVM
135+
loader silently coerced a non-boolean boolean-attr value (`Boolean.parseBoolean`
136+
maps any non-`"true"` string to `false`) where TS / C# / Python already reject it.
137+
`BooleanAttribute.setValueAsString` now parses strictly (accepts only
138+
case-insensitive `true`/`false`, throws otherwise — mirroring `IntAttribute`), so
139+
the parser's strict-mode catch surfaces `ERR_BAD_ATTR_VALUE`. This corrects every
140+
Java boolean attr, not just `@provided`; the full metadata + conformance + codegen
141+
suites pass with no regression.
142+
- **Follow-up (not here):** `@provided` on `object.value` (reference a hand-written
143+
value class) — same attribute, separate slice.
127144

128145
## Cross-references
129146
- [ADR-0026](../../../spec/decisions/ADR-0026-shared-and-provided-named-types.md)`@provided` as a cross-type provenance flag (enums + value objects); shared vs provided orthogonal; Option 2 upgrade path.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme::shop",
4+
"children": [
5+
{ "field.enum": { "name": "Priority", "abstract": true, "@values": ["LOW", "MEDIUM", "HIGH"] } },
6+
{ "field.enum": { "name": "Currency", "abstract": true, "@provided": true, "@values": ["USD", "EUR", "GBP"] } },
7+
{ "object.entity": { "name": "Ticket", "children": [
8+
{ "field.long": { "name": "id" } },
9+
{ "field.enum": { "name": "priority", "extends": "Priority" } },
10+
{ "field.enum": { "name": "currency", "extends": "Currency" } },
11+
{ "source.rdb": { "@table": "tickets" } },
12+
{ "identity.primary": { "name": "pk", "@fields": ["id"] } }
13+
] } },
14+
{ "object.entity": { "name": "Order", "children": [
15+
{ "field.long": { "name": "id" } },
16+
{ "field.enum": { "name": "priority", "extends": "Priority" } },
17+
{ "source.rdb": { "@table": "orders" } },
18+
{ "identity.primary": { "name": "pk", "@fields": ["id"] } }
19+
] } }
20+
]
21+
}
22+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.enum": {
7+
"name": "Priority",
8+
"package": "acme",
9+
"abstract": true,
10+
"@values": [
11+
"LOW",
12+
"HIGH"
13+
]
14+
}
15+
},
16+
{
17+
"field.enum": {
18+
"name": "Currency",
19+
"package": "acme",
20+
"abstract": true,
21+
"@provided": true,
22+
"@values": [
23+
"USD",
24+
"EUR"
25+
]
26+
}
27+
},
28+
{
29+
"object.entity": {
30+
"name": "Ticket",
31+
"children": [
32+
{
33+
"field.long": {
34+
"name": "id"
35+
}
36+
},
37+
{
38+
"field.enum": {
39+
"name": "priority",
40+
"extends": "Priority"
41+
}
42+
},
43+
{
44+
"field.enum": {
45+
"name": "currency",
46+
"extends": "Currency"
47+
}
48+
},
49+
{
50+
"identity.primary": {
51+
"@fields": [
52+
"id"
53+
]
54+
}
55+
}
56+
]
57+
}
58+
}
59+
]
60+
}
61+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{ "field.enum": { "name": "Priority", "abstract": true, "@values": ["LOW", "HIGH"] } },
6+
{ "field.enum": { "name": "Currency", "abstract": true, "@provided": true, "@values": ["USD", "EUR"] } },
7+
{
8+
"object.entity": {
9+
"name": "Ticket",
10+
"children": [
11+
{ "field.long": { "name": "id" } },
12+
{ "field.enum": { "name": "priority", "extends": "Priority" } },
13+
{ "field.enum": { "name": "currency", "extends": "Currency" } },
14+
{ "identity.primary": { "@fields": "id" } }
15+
]
16+
}
17+
}
18+
]
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_BAD_ATTR_VALUE",
5+
"source": {
6+
"format": "json",
7+
"files": [
8+
"meta.enums.json"
9+
],
10+
"jsonPath": "$['metadata.root'].children[0]['field.enum']"
11+
}
12+
}
13+
],
14+
"warnings": []
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{ "field.enum": { "name": "Priority", "abstract": true, "@provided": "yes", "@values": ["LOW", "HIGH"] } }
6+
]
7+
}
8+
}

fixtures/registry-conformance/expected-registry.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,12 @@
865865
"isArray": false,
866866
"required": false
867867
},
868+
{
869+
"name": "provided",
870+
"valueType": "boolean",
871+
"isArray": false,
872+
"required": false
873+
},
868874
{
869875
"name": "readOnly",
870876
"valueType": "boolean",

scripts/integration-test.sh

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ run_csharp() {
4646

4747
run_java() {
4848
echo "==> Java persistence conformance"
49-
( cd server/java && mvn -f integration-tests/pom.xml test ) || FAIL=1
49+
# integration-tests is EXCLUDED from the parent reactor (docker-only), so its
50+
# <version>-SNAPSHOT module dependencies are not on Maven Central — install them to
51+
# the local .m2 first (SERIALLY; a parallel -T build clobbers the shared render jar).
52+
# Without this a fresh CI checkout fails with
53+
# "Could not find artifact com.metaobjects:metaobjects-metadata:jar:<v>-SNAPSHOT".
54+
( cd server/java \
55+
&& mvn -q -DskipTests install -pl metadata,om,omdb,codegen-spring -am \
56+
&& mvn -f integration-tests/pom.xml test ) || FAIL=1
5057
}
5158

5259
run_python() {
@@ -64,7 +71,11 @@ run_python() {
6471

6572
run_kotlin() {
6673
echo "==> Kotlin persistence conformance"
67-
( cd server/java && mvn -f integration-tests-kotlin/pom.xml test ) || FAIL=1
74+
# See run_java: install the SNAPSHOT module deps to .m2 (serially) before the
75+
# docker-only integration-tests-kotlin module (also excluded from the reactor).
76+
( cd server/java \
77+
&& mvn -q -DskipTests install -pl metadata,codegen-kotlin -am \
78+
&& mvn -f integration-tests-kotlin/pom.xml test ) || FAIL=1
6879
}
6980

7081
case "$WHICH" in

0 commit comments

Comments
 (0)