Porting api.lawyer

The running jobjob_7Hq2 · waiting · on claim · $149.00 held · sign · tsk_7Hq2

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.

MeasureOfResultRead it
api.lawyer's behavior tests: its four playbook suites and its gate, settle and eligibility suites, at a6b27b177 tests60 covered by the generated brand · 17 not ported, because a Service names its own stepsIts behavior tests
api.lawyer's routes69 routes25 served as its clients call them · 17 adapted · 27 decidedRoute by route
api.lawyer's own suite: its front-door probes, unchanged17 probes17 passed · 0 failedThe suite
The brand's own suite, generated from its contract268 cases from 84 rows267 passed · 1 failed · 99 skippedIts suite

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

Run
npx services.dev verify --suite api-lawyer-suite.json
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

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:

MarkMeans
✓The generated operation serves the route as api.lawyer's clients call it.
adaptedThe operation exists with another shape, and the row says what a client changes.
decisionThe route stays the brand's own, or another generated surface serves it, and the row says which.

The ✓ and adapted rows, as run:

Run
node -e "import('./port.ts')"   # each api.lawyer route: its mark, the operation, as the legal brand renders it, and what it answered
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

The 27 decision rows, grouped:

RoutesDecisionWhy
GET /og.pngkept as a brand routeAn image of the brand's own, a file in public/ served as it is, like icp.json
/workspaces and its 4 routeskept as a brand routeA test key and test clocks are the sandbox, so the model provisions none
POST /matters/:id/advancenever a call: the gateThe gate advances the graph, and no caller drives it: Propose, then the gate commits
GET /verifythe brand's generated docsThe Run our tests page is a page of the brand's docs, beside its /verify/suite.json
/firms and its 12 routesPool.firmsFirms enroll their attorneys, and the firm console serves them
/tenants and its 5 routesBrand.tenantsTenants 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:

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 shows the same operations on every surface.