---
title: "Porting api.lawyer"
url: https://services.dev/docs/examples/legal/port
captured: [legal-api-lawyer-suite.txt, legal-port.txt]
---
# Porting api.lawyer

URL: https://services.dev/docs/examples/legal/port

> 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 words: matter is a job, asset is a subject, review is a task, signer is a member, network is a pool, outcome is a end (examples/legal/brand.ts).*

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](https://services.dev/docs/examples/legal/port.md#its-behavior-tests) |
| api.lawyer's routes | 69 routes | 25 served as its clients call them · 17 adapted · 27 decided | [Route by route](https://services.dev/docs/examples/legal/port.md#route-by-route) |
| api.lawyer's own suite: its front-door probes, unchanged | 17 probes | 17 passed · 0 failed | [The suite](https://services.dev/docs/examples/legal/port.md#the-suite) |
| The brand's own suite, generated from its contract | 268 cases from 84 rows | 267 passed · 1 failed · 99 skipped | [Its suite](https://services.dev/docs/examples/legal.md#its-suite) |

## The suite

api.lawyer's suite@1, unchanged, against the generated brand:

```bash
npx services.dev verify --suite api-lawyer-suite.json
```

```txt title="magic values: client.email = signs@example.test"
api-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

17 passed · 0 failed
```

## Route by route

`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:

```bash
node -e "import('./port.ts')"   # each api.lawyer route: its mark, the operation, as the legal brand renders it, and what it answered
```

```txt title="magic values: none"
adapted   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
✓         GET /matters/:id/attestations                 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
✓         GET /matters/:id/documents                    job.files.list                               GET /matters/:id/files · GET /matters/:id/documents                                                                               404 EMPTY not_found
✓         POST /matters/:id/documents                   job.files.put                                PUT /matters/:id/files/:sha256 · POST /matters/:id/files · POST /matters/:id/documents                                            404 EMPTY not_found
adapted   POST /matters/:id/consent                     ask.complete                                 POST /asks/:id                                                                                                                    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
adapted   POST /matters/:id/client-direction            ask.complete                                 POST /asks/:id                                                                                                                    POST /asks/:id on the direct ask a decline opens: the client's direction is theirs alone, through their own link, so no key or agent can send it
adapted   POST /matters/:id/gig-result                  task.attest · task.send_back · task.decline  POST /reviews/:id/attest · POST /reviews/:id/send_back · POST /reviews/:id/decline                                                POST /reviews/:id/attest, /send_back and /decline, in the signer's own session: split into three
adapted   GET /matters/:id/settlement                   job.ledger                                   GET /owner/matters/:id                                                                                                            GET /owner/matters/:id: the owner's read, with the ledger
✓         POST /matters/:id/terminal                    job.end                                      POST /matters/:id/end · POST /owner/matters/:id/end · POST /matters/:id/terminal                                                  404 EMPTY not_found
✓         GET /assets                                   subject.list                                 GET /assets · GET /owner/assets                                                                                                   200 EMPTY
✓         GET /assets/:id                               subject.get                                  GET /assets/:id · GET /owner/assets/:id                                                                                           404 EMPTY not_found
adapted   GET /deadlines/:assetId                       clock.list                                   GET /clocks · GET /owner/clocks                                                                                                   GET /clocks?subject=<asset>: an asset's clocks, each with its date, its owner and its rungs
adapted   POST /deadlines/:assetId/ack                  clock.ack                                    POST /clocks/:id/ack · POST /owner/clocks/:id/ack                                                                                 POST /clocks/:id/ack: by the clock, not the asset, and only by its owner
adapted   POST /deadlines/:assetId/satisfy              clock.satisfy                                POST /clocks/:id/satisfy · POST /owner/clocks/:id/satisfy                                                                         POST /clocks/:id/satisfy { evidence }: closed on evidence on the record, and 422 evidence_required without it
adapted   POST /deadlines/:assetId/resolve              clock.choose                                 POST /clocks/:id/choose · POST /owner/clocks/:id/choose                                                                           POST /clocks/:id/choose { option }: one of the clock's lapse options, at its date; a step's name runs that step
adapted   POST /deadlines/:assetId/monitor              clock.assign                                 POST /owner/clocks/:id/assign · POST /clocks/:id/assign                                                                           POST /owner/clocks/:id/assign { member }: the owner console assigns who answers for it, or the client orders the steward Service
adapted   POST /deadlines/:assetId/tick                 test_clock.advance                           POST /test_clocks/:id/advance                                                                                                     POST /test_clocks/:id/advance: a test clock moves every matter on it, in test mode only
✓         GET /events                                   event.list                                   GET /events · GET /owner/events                                                                                                   200 EMPTY
✓         GET /events/stream                            event.stream                                 GET /events/stream                                                                                                                200 text/event-stream
✓         GET /conflicts                                member.ledger.get                            GET /me/ledger · GET /conflicts                                                                                                   403 BLOCKED needs_a_person
adapted   GET /conflicts/:practitionerId                member.ledger.get                            GET /me/ledger · GET /conflicts                                                                                                   GET /conflicts in the signer's own session: a ledger is read by its signer alone, so no path names another's
✓         POST /conflicts/entries                       member.ledger.add                            POST /me/ledger · POST /conflicts/entries                                                                                         403 BLOCKED needs_a_person
adapted   POST /conflicts/:practitionerId/entries       member.ledger.add                            POST /me/ledger · POST /conflicts/entries                                                                                         POST /conflicts/entries, in the signer's own session
69 routes · 25 ✓ · 17 adapted · 27 decision
```

## What was decided

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](https://services.dev/docs/service/gate.md) |
| `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](https://services.dev/docs/people/parties.md) |

Two adaptations change who may call: a consent and a client's direction became asks, which only the party completes, through their own link.

## Its behavior tests

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 your own

`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:

```ts title="legal · port.ts"
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](https://services.dev/docs/examples/legal/surfaces.md) shows the same operations on every surface.

---

- Up: [The legal example](https://services.dev/docs/examples/legal.md)
- Previous: [When the attorney declines](https://services.dev/docs/examples/legal/decline.md)
- Next: [Take your first real matter](https://services.dev/docs/examples/legal/launch.md)
