test(dsl): pin the DSL surface across all eight primitives - #27
Conversation
The façade itself shipped in cdc5d42/1a536e8 — .input/.output/.policy/.mcp lifted, .as/.tool/.openapi/.client/.job/.contract bound in facade.ts, .def gone. What was missing was the reference an agent actually reads. - wiki/Actions.md gains "The fluent surface", the section its sibling Queries-And-Live-Queries.md has had since query got the same treatment: one row per member, plus why `handle` is unreachable (X_ACTION_FOREIGN) and why registerActions() names the export in place. - The six-artifacts table now names the method behind each projection — only .job() was named before. - Fix the projected route: derivePath('publishPost') is POST /api/posts/publish, never /_x/action/publish-post. The wrong path was copied verbatim into the spec, the site, the quickstart curl and the reference app's MCP README; naming.test.ts pins the real one. - Fix the introspection samples: MCP tool is publish_post, not publishPost, and both subcommands wrap the descriptor in the standard CLI envelope. Verified against the running CLI (x actions list/describe --json) and a scratch probe of all ten members, not read off the source. Co-Authored-By: Claude <noreply@anthropic.com>
The invocation core (parse → policy → handle → parse output) and the private declaration store already shipped; this closes the second authz path that survived them. - The HTTP pipeline evaluated an action's policy in its `authz` stage AND `invoke` evaluated it again. The stage runs before the handler, so it decides with `row: null` — every action declaring `row:` with a row-level rule 403'd for the row's own author and never reached the evaluation that had the row. - `RouteMeta.enforcedBy: 'pipeline' | 'handler'` says which layer decides; `toRoute` sets `'handler'` and the stage stands down. `meta.policy` stays set, so `x routes` and the manifest still name the guard. - `dev-hooks.ts` drops its action lookup — a decision it can no longer make correctly is a second authz system waiting for a caller. - `index.test.ts` now enforces what CLAUDE.md only claimed: the barrel never re-exports `defOf`/`stashDef`/`hasDef`, and no projection carries `handle` out. - New `http.test.ts` drives a row-level action over the real pipeline and counts evaluations: author 200, stranger 403, exactly one either way. Both new gates negative-proved. Co-Authored-By: Claude <noreply@anthropic.com>
defineApi, rpc, defineCatalogs, daysBetween — plus the seam the first of
them needed. defineService was already shipped; defineMail's app-side
signature was already fixed.
- action: defineApi({ actions, mutators, queries, llm }) is the app's one
boot call. Names come from export names; the returned value is also the
type rpc<Api['actions']>() is shaped from, so there is no second list.
- core: registrar.ts — the table one same-tier package reaches another
through. action cannot import query sideways, so query announces
registerQueries there and defineApi resolves it by kind. A missing
registrar is X_REGISTRAR_MISSING, never a silent skip that drops every
read; two different registrars for one kind is X_REGISTRAR_CONFLICT.
- action: createClient renamed to rpc. One name, no alias.
- i18n: defineCatalogs({ default, locales }) validates, flattens, registers
under the framework catalog and configures the supported set in one call.
currentTranslator renamed to useI18n<TCatalog>(); Translator is generic
and TranslationKey<T> makes an unknown key a compile error. Bare
Translator still defaults to string keys for ui and mail.
- time: daysBetween(from, to, zone) counts local calendar-day boundaries,
signed, DST-correct. Zone required — no ambient default, anywhere.
- app: endOfBillingPeriod moves to @postly/core; a billing period is
domain, not calendar time.
- app: catalog plurals were authored as nested { one, other } branches the
runtime never probes, so four strings rendered as a miss in production.
Moved to _one/_other leaves; the parity test that should have caught it
was vacuous and now asserts it found some.
verify 17/17 green. +39 tests. Full suite failure set byte-identical to
baseline. Reference app typecheck 307 -> 302, every one of the five symbols
now resolving.
Co-Authored-By: Claude <noreply@anthropic.com>
The code shipped earlier; the reference manual named the fluent surface for `action` and `query` only. Five primitives had a surface the manual never mentioned, so an agent reading the wiki could not find it. - job: `.enqueue()` `.as()` `.describe()` `.parse()` `.idempotencyKeyFor()`, and why `.as()` queues where an action's `.as()` runs — a job's execution surface IS the queue. `describe().steps` empty by design. - task: `.entries()` `.enqueue()` `.describe()`, the occurrence-scoped key a tick uses versus the plain key a manual fire uses, and `tz` now checked against the runtime's IANA database. Dropped two `Fields` rows that named declaration keys `TaskDefinition` does not have (`queue`, `enabled`) — both are type errors today, two lines above "Nothing else." - entity: the `$`-sigil members, and the type that explains them — `Entity = EntityCore & C`, so the entity IS its columns and `posts.name` must stay a column. Tenant resolution order written down. - mutator: `.local()` `.server()` `.conflict`, and that `.server()` routes through the action's own callable, so the offline half gets the same parse -> policy -> handle -> parse core because it IS that call. - route: the normalized descriptor — `meta()` always awaits, `budget` is always an object — plus the trap that normalization could have set, that `budget.js === undefined` still fails a hydrating `site/` route. - The-Eight-Primitives: the rule itself, once, with each primitive's surface. Two code fixes the docs pass surfaced: - `isMutator` was fail-open: it accepted any function carrying the brand, with no declaration check, so a hand-rolled look-alike counterfeited the one primitive `isAction` would have refused. Now structural, like its twin. - `packages/render/src/dsl.test.ts`: render was the only primitive package with a façade and no member-enumeration pin. Both new assertions were negative-proved — laundering the budget and adding a stray descriptor key each turn it red. Co-Authored-By: Claude <noreply@anthropic.com>
The four primitive packages (action/query/jobs/entity) already re-exported
`t`; the rule stopped there. A `defineMail` template, a `defineAppMcp` tool
and an `llm()` call each declare a schema too, and each had to reach past
its package to `@ultimat3/schema` — a second import, and the ambiguity tax
axiom 1 exists to remove.
- mail/mcp/ai re-export `t` + `Infer` verbatim from `@ultimat3/schema`.
All three already declared the dep, so no boundary changes (tier 4 → 0).
- index.test.ts per package asserts `t` is the SAME object, not a copy: a
spread would typecheck and silently freeze `schemaProvider()` at import.
Negative-proved — swapping the export for `{ ...schemaT }` turns it red.
- 5 reference-app files now take `t` from the package they declare in.
`apps/web/**/entity.ts` deliberately stays on `@ultimat3/schema`: those
declare no primitive, so schema already IS their one import.
- `@postly/mcp` drops its now-unused `@ultimat3/schema` dependency.
- `packages/{ai,mcp}/README.md` used `t` with no import at all — fixed.
- Rule stated once in `wiki/The-Eight-Primitives.md`, with why `policy`
and `route` re-export nothing: neither primitive declares a schema.
Co-Authored-By: Claude <noreply@anthropic.com>
- mutator was the one primitive whose fluent façade the dsl.test.ts pattern never enumerated — action/query/jobs/entity/policy/render all had a shape+delegation pin, mutator only had behavioral tests. - Adds it to packages/action/src/dsl.test.ts (mutator lives in the same package, built on action()): exact member set (action façade + isMutator/conflict/local/server/describeMutator), .local()/.server() delegation proofs, and a.tool().policy === a.policy for a mutator instance specifically. Co-Authored-By: Claude <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 25 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (30)
📝 WalkthroughWalkthroughThe pull request centralizes API registration, renames the typed client to ChangesAPI and primitive platform
Typed catalogs and translations
Calendar billing calculations
Primitive schema boundaries
Route and fluent API contracts
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
This PR looks ready — DSL surface pinned across all eight primitives, test plan passes. Consider applying the 🤖 Posted by developerz.ai — the maintainer agent, not a human. |
There was a problem hiding this comment.
Actionable comments posted: 22
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/action/README.md`:
- Line 9: Update the Typed RPC client example in the README to use the fluent
action instance method, replacing action.client({ baseUrl }) with
publishPost.client({ baseUrl }); alternatively document only
rpc<Api['actions']>() if that is the intended API. Keep the example consistent
with the fluent API shown later in the document.
In `@packages/action/src/define-api.test.ts`:
- Line 1: Add a 1–4 line header comment before the imports in the define-api
test module, stating its single responsibility and why the tests exist; leave
the existing imports and test implementation unchanged.
In `@packages/action/src/define-api.ts`:
- Around line 83-85: Update the API construction around defineApi’s returned
actions and queries maps so they contain only entries accepted by
registerActions() and the query registrar; do not copy every export through
mergeModules(). Preserve registered primitive entries and prevent unregistered
helper exports or duplicate names from being silently exposed or overwritten,
using registrar results or explicit module validation.
In `@packages/action/src/dsl.test.ts`:
- Around line 184-190: Add a denied-context case to the test around
defineMutatorTarget, invoking target.server with a context that lacks
authorization and asserting it rejects with the expected authorization error.
Ensure this assertion verifies the call flows through the action’s callable
invoke path rather than directly through the declared server implementation,
while preserving the existing successful-result comparison.
In `@packages/action/src/http.test.ts`:
- Around line 1-5: Shorten the header comment in
packages/action/src/http.test.ts (lines 1-5) to no more than four lines,
retaining only the module’s single responsibility and why it matters; move the
detailed authorization rationale into the relevant test or authorization block.
Apply the same header reduction in packages/cli/src/dev-hooks.ts (lines 2-8),
preserving only its single responsibility and rationale.
In `@packages/action/src/index.test.ts`:
- Around line 46-62: Update the projection assertion in the “no projection
carries the declaration out with it” test to check direct property access for
handle and def, rather than relying on Object.keys enumerability. Preserve the
existing exception for route’s handler while ensuring any handle or def value
exposed by a projection causes the test to fail.
In `@packages/ai/src/index.test.ts`:
- Around line 1-3: Add a 1–4 line module header comment before the imports in
packages/ai/src/index.test.ts (lines 1–3) describing the test file’s
responsibility and purpose. Add an equivalent responsibility-and-purpose header
before the imports in packages/mail/src/index.test.ts (lines 1–3); both files
require direct changes.
In `@packages/core/src/registrar.ts`:
- Around line 1-8: Shorten the header in packages/core/src/registrar.ts lines
1-8 to 1-4 lines while retaining its single responsibility and module-boundary
rationale. Add a 1-4 line header at packages/core/src/registrar.test.ts line 1
describing the test module’s responsibility and why it exists.
- Around line 37-40: Update the X_REGISTRAR_CONFLICT error definition in the
registrar conflict path so fix is a single executable dependency-repair command
rather than explanatory text. Ensure the command directly deduplicates the
conflicting `@ultimat3/`${kind} package, and provide an equivalent --json form for
the CLI path while preserving the existing error context.
In `@packages/i18n/CLAUDE.md`:
- Around line 26-28: Update the plural-key convention in the documented rule to
use the CLDR n_other suffix instead of n_plural, keeping it consistent with
migrated catalogs, tests, and runtime probing. Remove the duplicate convention
rather than defining an alternative.
In `@packages/i18n/README.md`:
- Line 23: Update the README snippet label and referenced path from the
framework package barrel `packages/i18n/src/index.ts` to a generic application
module such as `src/i18n.ts`, while preserving the snippet’s application catalog
imports and type declarations.
In `@packages/i18n/src/define-catalogs.test.ts`:
- Line 1: Add a 1–4 line header comment before the imports in the
define-catalogs test module, explaining why these tests protect boot-time
catalog registration and stating the module’s single responsibility; do not
merely describe what the tests do.
In `@packages/render/src/dsl.test.ts`:
- Around line 1-8: Shorten the module header comment above the route DSL tests
to no more than four lines. Keep only the single responsibility of pinning the
normalized route descriptor shape and why that contract matters; remove detailed
implementation notes about specific tests, readers, and branching.
- Around line 86-88: Update the assertions around assertModeInvariants in the
route-mode tests to capture the thrown errors and verify their stable
X_ROUTE_MODE_INVALID code, rather than only checking that an exception occurs.
Apply the same error-code assertion to both referenced cases while preserving
the non-throwing expectation for the valid budgeted route.
- Around line 98-105: Update the test around registerRoute to add a negative
case passing the raw minimal declaration, and assert the documented route error
code. Keep the existing normalized config assertions, ensuring the test verifies
both accepted descriptors and rejection of raw declarations.
In `@packages/time/src/zoned.ts`:
- Around line 210-213: Update localDayEpoch to construct the UTC midnight
timestamp without Date.UTC’s 1900–1999 year remapping, using setUTCFullYear so
years 0000–0099 remain unchanged. Add a regression test covering the
0099-to-0100 boundary and verifying the resulting day difference is 1.
In `@wiki/Actions.md`:
- Line 76: Update the “Typed client function” example in Actions.md to use the
exposed map-wide rpc client and its Api['actions'] member instead of
api.publishPost(...). Keep publishPost.client({ baseUrl }) as the single-action
projection example.
- Around line 52-64: Clarify the lifted-field contract in the member table:
state that only input, output, policy, and mcp are facade members, while
cache.invalidates, rateLimit, and idempotent are available through describe() as
structured metadata only. Ensure the wording reconciles the “every declared
field is lifted” statement with this documented exception.
In `@wiki/Entities-And-Migrations.md`:
- Line 115: Update the documentation for the invalid-tenant X_INVARIANT_VIOLATED
error to require a stable cause: and fix: structure, with fix: instructing users
to set tenant to one of the listed valid columns or remove tenant to enable
inference.
In `@wiki/Error-Codes.md`:
- Around line 44-45: Update the X_REGISTRAR_MISSING and X_REGISTRAR_CONFLICT
entries in Error-Codes.md to derive the owner package from the reported
primitive kind, instruct users to import that package to announce its registrar,
and replace the hardcoded `@ultimat3/query` guidance. Make the conflict diagnosis
and resolution executable by using bun’s JSON package listing and a concrete
deduplication command.
In `@wiki/Scheduled-Tasks.md`:
- Line 25: Update the canonical scheduled-task example’s enqueue callback to
accept and use occurrenceMs, matching the enqueue contract defined near the diff
and ensuring catch-up payloads carry the scheduled occurrence rather than the
current wall-clock time.
- Around line 62-67: Reconcile the task metadata shape with the CLI contract:
update the `x tasks list --json` schema and example to remove `enabled`, or
retain `enabled` in the task metadata consistently. Ensure the task definition
and CLI output publish only one shape, defining the fact once and projecting it
everywhere.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f408c38d-577b-45cd-9df5-0d4b4be76e55
📒 Files selected for processing (90)
docs/idea/02-primitives.mdexamples/dummy/CLAUDE.mdexamples/dummy/apps/web/api/index.tsexamples/dummy/apps/web/app/digest/mail.tsexamples/dummy/apps/web/app/orgs/entity.tsexamples/dummy/apps/web/app/orgs/service.tsexamples/dummy/apps/web/app/posts/entity.tsexamples/dummy/apps/web/app/posts/mail.tsexamples/dummy/apps/web/app/posts/prompts/summarize.tsexamples/dummy/apps/web/shared/client.tsexamples/dummy/packages/core/CLAUDE.mdexamples/dummy/packages/core/src/billing.test.tsexamples/dummy/packages/core/src/billing.tsexamples/dummy/packages/core/src/index.tsexamples/dummy/packages/i18n/CLAUDE.mdexamples/dummy/packages/i18n/README.mdexamples/dummy/packages/i18n/catalogs/en.jsonexamples/dummy/packages/i18n/catalogs/es.jsonexamples/dummy/packages/i18n/src/catalog.test.tsexamples/dummy/packages/i18n/src/index.tsexamples/dummy/packages/mcp/CLAUDE.mdexamples/dummy/packages/mcp/README.mdexamples/dummy/packages/mcp/package.jsonexamples/dummy/packages/mcp/src/tools.test.tsexamples/dummy/packages/mcp/src/tools.tspackages/action/CLAUDE.mdpackages/action/README.mdpackages/action/src/client.test.tspackages/action/src/client.tspackages/action/src/define-api.test.tspackages/action/src/define-api.tspackages/action/src/dsl.test.tspackages/action/src/facade.test.tspackages/action/src/http.test.tspackages/action/src/http.tspackages/action/src/index.test.tspackages/action/src/index.tspackages/action/src/mutator.test.tspackages/action/src/mutator.tspackages/ai/CLAUDE.mdpackages/ai/README.mdpackages/ai/src/index.test.tspackages/ai/src/index.tspackages/cli/src/dev-hooks.test.tspackages/cli/src/dev-hooks.tspackages/core/README.mdpackages/core/src/error-codes.tspackages/core/src/index.tspackages/core/src/registrar.test.tspackages/core/src/registrar.tspackages/http/CLAUDE.mdpackages/http/src/pipeline.test.tspackages/http/src/pipeline.tspackages/http/src/router.test.tspackages/http/src/router.tspackages/i18n/CLAUDE.mdpackages/i18n/README.mdpackages/i18n/src/context.test.tspackages/i18n/src/context.tspackages/i18n/src/define-catalogs.test.tspackages/i18n/src/define-catalogs.tspackages/i18n/src/index.tspackages/i18n/src/translator.test.tspackages/i18n/src/translator.tspackages/mail/CLAUDE.mdpackages/mail/README.mdpackages/mail/src/index.test.tspackages/mail/src/index.tspackages/mcp/CLAUDE.mdpackages/mcp/README.mdpackages/mcp/src/index.test.tspackages/mcp/src/index.tspackages/query/CLAUDE.mdpackages/query/src/registry.tspackages/render/src/dsl.test.tspackages/time/README.mdpackages/time/src/index.tspackages/time/src/zoned.test.tspackages/time/src/zoned.tssite/pages/index.mdsite/pages/quickstart.mdwiki/Actions.mdwiki/Entities-And-Migrations.mdwiki/Error-Codes.mdwiki/Getting-Started.mdwiki/Jobs-And-Workflows.mdwiki/Realtime.mdwiki/Routes-And-Render-Modes.mdwiki/Scheduled-Tasks.mdwiki/The-Eight-Primitives.md
💤 Files with no reviewable changes (1)
- examples/dummy/packages/mcp/package.json
Correctness:
- defineApi builds its maps from the registrars' own results, not from
every module export — a feature helper could otherwise sit in
Api['actions'] as a client method nothing serves, and two modules'
same-named helpers overwrote each other with no X_ACTION_DUPLICATE.
ModuleRegistrar now returns RegisteredPrimitive; the type filters too.
- Date.UTC remaps years 0-99 onto 1900-1999, so daysBetween across the
0099/0100 boundary returned -693959. One utcEpoch() in zones.ts now
builds every calendar epoch in the package: localDayEpoch, toZoned's
weekday, offsetAt, fromZoned's target, normalizeWall, cron occurrence.
fromZoned({ year: 50 }) resolved to 1950 'exact' before this.
- registerRoute refuses a raw declaration with the new
X_ROUTE_UNNORMALIZED instead of registering it and crashing later in
describeRoutes() on config.budget.js.
- X_REGISTRAR_CONFLICT's fix was `bun pm ls | grep ... - then dedupe it`,
which hands grep the prose as file arguments. Now one runnable command.
- Invalid `tenant:` now carries the edit that repairs it, not
`x entity explain`, which describes invariants the author never wrote.
Tests that could not fail:
- .server() denial asserted through invoke (X_UNAUTHENTICATED); the two
existing calls agreed even if it reached the declared half directly.
- Projections checked by property access, not Object.keys — a
non-enumerable handle/def passed the old assertion.
- Route mode failures assert X_ROUTE_MODE_INVALID, not any throw; the
"refuses a raw declaration" test now has that negative case.
Docs:
- action README + wiki reach projections through publishPost, not the
action factory; typed client is rpc<Api['actions']>().
- Lifted-field contract made exhaustive (four members; the rest is
describe()-only). i18n plural rule names the real CLDR suffix set.
- Scheduled tasks show occurrenceMs; `enabled` dropped — no such field.
- Four headers trimmed to the 1-4 line rule, four added.
Co-Authored-By: Claude <noreply@anthropic.com>
|
This PR looks ready — CI is green and there are no risk signals (patch impact, tests/docs for DSL surface pinning). Feel free to apply a 🤖 Posted by developerz.ai — the maintainer agent, not a human. |
|
🎉 Released v0.1.2! 🤖 Posted by developerz.ai — the maintainer agent, not a human. |
Summary
dsl.test.tsalready existed for action/query/jobs (job+task)/entity/policy/render (route) from prior sessions — this PR closes the one remaining gap:mutatorhad only behavioral tests, no shape+delegation pin.packages/action/src/dsl.test.ts(mutator lives in the action package, built onaction()): exact member set (whole action façade +isMutator/conflict/local/server/describeMutator), delegation proofs for.local()/.server(), anda.tool().policy === a.policyfor a mutator instance specifically — the same "one authz object across every surface" invariant already pinned for action/query/policy.defineApi/defineCatalogs/daysBetween/etc.), mirroring the façade on the other seven primitives, and re-exportingtfrom every primitive package.Test plan
bun test packages/action/src/dsl.test.ts— 14/14 passbun run verify— 17/17 greenbun test— 3598 pass / 27 fail (unchanged pre-existingexamples/dummybaseline, 0 framework regressions)🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
New Features
rpctyped client and expanded API introspection.Improvements
POST /api/posts/publish.Documentation