hacifootprint
Traversal

A destination the app mints

The cookbook for an action that creates a thing and then goes to its page. The address does not exist until the handler runs, so the claim is a page NAME and never an address — a half-address is not an address.

The failure this prevents

Place order creates order 8fa2 and then goes to /orders/8fa2. The destination is real, it is the whole point of the action — and it does not exist until the handler has run. There is nothing literal to declare at authoring time.

The library refuses every attempt to declare it anyway, loudly and at build (below). A production integration read those refusals as this library cannot describe my app and took one of two exits, both of which cost it the thing it was trying to keep:

  • Drop the destination. Now the action declares no goTo and no writes that anyone watches, so the agent that fires it sees nothing change and reports a working control as a dead one — the exact failure goesTo exists to end.
  • Invent a literal. href: '/orders/new' navigates somewhere real and wrong, and nothing in this library can catch it, because it is a true-shaped lie: a fully literal address that the app meant as a placeholder.

Both are the same mistake. A half-address is not an address. The fix is to stop trying to make it one.

The shape that works

Four declarations, and the split is the whole idea: the graph names a page, your app mints the address.

const  = ('desk', {
  : {
    : {
      : '/orders',
      : {
        'place-order': {
          : 'Place the order',
          : ['orders.count'],
          // 2. THE CLAIM — a page NAME, not an address. Nothing to guess.
          : 'order-detail',
          // 3. AN ELEMENT GESTURE, reached through your handler. Never a url binding.
          : { : 'element', : { : 'button', : 'Place order' } },
        },
      },
    },
    // 1. THE PAGE KIND — one page, whose route carries the param. Perfectly legal.
    'order-detail': { : '/orders/:id' },
  },
});

const  = .({ : 'orders', : { 'orders.count': 0 } });

.('orders', {
  : {
    // 4. THE MINTED ID COMES BACK AS DATA — the handler's return value.
    'place-order': () => (),
  },
});

1. The page is a kind, and its route says so

route: '/orders/:id' is one page node standing for every order. That is what matchRoute reads /orders/8fa2 back onto, and a paramful route has always been legal — the refusals below are about paramful addresses, which is a different thing.

2. goTo is a name, so it can be declared before the thing exists

{ "action": "orders.place-order", "does": "Place the order",
  "goesTo": "order-detail" }        // ← served before anything is fired

The row an agent reads carries the claim, and so does a journey frame's readySteps row. It says exactly what is knowable at authoring time — this goes to the order-detail page — and nothing that is not.

3. The gesture is element, and the handler is what performs it

Your own click handler already knows how to route to the order it just created. Hand it over and the library never has to build an address at all.

4. The id travels back as produced

The handler's return value is captured and served as the act → get-data-back channel:

const  = .('orders.place-order', { : 'agent' });
if (.) {
  const  = ..;
  void .().(() => {
    .(); // → { orderId: '8fa2', href: '/orders/8fa2' }
  });
}

producedFor is a fresh, sanitized snapshot, safe to fold into a tool result — and it is data, so an order id that happens to read like an instruction reads as an order id.

Corroboration: one channel, and it is sync()

goesTo is a claim. The claim becomes arrival: 'observed' when — and only when — the app reports being on the page the claim named. There is exactly one channel for that, and the minted case needs no special handling on it:

// wherever your router announces a URL change — '/orders/8fa2' this time:
.((.., .) ?? .);

matchRoute places /orders/8fa2 on order-detail by the route the page declared, the join lands, and did_it_work reports arrival: 'observed'. The ?? is doing its usual job: a path the table cannot place returns undefined, and the raw path is recorded off-graph rather than guessed onto a page.

And then the page needs its own actions. A minted page is the one place where arriving and being able to do anything come apart, because the actions on an order-detail page usually come from the order. whenPageChanges is the door for that — it fires after the cursor lands, which is when a live action store can be re-read for the page you are actually on:

.(() => ());

Skip it and the library will tell you: landing on a page where an agent fire of every action would refuse records a dead-end gap row and warns once, naming the three fixes. That warning is the ordinary outcome of doing steps 1–4 and stopping, so it is worth expecting rather than debugging.

The anti-pattern, named

A minted destination is never a url binding and never a crossLink, and both doors refuse it by name rather than letting it through as something that looks wired:

binding: { kind: 'url', href: '/orders/:id' }

hcifootprint: action 'orders.open' declares a url binding with href '/orders/:id' — a ':param' segment can never materialise (the library never guesses params). Give the gesture a fully literal address, or bind a handler instead.

fromRoutes({ orders: '/orders', 'order-detail': '/orders/:id' }, { crossLinks: ['order-detail'] })

hcifootprint: fromRoutes crossLinks names 'order-detail', whose route '/orders/:id' has a ':param' segment — a link to it could never be built (the library never guesses params). Drop it from crossLinks, or author a tool that supplies the param.

crossLinks: trueevery pageskips paramful pages instead of refusing, because all of them is a sweep and not a request about that page; naming one is a request, and a request for something impossible is answered. Either way no link tool is built, and that is the honest outcome: a cross-link is a literal address offered from everywhere, and there is no such address here.

The third door is quieter and worth knowing. goTo-ing a paramful page in a session that holds navigate does not error at build — the claim is fine — but nothing can perform it:

{ "affordanceId": "orders.go", "navigatesTo": "order-detail", "materialized": false }
// fire → { "ok": false, "reason": "NOT_MATERIALIZED", "affordanceId": "orders.go" }

The route is not fully literal, so no href can be synthesized from it. That refusal is the library declining to guess :id, and the fix is step 3: bind a handler, which is the thing that knows.

Honest limits

  • The graph has one node for every order. A page kind is a kind: order-detail is one node whether there are three orders or three million, and the cursor being on it says which kind of page you are on, never which order. If a per-order fact matters to a guard, it belongs in your projected state, where your app can put the open order's id.
  • The library never reads the id back out of the URL. matchRoute answers which page, and its parameter names are never read. Your app already parsed the route; this library does not need a second, worse copy of that.
  • produced is only as good as your handler's return. A handler that routes and returns nothing has told the library nothing to hand on, and producedFor correctly says undefined rather than reaching for the URL.
  • goesTo stays a claim after the fire lands. It says what the app declared, not what the app did — arrival is the axis that can move, and its only two values are 'claimed' and 'observed'. There is no did not arrive.
  • A route table with two paramful siblings is the matcher's problem, not this page's. /orders/:id and /orders/new compete, and the more literal route wins by design — which is why the join is asked over the whole table rather than the claimed page's own route.

On this page