Porting api.lawyer
Every api.lawyer route, matched to the operation the generated legal brand serves it with, adapted to a new shape, or decided. Then api.lawyer's own suite, passing against the generated brand.
legal's wordsmatter is a jobasset is a subjectreview is a tasksigner is a membernetwork is a pooloutcome is a end
api.lawyer was built by hand. The legal example generates a brand from its files, and this page holds the two side by side: the acceptance test for People, a real legal brand's routes, suite and behavior tests against a folder of Service() files.
| Measure | Of | Result | Read it |
|---|---|---|---|
api.lawyer's behavior tests: its four playbook suites and its gate, settle and eligibility suites, at a6b27b1 | 77 tests | 60 covered by the generated brand · 17 not ported, because a Service names its own steps | Its behavior tests |
| api.lawyer's routes | 69 routes | 25 served as its clients call them · 17 adapted · 27 decided | Route by route |
| api.lawyer's own suite: its front-door probes, unchanged | 17 probes | 17 passed · 0 failed | The suite |
| The brand's own suite, generated from its contract | 268 cases from 84 rows | 267 passed · 1 failed · 99 skipped | Its suite |
api.lawyer's suite@1, unchanged, against the generated brand:
npx services.dev verify --suite api-lawyer-suite.jsonapi-lawyer@1 · 17 rows · sha256:5e6f1f8f60a6c05908f07902cdb5a8e67c27496e01e071a7610f59426fcff064
from api-lawyer-suite.json · against http://localhost:8787
✓ llms-front-door GET /llms.txt ok
✓ capability-card GET /.well-known/agents.json ok
✓ openapi-contract GET /openapi.json ok
✓ pricing-free-truth GET /pricing ok
✓ matters-keyless-ok GET /matters ok
✓ matters-typed-empty GET /matters?filter=none ok
✓ matters-typed-blocked GET /matters?scope=admin ok
✓ healthz-typed-ok GET /healthz ok
✓ icp-self-classification GET /icp.json ok
✓ catalog-empty-but-real GET /catalog ok
✓ events-tail-typed GET /events ok
✓ conflicts-tier-boundary GET /conflicts ok
✓ workspace-unknown-is-typed GET /workspaces/ws_probe_none ok
✓ firm-unknown-is-typed GET /firms/fm_probe_none ok
✓ home-json-face GET /index.json ok
✓ home-md-face GET /index.md ok
✓ suite-self-serving GET /verify/suite.json ok
port.ts reads each api.lawyer route, finds the operation that serves it, and calls the running brand. Each row is marked one of three ways:
| Mark | Means |
|---|---|
| ✓ | The generated operation serves the route as api.lawyer's clients call it. |
| adapted | The operation exists with another shape, and the row says what a client changes. |
| decision | The route stays the brand's own, or another generated surface serves it, and the row says which. |
The ✓ and adapted rows, as run:
node -e "import('./port.ts')" # each api.lawyer route: its mark, the operation, as the legal brand renders it, and what it answeredadapted GET / brand.home GET /index.json · GET /index.md GET / is the Site's page; its JSON and Markdown faces are GET /index.json and GET /index.md
✓ GET /index.json brand.home GET /index.json · GET /index.md 200 application/json
✓ GET /index.md brand.home GET /index.json · GET /index.md 200 text/markdown
✓ GET /llms.txt brand.llms GET /llms.txt 200 text/markdown
✓ GET /openapi.json brand.openapi GET /openapi.json 200 application/json
✓ GET /.well-known/agents.json brand.card GET /.well-known/agents.json 200 application/json
✓ GET /pricing brand.pricing GET /pricing 200 application/json
✓ GET /healthz brand.health GET /healthz 200 OK
✓ GET /icp.json public/icp.json GET /icp.json · public/icp.json 200 application/json
✓ GET /verify/suite.json brand.verify GET /verify/suite.json 200 application/json
✓ GET /catalog service.list GET /catalog 200 OK
✓ GET /catalog/:sku service.get GET /catalog/:id 404 EMPTY not_found
✓ GET /fixtures sandbox.fixtures GET /fixtures 200 OK
adapted POST /keys key.create POST /keys POST /keys answers the test key at once, with its cap, and takes no email; a live key is a person's, at checkout
adapted POST /early-access service.waitlist POST /:service/waitlist POST /<service>/waitlist { email }: one waitlist per Service, the next a not_live answer names
adapted POST /matters service.order POST /matters · POST /:service POST /matters { service, …input }: the body names the Service, and POST /<service> takes the input alone
adapted POST /orders service.order POST /matters · POST /:service POST /<service>: each Service is its own door
✓ GET /matters job.list GET /matters · GET /owner/matters 200 EMPTY
✓ GET /matters/:id job.get GET /matters/:id 404 EMPTY not_found
✓ GET /matters/:id/journal job.record GET /matters/:id/record · GET /owner/matters/:id/record · GET /matters/:id/journal · GET /matters/:id/attestations?type=attested 404 EMPTY not_found
The 27 decision rows, grouped:
| Routes | Decision | Why |
|---|---|---|
GET /og.png | kept as a brand route | An image of the brand's own, a file in public/ served as it is, like icp.json |
/workspaces and its 4 routes | kept as a brand route | A test key and test clocks are the sandbox, so the model provisions none |
POST /matters/:id/advance | never a call: the gate | The gate advances the graph, and no caller drives it: Propose, then the gate commits |
GET /verify | the brand's generated docs | The Run our tests page is a page of the brand's docs, beside its /verify/suite.json |
/firms and its 12 routes | Pool.firms | Firms enroll their attorneys, and the firm console serves them |
/tenants and its 5 routes | Brand.tenants | Tenants resell the brand's Services as their own offers: Buyer, client, party |
Two adaptations change who may call: a consent and a client's direction became asks, which only the party completes, through their own link.
Past the routes, each of api.lawyer's flow tests maps to the generated brand's case that covers it, or is marked not ported, with why.
Fifteen of the 17 not ported test a playbook proposing each stage of api.lawyer's fixed ladder. A Service names its own steps, so there's no such proposal to port.
port.ts is a list: each route of the brand you're porting, the operation that serves it, and its mark. The run above calls every row against the generated brand and prints what it answered, so a ✓ is an answer, never a claim. Its type, and one row of each mark:
export interface Port {
route: string
op: string | readonly string[]
mark: Mark
moved?: string
decision?: 'a generated surface' | 'kept as a brand route'
why?: string
}
export default [
{ route: 'GET /matters/:id/journal', op: 'job.record', mark: '✓' },
{ route: 'POST /matters/:id/consent', op: 'ask.complete', mark: 'adapted', moved: "POST /asks/:id on the consent ask the matter opens before its first review posts: consent is the client's own act, through their own link, so it is an ask, never a matter verb a key could send" },
{ route: 'POST /matters/:id/advance', op: 'step.propose', mark: 'decision', decision: 'a generated surface', why: "the gate advances the graph and no caller drives it; a staff role's proposal is an act in its role's inbox in the console" },
] as const satisfies readonly Port[]flows.ts does the same for the brand's tests: each one names the case that proves it, or why it isn't ported.
What the brand gets shows the same operations on every surface.