@augurworks/augur 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/_worker.js CHANGED
@@ -104,6 +104,10 @@ import { PURGED_AUTHOR, purgeThreads, idCollisions } from "./purge.mjs";
104
104
  import { authoredUnits, unitOfPath, unitPaths } from "./publish-units.mjs";
105
105
  import { composePublish, forkLanded } from "./publish-compose.mjs";
106
106
  import { normUnit, splitDraftPath, unitTable, draftAddress, DRAFT_ID_RE } from "./unit-core.mjs";
107
+ import {
108
+ siteModel, derivedPathKind, renderRootIndex, renderOpportunityIndex, renderPlaygroundIndex,
109
+ renderTierIndex, renderComponentsIndex, searchIndex, catalogFrom,
110
+ } from "./galleries.mjs";
107
111
  // `F-fork-verb`. Fork as a deliberate verb — one unit aliased to a new path, zero bytes
108
112
  // moved — plus the rule that keeps a fork's lineage and owner alive across every later
109
113
  // publish. Both are pure, and both are the CLI's too if it ever needs them.
@@ -4333,7 +4337,7 @@ function resolveBundlePath(manifests, pathname) {
4333
4337
  // against. It is the first argument for the same reason `loadTenantContext` takes it
4334
4338
  // first: everything below reads one workspace's store, and a function that had to guess
4335
4339
  // would resolve a path against whatever the isolate happened to hold.
4336
- async function assetFetch(tenantId, env, request) {
4340
+ async function assetFetch(tenantId, env, request, opts = {}) {
4337
4341
  if (!bundleMode(env)) return env.ASSETS.fetch(request);
4338
4342
  const url = new URL(request.url);
4339
4343
  // ── a draft address: the draft's table, not the manifest ─────────────────────────
@@ -4352,6 +4356,22 @@ async function assetFetch(tenantId, env, request) {
4352
4356
  return blobResponse(env, request, f);
4353
4357
  }
4354
4358
  const manifests = await loadManifests(tenantId, env);
4359
+ // ── a design-system draft, across the site (`?ds=<draft>`, §5) ──────────────────
4360
+ // A path under the workspace's declared skill folder resolves from THAT draft's table
4361
+ // when the request carries one, so a shared change can be looked at on every prototype
4362
+ // before it lands. Only the skill's own paths: a prototype's files never come from it,
4363
+ // and a draft that does not hold the file falls through to main.
4364
+ if (opts.dsDraft && DRAFT_ID_RE.test(opts.dsDraft) && decoded) {
4365
+ const skill = Object.values(manifests)
4366
+ .flatMap((m) => ((m && m.routing) || {}).publicSkillPrefixes || [])
4367
+ .map(normUnit).find((p) => p && decoded.startsWith(p));
4368
+ const stub = skill ? unitStub(env, tenantId, skill) : null;
4369
+ if (stub) {
4370
+ const dr = await unitCall(stub, `/draft/${opts.dsDraft}`, null, "GET");
4371
+ const f = dr.status === 200 && dr.body.table ? dr.body.table[decoded] : null;
4372
+ if (f) return blobResponse(env, request, f);
4373
+ }
4374
+ }
4355
4375
  const r = resolveBundlePath(manifests, url.pathname);
4356
4376
  if (r.redirect) return Response.redirect(new URL(r.redirect + url.search, url).toString(), 308);
4357
4377
  if (r.miss) return new Response("Not Found", { status: 404 });
@@ -4783,6 +4803,9 @@ function publishRefusalBody(refusal) {
4783
4803
  "viewer-role": "This account can look around but not publish.",
4784
4804
  "not-an-admin": "This token was minted for an admin and this account is no longer one. Run `augur login` again for a token scoped to what it may still publish.",
4785
4805
  }[refusal];
4806
+ // `unknown-token` and `wrong-space` stay a bare `forbidden` ON PURPOSE: a guessed token
4807
+ // must not learn whether it exists somewhere. The sentence a person needs — pair again —
4808
+ // is said by the CLI, which knows it just used a saved token (scripts/lib/draft.mjs).
4786
4809
  return message ? { error: "forbidden", message } : { error: "forbidden" };
4787
4810
  }
4788
4811
 
@@ -5029,21 +5052,23 @@ const UNIT_CT_RE = /^[\w.+-]+\/[\w.+-]+(; ?charset=[\w-]+)?$/i;
5029
5052
  // to anonymous visitors, from a draft on a folder nobody edits.
5030
5053
  const isNewUnitPath = (unit) => unit.split("/").filter(Boolean).length === 2;
5031
5054
  // Top-level folders a NEW unit may not be created under. A landing replaces a unit's
5032
- // folder wholesale, so a draft opened on `/components/button/` or `/skills/x-ui/` would
5033
- // have replaced the design system's or the gallery's own files with whatever one draft
5034
- // held. These are the folders a site builds from and every prototype reads: the design
5035
- // system, the gallery pages, the changelog, the search index, engine chrome (`_`, `__`).
5036
- // Stateless and the same on every workspace an opportunity folder is anything else,
5037
- // `/playground/<name>/` included. A unit the manifest ALREADY declares is never held to
5038
- // this: the manifest is the authority on what exists.
5055
+ // folder wholesale, so the folders whose contents are GENERATED the tokens page, the
5056
+ // primitives gallery, the changelog, the search index, fonts, engine chrome (`_`, `__`)
5057
+ // are never a unit. The four library tiers are NOT here: `/components/button/` is one
5058
+ // demo folder, a unit like any prototype, and the tier's own index is a sibling file the
5059
+ // derived renderer owns. `skills` stays reserved with ONE exception, decided where the
5060
+ // workspace's declared design system is known (`unitApi`): the skill folder the space
5061
+ // names is the design-system unit. Stateless and the same on every workspace. A unit the
5062
+ // manifest ALREADY declares is never held to this: the manifest is the authority.
5039
5063
  const RESERVED_UNIT_FOLDERS = Object.freeze([
5040
- "base", "components", "pages", "patterns", "skills", "tokens", "fonts",
5041
- "admin", "changelog", "search",
5064
+ "skills", "tokens", "primitives", "fonts", "tracks", "admin", "changelog", "search",
5042
5065
  ]);
5043
5066
  const isReservedUnitFolder = (unit) => {
5044
5067
  const first = unit.split("/").filter(Boolean)[0] || "";
5045
5068
  return first.startsWith("_") || RESERVED_UNIT_FOLDERS.includes(first);
5046
5069
  };
5070
+ /** The one unit allowed under `skills/`: the design system the workspace declares. */
5071
+ const isDeclaredSkillUnit = (unit, tctx) => ((tctx && tctx.PUBLIC_SKILL_PREFIXES) || []).some((p) => normUnit(p) === unit);
5047
5072
  const shortText = (s, n) => String(s == null ? "" : s).slice(0, n);
5048
5073
  // `tctx.SPACES` is a routing field, filled from the live manifests by `loadTenantContext`
5049
5074
  // on the request's normal path — which this route also goes through, since `handleRequest`
@@ -5077,7 +5102,12 @@ function defaultSpaceIdFromManifests(manifests) {
5077
5102
  * rule the commit handler already keeps. `unitSources` records the landing so the old
5078
5103
  * composed publish treats the unit as somebody's work rather than as a fast-forward.
5079
5104
  */
5080
- const UNIT_LANDING_ATTEMPTS = 3;
5105
+ // Every landing in a space writes the one manifest by compare-and-set. Three attempts
5106
+ // with no pause between them lost 2 of 8 landings made in the same second (measured live,
5107
+ // 6 Sep 2026); six attempts with a little jitter between them is what a burst of agents
5108
+ // landing at once needs. The CLI lands again on `manifest-contended` as well.
5109
+ const UNIT_LANDING_ATTEMPTS = 6;
5110
+ const landingBackoff = (attempt) => new Promise((r) => setTimeout(r, 30 + Math.random() * 90 * (attempt + 1)));
5081
5111
  async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, now) {
5082
5112
  const bundles = bundlesFor(env, tctx.tenantId);
5083
5113
  const key = `spaces/${spaceId}/manifest.json`;
@@ -5140,7 +5170,11 @@ async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, n
5140
5170
  // A unit the manifest already declares keeps the entry it has, spelling and all: the
5141
5171
  // set union that replaced it added a second, normalized entry beside one written
5142
5172
  // without its trailing slash, and two entries for one unit is two things to prune.
5143
- routing.publicPrefixes = authoredUnits(cur).has(unit)
5173
+ // The design-system unit is the one landing that adds NO public prefix: its public
5174
+ // surface is the rendered-asset rule `publicSkillPrefixes` already spells, and a whole
5175
+ // skill folder opened to the gate would expose the documents that rule keeps gated.
5176
+ const skillUnit = ((cur.routing || {}).publicSkillPrefixes || []).some((p) => normUnit(p) === unit);
5177
+ routing.publicPrefixes = authoredUnits(cur).has(unit) || skillUnit
5144
5178
  ? [...(routing.publicPrefixes || [])]
5145
5179
  : [...(routing.publicPrefixes || []), unit];
5146
5180
  routing.unitSources = { ...(routing.unitSources || {}), [unit]: { sha: null, dirty: false, landed: true, by: who.personId, at: now } };
@@ -5158,7 +5192,7 @@ async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, n
5158
5192
  // A store that answers `null` refused the precondition — R2's way of saying the object
5159
5193
  // moved under us. Anything else is a write.
5160
5194
  const wrote = await bundles.put(key, JSON.stringify(out), etag ? { onlyIf: { etagMatches: etag } } : undefined);
5161
- if (etag && wrote === null) { bustManifests(tctx.tenantId); continue; }
5195
+ if (etag && wrote === null) { bustManifests(tctx.tenantId); await landingBackoff(attempt); continue; }
5162
5196
  // THE BYTES ARE LIVE FROM HERE: the manifest is the pointer visitors follow. The version
5163
5197
  // document is the rollback record, and a store failure writing it must not turn a landing
5164
5198
  // that already happened into a reported failure with no lease release — it is logged as
@@ -5249,6 +5283,12 @@ async function unitCaller(tctx, request, env, spaceId) {
5249
5283
  return { who: { personId: personId(me.email), label: me.email }, session: session || "browser" };
5250
5284
  }
5251
5285
 
5286
+ /** A `draft-closed` answer names who landed it by id; the roster puts the face on it. */
5287
+ function closedFace(tctx, body) {
5288
+ if (!body || body.error !== "draft-closed" || !body.by) return body;
5289
+ return { ...body, ...personFace(tctx.USERS, body.by) };
5290
+ }
5291
+
5252
5292
  async function unitApi(tctx, request, url, env) {
5253
5293
  const verb = url.pathname.slice(UNIT_API_PREFIX.length);
5254
5294
  if (!/^[a-z-]+$/.test(verb)) return jsonResponse({ error: "bad-path" }, 400);
@@ -5297,7 +5337,7 @@ async function unitApi(tctx, request, url, env) {
5297
5337
  // reached, so a folder that is not one is never given a draft to be adopted from later.
5298
5338
  // A unit the space ALREADY publishes is a unit whatever its shape (the manifest is the
5299
5339
  // authority on what exists); anything else has to be the shape a new one is created in.
5300
- if (!authoredUnits(live).has(unit)) {
5340
+ if (!authoredUnits(live).has(unit) && !isDeclaredSkillUnit(unit, tctx)) {
5301
5341
  if (!isNewUnitPath(unit)) return jsonResponse({ error: "bad-unit", reason: "not-a-prototype-folder" }, 400);
5302
5342
  if (isReservedUnitFolder(unit)) return jsonResponse({ error: "bad-unit", reason: "reserved-folder" }, 400);
5303
5343
  }
@@ -5373,13 +5413,13 @@ async function unitApi(tctx, request, url, env) {
5373
5413
  }
5374
5414
  if (missing.length) return jsonResponse({ error: "missing-blobs", missing: [...new Set(missing)] }, 409);
5375
5415
  const r = await unitCall(stub, "/save", { draftId: body.draftId, draftRevision: body.draftRevision, changes, baseRevision: body.baseRevision, at: now });
5376
- return jsonResponse(r.body, r.status);
5416
+ return jsonResponse(closedFace(tctx, r.body), r.status);
5377
5417
  }
5378
5418
  if (verb === "land" || verb === "restore") {
5379
5419
  const r = await unitCall(stub, `/${verb}`, verb === "land"
5380
5420
  ? { draftId: body.draftId, baseRevision: body.baseRevision, at: now }
5381
5421
  : { revision: body.revision, at: now });
5382
- if (r.status !== 200) return jsonResponse(r.body, r.status);
5422
+ if (r.status !== 200) return jsonResponse(closedFace(tctx, r.body), r.status);
5383
5423
  const written = await writeUnitLanding(tctx, env, spaceId, unit, r.body.table, r.body.changed, who, now);
5384
5424
  if (written.error) {
5385
5425
  await unitCall(stub, "/abandon-land", { lease: r.body.lease });
@@ -5418,7 +5458,7 @@ async function unitApi(tctx, request, url, env) {
5418
5458
  if (verb === "sync" || verb === "discard") {
5419
5459
  const r = await unitCall(stub, `/${verb}`, { draftId: body.draftId, at: now });
5420
5460
  if (verb === "discard" && r.status === 200) await noteUnitDrafts(tctx, env, stub, unit, now);
5421
- return jsonResponse(r.body, r.status);
5461
+ return jsonResponse(closedFace(tctx, r.body), r.status);
5422
5462
  }
5423
5463
  return jsonResponse({ error: "unknown-verb" }, 404);
5424
5464
  }
@@ -5427,46 +5467,6 @@ async function publishApi(tctx, request, url, env) {
5427
5467
  const [spaceId, op, arg] = url.pathname.slice("/__publish/".length).split("/");
5428
5468
  if (!spaceId || !op || !/^[a-z0-9_][a-z0-9-]*$/.test(spaceId)) return jsonResponse({ error: "bad-path" }, 400);
5429
5469
 
5430
- // ── working marks (`F-presence-marks`) ─────────────────────────────────────
5431
- //
5432
- // AHEAD OF THE BUNDLE-STORE GUARD, deliberately: a mark is not published content and
5433
- // holds nothing the store knows about, so an instance serving from ASSETS — `augur dev`,
5434
- // `npm run offline`, a raw engine build — is exactly where two agents most need to stay
5435
- // out of each other's way, and 501 there would be an accident of where the check sits.
5436
- //
5437
- // ANY VALID PUBLISH TOKEN, whatever its scope. A mark names a path, not a space, and a
5438
- // space-scoped token holder is precisely the person whose work-start is worth announcing.
5439
- // The capability gate still applies, so a restricted credential (the control plane's
5440
- // purge token) reaches this no more than it reaches anything else.
5441
- if (spaceId === "_marks") {
5442
- const a = await publishAuthDetailed(tctx, request, env, spaceId, true);
5443
- if (!a.entry) return jsonResponse(publishRefusalBody(a.refusal), 403);
5444
- if (capabilityRefusal(a.entry, spaceId, op)) {
5445
- return jsonResponse({ error: "forbidden", reason: "capability-not-granted" }, 403);
5446
- }
5447
- // WHO, from the credential and never from the body. `augur login` labels a token with
5448
- // the holder's address; a token an admin minted by hand carries whatever they typed,
5449
- // which hashes to a stable id that resolves to no roster face — honest, and better
5450
- // than letting the caller name itself.
5451
- const who = { personId: personId(a.entry.label || "") };
5452
- if (op === "list" && request.method === "GET") {
5453
- return jsonResponse({ ...(await readMarks(tctx, env)), ttlMs: MARK_TTL_MS, maxTtlMs: MARK_TTL_MAX_MS });
5454
- }
5455
- let body = null;
5456
- if (request.method === "POST") {
5457
- try { body = await request.json(); } catch (e) { return jsonResponse({ error: "bad-json" }, 400); }
5458
- }
5459
- if (op === "set" && request.method === "POST") {
5460
- const out = await writeMark(tctx, env, who, { path: body && body.path, ttl: body && body.ttl });
5461
- return out.error ? jsonResponse(out, out.error === "bad-input" ? 400 : 503) : jsonResponse(out);
5462
- }
5463
- if (op === "clear" && request.method === "POST") {
5464
- const out = await clearMark(tctx, env, who, { path: body && body.path });
5465
- return out.error ? jsonResponse(out, out.error === "bad-input" ? 400 : 503) : jsonResponse(out);
5466
- }
5467
- return jsonResponse({ error: "unknown-op" }, 400);
5468
- }
5469
-
5470
5470
  if (!env.BUNDLES) return jsonResponse({ error: "bundle-store-not-configured" }, 501);
5471
5471
  // This workspace's view of the store. `blobs/` is content-addressed and shared, so the
5472
5472
  // three `blobs/…` operations below deliberately keep using the binding directly — the
@@ -7238,6 +7238,94 @@ async function composeChrome(tctx, res, url) {
7238
7238
  return new Response(html, { status: res.status, statusText: res.statusText, headers });
7239
7239
  }
7240
7240
 
7241
+ // ---- Derived pages (drafts that land, §6.4) -------------------------------------
7242
+ // Where drafts are served, the gallery, each opportunity's index, the playground, the
7243
+ // library tiers and the finder's index are RENDERED HERE from the live store, so a landing
7244
+ // is on them at once and no client build ships them. The stored copies a publish once
7245
+ // baked are ignored on this path — they cannot know about a landing. Where drafts are not
7246
+ // served, nothing here runs and the stored pages serve exactly as before.
7247
+ //
7248
+ // Two store reads feed a render besides the manifest — the status baseline
7249
+ // (`/prototype-status.json`) and the design-system catalog (`/registry.json`) — cached per
7250
+ // manifest version; the status OVERLAY is read fresh every time, one get, as the status
7251
+ // route itself reads it. Per-request work after that is string rendering.
7252
+ const DERIVED = tenantCache("derived");
7253
+ const DERIVED_PATH_RE = /^\/(?:[^/]+\/?(?:index\.html)?|index\.html|__search\.json)?$/;
7254
+ async function derivedInputs(tctx, env, spaceId, manifest) {
7255
+ const slot = DERIVED.entry(tctx.tenantId, () => ({ key: null, baseline: {}, catalog: {} }));
7256
+ const key = `${spaceId}:${manifest.version}`;
7257
+ if (slot.key === key) return slot;
7258
+ const readJson = async (p) => {
7259
+ const f = (manifest.files || {})[p];
7260
+ if (!f || !env.BUNDLES) return null;
7261
+ try {
7262
+ const obj = await env.BUNDLES.get("blobs/" + f.h);
7263
+ return obj ? JSON.parse(await obj.text()) : null;
7264
+ } catch (e) { return null; }
7265
+ };
7266
+ const base = await readJson("/prototype-status.json");
7267
+ const baseline = {};
7268
+ for (const [k, v] of Object.entries(base && typeof base === "object" ? base : {})) {
7269
+ if (!k.startsWith("_") && typeof v === "string") baseline[k] = v;
7270
+ }
7271
+ slot.baseline = baseline;
7272
+ slot.catalog = catalogFrom(await readJson("/registry.json"));
7273
+ slot.key = key;
7274
+ return slot;
7275
+ }
7276
+ /** A derived page for this path, or null when the path is not one (or drafts are not served here). */
7277
+ async function derivedPage(tctx, env, url) {
7278
+ if (!draftsServedHere(env) || !DERIVED_PATH_RE.test(url.pathname)) return null;
7279
+ const manifests = await loadManifests(tctx.tenantId, env);
7280
+ const spaceId = defaultSpaceIdFromCtx(tctx) || defaultSpaceIdFromManifests(manifests);
7281
+ const manifest = spaceId ? manifests[spaceId] : null;
7282
+ if (!manifest || !manifest.files) return null;
7283
+ const inputs = await derivedInputs(tctx, env, spaceId, manifest);
7284
+ const store = overlayFor(env, tctx);
7285
+ let statuses = {};
7286
+ try { statuses = store ? (await store.read("statuses")) || {} : {}; } catch (e) { statuses = {}; }
7287
+ // A recorded author resolves to a face through the roster; an id nobody answers to
7288
+ // (an ex-member, the `live` adoption) is nobody's face rather than a blank chip.
7289
+ const people = (id) => { const f = personFace(tctx.USERS, id); return f.name ? { id, ...f } : null; };
7290
+ const now = Date.now();
7291
+ const model = siteModel({ manifest, statuses, baseline: inputs.baseline, people, now });
7292
+ const kind = derivedPathKind(url.pathname, model);
7293
+ if (!kind) return null;
7294
+ const sp = (tctx.SPACES || []).find((s) => s.id === spaceId) || {};
7295
+ const ctx = { spaces: tctx.SPACES || [], activeSpace: spaceId, chrome: tctx.CHROME_POINTER || null, projectsLabel: sp.projectsLabel || "", now };
7296
+ if (kind.kind === "search") return jsonResponse(searchIndex(model, ctx), 200, { "Cache-Control": "no-cache" });
7297
+ if (kind.kind === "opportunity" && !kind.slash) return Response.redirect(new URL(`/${encodeURIComponent(kind.name)}/${url.search}`, url).toString(), 308);
7298
+ const html = kind.kind === "root" ? renderRootIndex(model, ctx)
7299
+ : kind.kind === "opportunity" ? renderOpportunityIndex(model, kind.name, ctx)
7300
+ : kind.kind === "playground" ? renderPlaygroundIndex(model, ctx)
7301
+ : kind.kind === "tier" ? renderTierIndex(model, kind.tier, ctx)
7302
+ : kind.kind === "components" ? renderComponentsIndex(model, inputs.catalog, ctx)
7303
+ : null;
7304
+ if (html == null) return null;
7305
+ return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": ASSET_REVALIDATE } });
7306
+ }
7307
+
7308
+ // ---- The design-system overlay cookie (`?ds=`) --------------------------------
7309
+ // `?ds=<draft>` on any page names a design-system draft to look through; the cookie keeps
7310
+ // it while the person navigates; `?ds=` with nothing drops it. A malformed id is nobody's
7311
+ // draft and touches no cookie. Members only — the branch that reads this is past the gate.
7312
+ const DS_COOKIE = "augur_ds";
7313
+ function dsOverlay(request, url) {
7314
+ if (url.searchParams.has("ds")) {
7315
+ const v = url.searchParams.get("ds") || "";
7316
+ if (v === "") return { draft: null, setCookie: `${DS_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax; Secure; HttpOnly` };
7317
+ if (!DRAFT_ID_RE.test(v)) return { draft: null, setCookie: null };
7318
+ return { draft: v, setCookie: `${DS_COOKIE}=${v}; Path=/; SameSite=Lax; Secure; HttpOnly` };
7319
+ }
7320
+ const c = cookieValue(request.headers.get("Cookie") || "", DS_COOKIE);
7321
+ return { draft: c && DRAFT_ID_RE.test(c) ? c : null, setCookie: null };
7322
+ }
7323
+ function withSetCookie(res, cookie) {
7324
+ const out = new Response(res.body, res);
7325
+ out.headers.append("Set-Cookie", cookie);
7326
+ return out;
7327
+ }
7328
+
7241
7329
  // ---- The draft bar (drafts that land, §5) -------------------------------------
7242
7330
  // A prototype is raw HTML from a space and carries no chrome; a draft has no build step
7243
7331
  // that could bake one in. So a member looking at a unit's page — main or a draft address —
@@ -7835,16 +7923,6 @@ const OVERLAY_KV_KEYS = Object.freeze({
7835
7923
  boards: Object.freeze({ doc: "board", layout: "keyed", workspaceScoped: true }),
7836
7924
  // Canvas image METADATA — the bytes are in R2. See assetApi.
7837
7925
  assets: Object.freeze({ doc: "basset-meta", layout: "keyed" }),
7838
- // WORKING MARKS — "something is editing here right now". One row per path, and the row
7839
- // is only meaningful until its own TTL runs out. See the marks section below.
7840
- //
7841
- // `map` RATHER THAN `keyed`, and the trade is worth naming because it looks backwards.
7842
- // Keyed would give one KV document per path, so two marks written in the same window
7843
- // could not lose each other. It would also turn every READ into a kv.list plus a get per
7844
- // row — and marks are read by a gallery page as well as by the CLI, which puts a listing
7845
- // in front of ordinary page loads on a store whose daily get budget has been exhausted
7846
- // before. One document is one get. What it costs is stated on `writeMark`.
7847
- marks: Object.freeze({ doc: "marks", layout: "map" }),
7848
7926
  // WHICH UNITS HAVE OPEN DRAFTS — a hint the gallery reads so it can ask the right unit
7849
7927
  // objects and no others (`draftsIndexApi`). One row per unit, `{n, at}`, written from
7850
7928
  // the object's own presence answer after open, land and discard. NEVER THE TRUTH: the
@@ -9055,11 +9133,12 @@ function doorText(f) {
9055
9133
  + ` ${f.connect}\n\n`
9056
9134
  + `It prints a link and a code. The owner of this workspace opens the link in a\n`
9057
9135
  + `browser they are already signed in to and enters the code. The token lands on\n`
9058
- + `that machine, and \`augur publish\` / \`augur ship\` use it from then on. With no\n`
9136
+ + `that machine, and every verb below uses it from then on. Talking to that person\n`
9137
+ + `through messages rather than a terminal? Add --no-wait: it prints the line and\n`
9138
+ + `exits; run the same command again after they approve and the token is collected.\n`
9139
+ + `With no\n`
9059
9140
  + `source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches one (it reads\n`
9060
9141
  + `the origin from the pairing).\n\n`
9061
- + `Not on npm yet? The engine clone sits next to every workspace that publishes:\n\n`
9062
- + ` node <engine>/scripts/cli.mjs connect --origin ${f.origin}\n\n`
9063
9142
  : `Device pairing is switched off on this workspace. Ask an admin for an invite;\n`
9064
9143
  + `once you have signed in, \`augur login\` (email and password, meant for CI)\n`
9065
9144
  + `trades that for a publish token.\n\n`;
@@ -10221,235 +10300,6 @@ async function nameApi(tctx, request, url, env) {
10221
10300
  return jsonResponse({ error: "method-not-allowed" }, 405);
10222
10301
  }
10223
10302
 
10224
- // ---- Working marks (KV-backed, single key) ----------------------------------
10225
- //
10226
- // `F-presence-marks`. Nothing anywhere said what was already being worked on. Two
10227
- // collaborators' tools — usually two agents, on two machines, told to improve "the
10228
- // checkout flow" — would each open the same folder, each edit it, and find out at publish
10229
- // time, where the answer is a fork and a conflict file nobody asked for.
10230
- //
10231
- // ⚠️ THIS IS DELIBERATELY NOT A LOCK, and every line below is written so it cannot become
10232
- // one. A mark REFUSES NOTHING. It is not consulted by the gate, by the publish handler, by
10233
- // the commit CAS or by anything else that could say no. It is a note left where the next
10234
- // reader will look, and the whole protocol is: write one before you start, read them
10235
- // before you start. Enforcement when coordination fails is the composed publish's job
10236
- // (`src/publish-compose.mjs`), which is the only place in this engine allowed to refuse a
10237
- // write over a collision — and it does it on evidence, after the fact, never on a claim.
10238
- //
10239
- // THE PROTOCOL IS AGENT-FIRST. The badge a person sees on a gallery card is the byproduct,
10240
- // not the point: as an agent's edit shrinks toward seconds, a mark is FELT almost never and
10241
- // READ always. So the write side is the CLI (`augur mark`, over a publish token, see the
10242
- // `_marks` branch in publishApi) and the browser side is read-only — a person editing in a
10243
- // tab is not running a work-start step and inventing one for them would be a lie about who
10244
- // wrote what.
10245
- //
10246
- // ⚠️ A MARK EXPIRES BY ITSELF AND IS NEVER TRUSTED TO BE CLEARED. The thing that leaves a
10247
- // mark is a process that can be killed — Ctrl-C, an OOM, a laptop lid — and a claim that
10248
- // outlives the claimant is worse than no claim at all, because the next reader believes it.
10249
- // So EXPIRY IS A READ-TIME FILTER (`liveMarks`), not a cleanup job: the moment `startedAt +
10250
- // ttl` is in the past the mark is gone from every answer, whether or not anything ever runs
10251
- // again. `sweepExpired` below only reclaims the BYTES, opportunistically, and correctness
10252
- // never depends on it having run.
10253
-
10254
- /** How long a mark is good for when the caller does not say. */
10255
- const MARK_TTL_MS = 10 * 60_000;
10256
- /**
10257
- * The longest a caller may ask for. An agent that wants four hours is describing a lock,
10258
- * and the answer to a lock is a shorter mark re-written as the work continues.
10259
- */
10260
- const MARK_TTL_MAX_MS = 60 * 60_000;
10261
- /** The shortest, so a `--ttl 0` cannot write a mark that is already dead. */
10262
- const MARK_TTL_MIN_MS = 5_000;
10263
- /**
10264
- * How many lapsed rows one write may reclaim. Bounded low because on the KV backing each
10265
- * row delete is a whole-document read and put, so the sweep can cost more than the litter
10266
- * it collects; an unbounded one would turn a work-start step into a hundred writes on a
10267
- * workspace nobody has marked in a month. Nothing depends on it running at all.
10268
- */
10269
- const MARK_SWEEP_MAX = 4;
10270
- /** Belt and braces: a workspace cannot be filled with marks by a loop. */
10271
- const MARK_MAX_ROWS = 200;
10272
-
10273
- /**
10274
- * One spelling of a path, so two tools that mean the same folder agree.
10275
- *
10276
- * Leading and trailing slash, always: a mark names a UNIT — the prototype folder a URL
10277
- * names and a person edits — and `unitOfPath` in src/publish-units.mjs decides containment
10278
- * by prefix, which only works when a folder ends in a slash. `/a/b` and `/a/bc/` would
10279
- * otherwise overlap.
10280
- */
10281
- function normalizeMarkPath(p) {
10282
- const s = clamp(p, 300);
10283
- if (!s) return "";
10284
- const trimmed = s.trim().replace(/^\.\//, "").replace(/\/{2,}/g, "/");
10285
- if (!trimmed || trimmed === "/") return "/";
10286
- return `/${trimmed.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
10287
- }
10288
-
10289
- /**
10290
- * Do these two paths describe overlapping work? Containment in either direction — a mark
10291
- * on `/checkout/` covers `/checkout/step-two/`, and a mark on `/checkout/step-two/` is
10292
- * worth showing to somebody about to take `/checkout/`.
10293
- */
10294
- function markPathsOverlap(a, b) {
10295
- const x = normalizeMarkPath(a), y = normalizeMarkPath(b);
10296
- if (!x || !y) return false;
10297
- return x === y || x.startsWith(y) || y.startsWith(x);
10298
- }
10299
-
10300
- /** The instant a mark stops meaning anything. Pure, and the only definition of expiry. */
10301
- function markExpiresAt(m) {
10302
- const started = Date.parse((m && m.startedAt) || "");
10303
- if (!Number.isFinite(started)) return 0;
10304
- const ttl = Number.isFinite(+(m && m.ttl)) ? +m.ttl : MARK_TTL_MS;
10305
- return started + Math.min(Math.max(ttl, MARK_TTL_MIN_MS), MARK_TTL_MAX_MS);
10306
- }
10307
-
10308
- /**
10309
- * The live marks in a stored map, newest first. THE expiry rule — every reader goes
10310
- * through here, so a lapsed mark cannot be reported by one surface and hidden by another.
10311
- */
10312
- function liveMarks(map, now = Date.now()) {
10313
- return Object.entries(map || {})
10314
- .map(([path, m]) => (m && typeof m === "object" ? { ...m, path: m.path || path } : null))
10315
- .filter((m) => m && markExpiresAt(m) > now)
10316
- .sort((a, b) => Date.parse(b.startedAt || 0) - Date.parse(a.startedAt || 0));
10317
- }
10318
-
10319
- /**
10320
- * What a mark looks like on the wire: the stored row plus two things a reader would
10321
- * otherwise have to compute, and one it could not — the display name behind the id.
10322
- *
10323
- * The NAME IS RESOLVED, NEVER STORED. `personId` is the same one-way hash a comment
10324
- * carries, so a mark holds no address; the roster turns it back into a face at read time,
10325
- * which also means a rename shows through and an ex-member resolves to nobody.
10326
- */
10327
- function decorateMark(m, users, now = Date.now()) {
10328
- const u = (users || []).find((x) => x && personId(x.email) === m.personId);
10329
- return {
10330
- path: m.path,
10331
- personId: m.personId || null,
10332
- startedAt: m.startedAt,
10333
- ttl: markExpiresAt(m) - Date.parse(m.startedAt),
10334
- by: u ? u.name || nameFromEmail(u.email) : null,
10335
- initials: u ? u.initials || initialsFor(u.name || u.email) : null,
10336
- color: u ? u.color || null : null,
10337
- expiresIn: Math.max(0, markExpiresAt(m) - now),
10338
- };
10339
- }
10340
-
10341
- /** Read the live marks for a workspace, decorated. `null` store answers with nothing. */
10342
- async function readMarks(tctx, env) {
10343
- const store = overlayFor(env, tctx);
10344
- if (!store) return { marks: [], warning: "no-kv-binding" };
10345
- const map = await store.read("marks");
10346
- const now = Date.now();
10347
- return { marks: liveMarks(map, now).map((m) => decorateMark(m, tctx.USERS, now)), now };
10348
- }
10349
-
10350
- /**
10351
- * Reclaim the bytes of rows that lapsed. NOT the expiry mechanism — `liveMarks` already
10352
- * stopped reporting these, and this runs only so a one-way author id does not sit in the
10353
- * store for months after it stopped meaning anything. Per-key deletes, never a whole-family
10354
- * `replace`: a replace computed from a read taken moments ago would drop a mark another
10355
- * agent wrote in between, and on the workspace object it would delete rows it never read.
10356
- *
10357
- * It swallows its own failures on purpose. Reclaiming bytes may never be the reason a
10358
- * work-start step reports a failure, because the mark it was announcing is already written.
10359
- */
10360
- async function sweepExpired(store, map, now, keep) {
10361
- let swept = 0;
10362
- for (const [path, m] of Object.entries(map || {})) {
10363
- if (swept >= MARK_SWEEP_MAX) break;
10364
- if (path === keep) continue;
10365
- if (markExpiresAt(m) > now) continue;
10366
- try { await store.set("marks", "", path, null); swept++; } catch (e) { break; }
10367
- }
10368
- return swept;
10369
- }
10370
-
10371
- /**
10372
- * Write one mark. `who` is resolved by the caller from a credential — a session or a
10373
- * publish token — and NEVER from the request body, exactly like a comment's authorship.
10374
- *
10375
- * ⚠️ ON THE KV BACKING TWO MARKS WRITTEN IN THE SAME WINDOW CAN LOSE EACH OTHER, and that
10376
- * is a known cost rather than an oversight. A `map` family is one document: `set` reads it,
10377
- * changes one key and puts it back, so a mark written between the read and the put is
10378
- * overwritten — and KV reads converge globally rather than instantly, which makes the
10379
- * window as wide as the convergence, not as wide as the round trip. The overlay's own
10380
- * header says the same thing about statuses, names and pins; the workspace object closes
10381
- * it for all of them at once by making each key a row.
10382
- *
10383
- * WHY IT IS SURVIVABLE HERE AND WOULD NOT BE IN A LOCK. A lost mark costs the next reader
10384
- * a hint. It cannot cost anybody work, because nothing anywhere asks a mark for permission:
10385
- * the loser of the race is still editing, still publishing, and still protected by the
10386
- * composed publish, which settles a real collision on evidence. A lock that lost a write
10387
- * would hand two writers the same exclusive claim, which is why this is not one.
10388
- */
10389
- async function writeMark(tctx, env, who, { path, ttl }) {
10390
- const store = overlayFor(env, tctx);
10391
- if (!store) return { error: "no-kv-binding" };
10392
- const p = normalizeMarkPath(path);
10393
- if (!p) return { error: "bad-input" };
10394
- const ms = Number.isFinite(+ttl) && +ttl > 0
10395
- ? Math.min(Math.max(+ttl, MARK_TTL_MIN_MS), MARK_TTL_MAX_MS)
10396
- : MARK_TTL_MS;
10397
- const now = Date.now();
10398
- const before = await store.read("marks");
10399
- // The only refusal on this route, and it is a runaway-loop guard rather than a policy:
10400
- // this many things being worked on at once in one workspace is a script, not a team.
10401
- const live = liveMarks(before, now);
10402
- if (live.length >= MARK_MAX_ROWS && !live.some((m) => m.path === p)) {
10403
- return { error: "too-many-marks" };
10404
- }
10405
- // ⚠️ THE ROW CARRIES NO ADDRESS — not in the value and not in the `owner` column. A mark
10406
- // is read by more things than a comment thread is (a gallery page stamps a badge from
10407
- // it), and `personId` is exactly enough to put a face on it.
10408
- const mark = { path: p, personId: who.personId, startedAt: new Date(now).toISOString(), ttl: ms };
10409
- await store.set("marks", "", p, mark, null);
10410
- const swept = await sweepExpired(store, before, now, p);
10411
- // The answer is COMPUTED from what was just written, never re-read. A second read costs
10412
- // a round trip to say what this function already knows, and on KV it can come back
10413
- // STALER than the write it was meant to confirm — a work-start step that printed "your
10414
- // mark is not there" right after writing it would teach people to distrust the tool.
10415
- const marks = [mark, ...live.filter((m) => m.path !== p)];
10416
- return {
10417
- mark: decorateMark(mark, tctx.USERS, now),
10418
- marks: marks.map((m) => decorateMark(m, tctx.USERS, now)),
10419
- swept,
10420
- };
10421
- }
10422
-
10423
- /**
10424
- * Release a mark early. A COURTESY, never the guarantee — the TTL is the guarantee, and a
10425
- * tool that is killed never reaches this. Only the mark's own author may clear it: taking
10426
- * somebody else's mark down would turn the note into something worth fighting over.
10427
- */
10428
- async function clearMark(tctx, env, who, { path }) {
10429
- const store = overlayFor(env, tctx);
10430
- if (!store) return { error: "no-kv-binding" };
10431
- const p = normalizeMarkPath(path);
10432
- if (!p) return { error: "bad-input" };
10433
- const map = await store.read("marks");
10434
- const cur = (map || {})[p];
10435
- const now = Date.now();
10436
- if (!cur || markExpiresAt(cur) <= now) return { cleared: false, reason: "no-mark" };
10437
- if (cur.personId !== who.personId) return { cleared: false, reason: "not-yours" };
10438
- await store.set("marks", "", p, null);
10439
- const marks = liveMarks(map, now).filter((m) => m.path !== p);
10440
- return { cleared: true, marks: marks.map((m) => decorateMark(m, tctx.USERS, now)) };
10441
- }
10442
-
10443
- /**
10444
- * The browser's read. GET only, on purpose — see the header: the badge is the byproduct of
10445
- * an agent protocol, and a tab is not a work-start step.
10446
- */
10447
- async function marksApi(tctx, request, url, env) {
10448
- if (request.method !== "GET") return jsonResponse({ error: "method-not-allowed" }, 405);
10449
- const out = await readMarks(tctx, env);
10450
- return jsonResponse({ ...out, ttlMs: MARK_TTL_MS });
10451
- }
10452
-
10453
10303
  // ---- Prototype deletion (repo-write via dispatch webhook) -------------------
10454
10304
  // "Delete forever" on a prototype card. The worker holds NO repo credentials —
10455
10305
  // it forwards the request to a per-instance webhook (a GitHub repository_dispatch
@@ -12299,13 +12149,6 @@ async function handleRequest(request, env, ctx, url, trace) {
12299
12149
  if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
12300
12150
  return pinsApi(tctx, request, url, env, me);
12301
12151
  }
12302
- // Working marks — READ ONLY here. Who is working where is workspace-internal, so it
12303
- // asks for a session like the gallery around it; the WRITE side is a publish token and
12304
- // lives under /__publish/_marks, because a work-start step is something a tool runs.
12305
- if (url.pathname === "/__marks") {
12306
- if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
12307
- return marksApi(tctx, request, url, env);
12308
- }
12309
12152
  if (url.pathname === "/__name") {
12310
12153
  if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
12311
12154
  const denied = viewerWriteRefusal(request, url, me, "name", tctx.SPACES);
@@ -12433,13 +12276,20 @@ async function handleRequest(request, env, ctx, url, trace) {
12433
12276
  // Past the gate (or nothing gates the site) → serve. A 404 gets one more chance
12434
12277
  // as a created canvas (a KV-registered board with no repo file — see canvasesApi).
12435
12278
  if (authed) {
12436
- const asset = await assetFetch(tctx.tenantId, env, request);
12279
+ // A design-system draft to look through (`?ds=`), remembered in a cookie.
12280
+ const ds = dsOverlay(request, url);
12281
+ const stamp = (res) => (ds.setCookie ? withSetCookie(res, ds.setCookie) : res);
12282
+ // Where drafts are served, the gallery and its indexes are derived from the live
12283
+ // store rather than read from it — a landing is on them at once.
12284
+ const derived = await derivedPage(tctx, env, url);
12285
+ if (derived) return stamp(derived.status === 308 ? derived : await serveContent(tctx, derived, url, me, env));
12286
+ const asset = await assetFetch(tctx.tenantId, env, request, { dsDraft: ds.draft });
12437
12287
  if (asset.status === 404) {
12438
12288
  const virt = await virtualCanvas(tctx, request, env, url);
12439
- if (virt) return virt;
12440
- return notFoundResponse(tctx);
12289
+ if (virt) return stamp(virt);
12290
+ return stamp(notFoundResponse(tctx));
12441
12291
  }
12442
- return serveContent(tctx, asset, url, me, env);
12292
+ return stamp(await serveContent(tctx, asset, url, me, env));
12443
12293
  }
12444
12294
 
12445
12295
  // Created canvas boards are public like published prototypes — same obscure
@@ -12504,9 +12354,6 @@ export const __testables = Object.freeze({
12504
12354
  nextPublishVersion, overlayFor, overlayKvKey, statusApi, nameApi, pinsApi,
12505
12355
  currencyApi, currencyRows, freshness, whenWords, parseSince, unitKey, unitProvenance,
12506
12356
  STALE_AFTER_DAYS, STATUS_LABELS, VALID_STATUS,
12507
- marksApi, readMarks, writeMark, clearMark, liveMarks, markExpiresAt, decorateMark,
12508
- normalizeMarkPath, markPathsOverlap, sweepExpired,
12509
- MARK_TTL_MS, MARK_TTL_MAX_MS, MARK_TTL_MIN_MS, MARK_SWEEP_MAX, MARK_MAX_ROWS,
12510
12357
  exportState, importState,
12511
12358
  quotaBump, quotaMinute, quotaDay, workspaceStatus, touchWorkspaceActivity,
12512
12359
  deleteWorkspace, purgeDue, blobGc, clearFamilies, NEVER_CLEARED,
@@ -12524,7 +12371,7 @@ export const __testables = Object.freeze({
12524
12371
  doorFacts, doorText, wantsMachineDoor, gateResponse, DOOR_DOCS, DOOR_WELL_KNOWN,
12525
12372
  resumeAfterDormancy,
12526
12373
  PITI_VIEW_KEY, PITI_REMARKS_KEY,
12527
- publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, isEngineChrome, publishRefusalBody, splitDraftPath,
12374
+ publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, derivedPage, dsOverlay, isEngineChrome, publishRefusalBody, splitDraftPath,
12528
12375
  adminStorageApi,
12529
12376
  adminCustomDomainApi,
12530
12377
  isPrefixBacked, backedPublicPrefixes,