@augurworks/augur 0.15.1

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.
Files changed (190) hide show
  1. package/CANVAS.md +858 -0
  2. package/INSTALL.md +603 -0
  3. package/LICENSE +27 -0
  4. package/README.md +150 -0
  5. package/TRADEMARKS.md +72 -0
  6. package/agents/README.md +65 -0
  7. package/agents/canon-extract.md +117 -0
  8. package/agents/canon.md +151 -0
  9. package/agents/canvas.md +88 -0
  10. package/agents/currency.md +74 -0
  11. package/agents/drafts.md +92 -0
  12. package/agents/identity.md +148 -0
  13. package/agents/prototype-contract.md +69 -0
  14. package/agents/publishing.md +324 -0
  15. package/agents/review-feedback.md +76 -0
  16. package/agents/space-json.md +132 -0
  17. package/agents/ui-skill.md +101 -0
  18. package/agents/working-marks.md +86 -0
  19. package/brand/augur-eye.svg +3 -0
  20. package/brand/augur-icon.svg +41 -0
  21. package/brand/augur-mark.svg +5 -0
  22. package/brand/lentianova-bulky.otf +0 -0
  23. package/brand/lentianova-median.otf +0 -0
  24. package/brand/lentianova.woff2 +0 -0
  25. package/build.js +8409 -0
  26. package/changelog.md +133 -0
  27. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/package.json +57 -0
  29. package/pitis/PETS.md +121 -0
  30. package/pitis/README.md +117 -0
  31. package/pitis/index.html +266 -0
  32. package/pitis/piti.build.js +81 -0
  33. package/pitis/piti.js +929 -0
  34. package/pitis/roast-agent.md +185 -0
  35. package/realtime/src/index.js +119 -0
  36. package/realtime/wrangler.example.toml +44 -0
  37. package/scripts/adopt.mjs +106 -0
  38. package/scripts/board-snapshot.mjs +197 -0
  39. package/scripts/build-seed-pack.mjs +33 -0
  40. package/scripts/bundle-rekey.mjs +112 -0
  41. package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
  42. package/scripts/canon-extract.mjs +578 -0
  43. package/scripts/canon.mjs +599 -0
  44. package/scripts/canvas-screen.mjs +333 -0
  45. package/scripts/check-local.mjs +136 -0
  46. package/scripts/clawd-art.mjs +75 -0
  47. package/scripts/clawd-canvas.mjs +741 -0
  48. package/scripts/cli.mjs +100 -0
  49. package/scripts/clone.mjs +422 -0
  50. package/scripts/close.mjs +30 -0
  51. package/scripts/connect.mjs +103 -0
  52. package/scripts/deploy.mjs +157 -0
  53. package/scripts/dev.mjs +54 -0
  54. package/scripts/doc-lint.mjs +143 -0
  55. package/scripts/export.mjs +300 -0
  56. package/scripts/fork.mjs +64 -0
  57. package/scripts/freeze.mjs +74 -0
  58. package/scripts/frontdoor-parity.mjs +180 -0
  59. package/scripts/hook.mjs +67 -0
  60. package/scripts/identity-rekey.mjs +112 -0
  61. package/scripts/init.mjs +122 -0
  62. package/scripts/land.mjs +40 -0
  63. package/scripts/lib/adapters.mjs +156 -0
  64. package/scripts/lib/board-snapshot.mjs +511 -0
  65. package/scripts/lib/draft.mjs +385 -0
  66. package/scripts/lib/graduate.mjs +276 -0
  67. package/scripts/lib/instance.mjs +74 -0
  68. package/scripts/lib/marks.mjs +107 -0
  69. package/scripts/lib/materialize.mjs +138 -0
  70. package/scripts/lib/merge3.mjs +124 -0
  71. package/scripts/lib/offline-posture.mjs +120 -0
  72. package/scripts/lib/offline-respawn.mjs +18 -0
  73. package/scripts/lib/offline-wrangler.mjs +78 -0
  74. package/scripts/lib/publish-compose.mjs +12 -0
  75. package/scripts/lib/publish-conflict.mjs +78 -0
  76. package/scripts/lib/publish-evidence.mjs +144 -0
  77. package/scripts/lib/refine-compare.mjs +140 -0
  78. package/scripts/lib/refine-ledger.mjs +150 -0
  79. package/scripts/lib/refine-png.mjs +149 -0
  80. package/scripts/lib/seed-pack-build.mjs +141 -0
  81. package/scripts/lib/state-compare.mjs +68 -0
  82. package/scripts/lib/store.mjs +122 -0
  83. package/scripts/login.mjs +84 -0
  84. package/scripts/mark.mjs +112 -0
  85. package/scripts/migrate-board-keys.mjs +164 -0
  86. package/scripts/migrate.mjs +265 -0
  87. package/scripts/no-foreign-vocabulary.mjs +216 -0
  88. package/scripts/no-product-names.mjs +157 -0
  89. package/scripts/no-tenant-globals.mjs +1134 -0
  90. package/scripts/offline.mjs +247 -0
  91. package/scripts/og.mjs +170 -0
  92. package/scripts/one-tenant-resolver.mjs +146 -0
  93. package/scripts/open.mjs +63 -0
  94. package/scripts/publish.mjs +1041 -0
  95. package/scripts/read.mjs +31 -0
  96. package/scripts/refine.mjs +455 -0
  97. package/scripts/release-drift.mjs +138 -0
  98. package/scripts/restore.mjs +322 -0
  99. package/scripts/review.mjs +125 -0
  100. package/scripts/save.mjs +21 -0
  101. package/scripts/shell-lint.mjs +189 -0
  102. package/scripts/ship.mjs +460 -0
  103. package/scripts/shoot.mjs +264 -0
  104. package/scripts/smoke-invite.mjs +113 -0
  105. package/scripts/state-inventory.mjs +223 -0
  106. package/scripts/status.mjs +177 -0
  107. package/scripts/sync.mjs +27 -0
  108. package/scripts/tenant-do-rehearsal.mjs +1314 -0
  109. package/scripts/ui-copy-lint.mjs +202 -0
  110. package/scripts/watch.mjs +32 -0
  111. package/scripts/wrangler-preflight.mjs +246 -0
  112. package/seed/CANON.md +43 -0
  113. package/seed/README.md +119 -0
  114. package/seed/prototype-status.json +9 -0
  115. package/seed/registry.json +88 -0
  116. package/seed/skills/starter-ui/SKILL.md +52 -0
  117. package/seed/skills/starter-ui/skill.json +11 -0
  118. package/seed/skills/starter-ui/starter-tokens.css +73 -0
  119. package/seed/skills/starter-ui/starter-ui.css +318 -0
  120. package/seed/skills/starter-ui/starter-ui.js +50 -0
  121. package/seed/space.json +5 -0
  122. package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
  123. package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
  124. package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
  125. package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
  126. package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
  127. package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
  128. package/seed/threads.json +52 -0
  129. package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
  130. package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
  131. package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
  132. package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
  133. package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
  134. package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
  135. package/src/_worker.js +12668 -0
  136. package/src/board-key.mjs +57 -0
  137. package/src/board-room.mjs +881 -0
  138. package/src/bundle-keys.mjs +138 -0
  139. package/src/canon/collect-in-browser.js +215 -0
  140. package/src/canon/emit.mjs +232 -0
  141. package/src/canon/observe.mjs +260 -0
  142. package/src/canon/schema.mjs +451 -0
  143. package/src/canvas/DSEG-LICENSE.txt +95 -0
  144. package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
  145. package/src/canvas/canvas.css +885 -0
  146. package/src/canvas/canvas.js +5780 -0
  147. package/src/canvas/capture.js +485 -0
  148. package/src/chrome/appchrome.mjs +568 -0
  149. package/src/currency.mjs +203 -0
  150. package/src/delete-confirmation.mjs +254 -0
  151. package/src/drafts/drafts.js +326 -0
  152. package/src/entry.js +58 -0
  153. package/src/galleries.mjs +400 -0
  154. package/src/health-cron.mjs +173 -0
  155. package/src/identity.json +1 -0
  156. package/src/kv-codec.mjs +135 -0
  157. package/src/kv-identity.mjs +259 -0
  158. package/src/mail.mjs +599 -0
  159. package/src/provenance.mjs +74 -0
  160. package/src/publish-compose.mjs +363 -0
  161. package/src/publish-fork.mjs +221 -0
  162. package/src/publish-units.mjs +44 -0
  163. package/src/purge.mjs +81 -0
  164. package/src/review/COMMENTING-UX.md +306 -0
  165. package/src/review/cat.png +0 -0
  166. package/src/review/comment-cursor.svg +7 -0
  167. package/src/review/comments.js +1738 -0
  168. package/src/room-ticket.mjs +96 -0
  169. package/src/roster-chip.mjs +32 -0
  170. package/src/seed-pack.mjs +261 -0
  171. package/src/state-inventory.mjs +249 -0
  172. package/src/sw-logic.mjs +48 -0
  173. package/src/tenant-cache.mjs +141 -0
  174. package/src/tenant-context.mjs +386 -0
  175. package/src/tenant-do.js +2940 -0
  176. package/src/tenant-host.mjs +144 -0
  177. package/src/tenant-quotas.mjs +140 -0
  178. package/src/unit-core.mjs +103 -0
  179. package/src/unit-object.mjs +338 -0
  180. package/templates/README.md +67 -0
  181. package/templates/shell/deploy.yml +145 -0
  182. package/templates/shell/engine-bump.yml +142 -0
  183. package/templates/shell/health.yml +233 -0
  184. package/templates/shell/kv-backup.yml +201 -0
  185. package/templates/shell/roster-update.yml +110 -0
  186. package/templates/shell/space-preflight.yml +35 -0
  187. package/templates/shell/space-rebake.yml +79 -0
  188. package/templates/shell/store-backup.yml +152 -0
  189. package/templates/shell/wrangler.example.toml +266 -0
  190. package/templates/space/publish.yml +91 -0
@@ -0,0 +1,149 @@
1
+ // refine-png — the smallest PNG reader/writer the refine harness needs, and no dependency.
2
+ //
3
+ // WHY NOT A LIBRARY. The comparison has to run in `npm test`, on a machine with no
4
+ // browser downloaded and no network. A pixel comparator that needs a canvas needs a
5
+ // browser; an image library is a new engine dependency, and every dependency this repo
6
+ // takes is one every instance takes at its next pin bump. The subset below is small
7
+ // enough to read in one sitting: 8-bit, non-interlaced PNG, which is the only thing a
8
+ // browser screenshot ever is.
9
+ //
10
+ // WHAT IT REFUSES, LOUDLY. 16-bit samples, interlaced (Adam7) images, and palette
11
+ // images all throw by name rather than decoding to something plausible — a comparator
12
+ // fed a silently wrong decode reports a silently wrong number, which is the one failure
13
+ // this whole harness exists to prevent.
14
+
15
+ import zlib from "node:zlib";
16
+
17
+ const SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
18
+
19
+ const CRC_TABLE = (() => {
20
+ const t = new Int32Array(256);
21
+ for (let n = 0; n < 256; n++) {
22
+ let c = n;
23
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
24
+ t[n] = c;
25
+ }
26
+ return t;
27
+ })();
28
+
29
+ function crc32(buf) {
30
+ let c = -1;
31
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
32
+ return (c ^ -1) >>> 0;
33
+ }
34
+
35
+ /** Bytes per pixel for the colour types this reader accepts. */
36
+ const CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
37
+
38
+ function paeth(a, b, c) {
39
+ const p = a + b - c;
40
+ const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
41
+ if (pa <= pb && pa <= pc) return a;
42
+ return pb <= pc ? b : c;
43
+ }
44
+
45
+ /**
46
+ * Decode a PNG buffer to `{width, height, data}` where `data` is RGBA, 8 bits per
47
+ * sample, row-major — the same shape a canvas ImageData carries.
48
+ */
49
+ export function decodePng(buf) {
50
+ if (!Buffer.isBuffer(buf) || buf.length < 8 || !buf.subarray(0, 8).equals(SIG)) {
51
+ throw new Error("not a PNG (bad signature)");
52
+ }
53
+ let off = 8;
54
+ let ihdr = null;
55
+ const idat = [];
56
+ while (off + 8 <= buf.length) {
57
+ const len = buf.readUInt32BE(off);
58
+ const type = buf.toString("latin1", off + 4, off + 8);
59
+ const body = buf.subarray(off + 8, off + 8 + len);
60
+ if (type === "IHDR") {
61
+ ihdr = {
62
+ width: body.readUInt32BE(0),
63
+ height: body.readUInt32BE(4),
64
+ bitDepth: body[8],
65
+ colorType: body[9],
66
+ compression: body[10],
67
+ filter: body[11],
68
+ interlace: body[12],
69
+ };
70
+ } else if (type === "IDAT") {
71
+ idat.push(Buffer.from(body));
72
+ } else if (type === "IEND") {
73
+ break;
74
+ }
75
+ off += 12 + len;
76
+ }
77
+ if (!ihdr) throw new Error("PNG has no IHDR chunk");
78
+ if (ihdr.bitDepth !== 8) throw new Error(`PNG bit depth ${ihdr.bitDepth} unsupported — this reader is 8-bit only`);
79
+ if (ihdr.interlace !== 0) throw new Error("interlaced PNG unsupported — this reader reads non-interlaced only");
80
+ if (!CHANNELS[ihdr.colorType]) throw new Error(`PNG colour type ${ihdr.colorType} unsupported (0, 2, 4 and 6 only)`);
81
+
82
+ const { width, height, colorType } = ihdr;
83
+ const ch = CHANNELS[colorType];
84
+ const raw = zlib.inflateSync(Buffer.concat(idat));
85
+ const stride = width * ch;
86
+ if (raw.length < (stride + 1) * height) throw new Error("PNG pixel data is short — truncated file");
87
+
88
+ // Unfilter in place into one contiguous buffer of native-channel samples.
89
+ const flat = Buffer.alloc(stride * height);
90
+ let prev = Buffer.alloc(stride);
91
+ for (let y = 0; y < height; y++) {
92
+ const ft = raw[y * (stride + 1)];
93
+ const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
94
+ const out = flat.subarray(y * stride, (y + 1) * stride);
95
+ for (let i = 0; i < stride; i++) {
96
+ const x = line[i];
97
+ const a = i >= ch ? out[i - ch] : 0;
98
+ const b = prev[i];
99
+ const c = i >= ch ? prev[i - ch] : 0;
100
+ let v;
101
+ switch (ft) {
102
+ case 0: v = x; break;
103
+ case 1: v = x + a; break;
104
+ case 2: v = x + b; break;
105
+ case 3: v = x + ((a + b) >> 1); break;
106
+ case 4: v = x + paeth(a, b, c); break;
107
+ default: throw new Error(`PNG filter type ${ft} is not one of the five defined ones`);
108
+ }
109
+ out[i] = v & 0xff;
110
+ }
111
+ prev = out;
112
+ }
113
+
114
+ // Widen to RGBA.
115
+ const data = Buffer.alloc(width * height * 4);
116
+ for (let p = 0; p < width * height; p++) {
117
+ const s = p * ch, d = p * 4;
118
+ if (colorType === 6) { data[d] = flat[s]; data[d + 1] = flat[s + 1]; data[d + 2] = flat[s + 2]; data[d + 3] = flat[s + 3]; }
119
+ else if (colorType === 2) { data[d] = flat[s]; data[d + 1] = flat[s + 1]; data[d + 2] = flat[s + 2]; data[d + 3] = 255; }
120
+ else if (colorType === 0) { data[d] = data[d + 1] = data[d + 2] = flat[s]; data[d + 3] = 255; }
121
+ else { data[d] = data[d + 1] = data[d + 2] = flat[s]; data[d + 3] = flat[s + 1]; }
122
+ }
123
+ return { width, height, data };
124
+ }
125
+
126
+ function chunk(type, body) {
127
+ const out = Buffer.alloc(body.length + 12);
128
+ out.writeUInt32BE(body.length, 0);
129
+ out.write(type, 4, "latin1");
130
+ body.copy(out, 8);
131
+ out.writeUInt32BE(crc32(out.subarray(4, 8 + body.length)), 8 + body.length);
132
+ return out;
133
+ }
134
+
135
+ /** Encode `{width, height, data}` (RGBA) back to a PNG buffer. Used for diff images and test fixtures. */
136
+ export function encodePng({ width, height, data }) {
137
+ const stride = width * 4;
138
+ const raw = Buffer.alloc((stride + 1) * height);
139
+ for (let y = 0; y < height; y++) {
140
+ raw[y * (stride + 1)] = 0; // filter: none — these images are written once and read once
141
+ data.copy ? data.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride)
142
+ : Buffer.from(data.subarray(y * stride, (y + 1) * stride)).copy(raw, y * (stride + 1) + 1);
143
+ }
144
+ const ihdr = Buffer.alloc(13);
145
+ ihdr.writeUInt32BE(width, 0);
146
+ ihdr.writeUInt32BE(height, 4);
147
+ ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
148
+ return Buffer.concat([SIG, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(raw)), chunk("IEND", Buffer.alloc(0))]);
149
+ }
@@ -0,0 +1,141 @@
1
+ // Build the seed pack: `seed/` composed by the real build, folded into ONE document.
2
+ //
3
+ // `F-seed-pack-at-provision`. The pack is what a freshly provisioned workspace is furnished
4
+ // with, and it is built ONCE PER ENGINE PIN rather than composed per signup: build.js calls
5
+ // this on every engine-only build (what a deploy shell runs) and writes the result to
6
+ // `dist/__seed/pack.json`, beside `__config/`, sealed from external requests like it.
7
+ // `node scripts/build-seed-pack.mjs` builds the same document by hand.
8
+ //
9
+ // HOW: a CHILD build of build.js with `GV_SPACES_ROOT` pointed at `seed/` and `GV_DIST` at
10
+ // a scratch directory — the same composition `augur publish` runs over a clone of the seed,
11
+ // so the pages a workspace is born with are byte-for-byte what a person would have published
12
+ // from the same tree with the same engine. The child gets NO identity file and NO deploy
13
+ // config, whatever the parent build was handed: a shell's deploy config is that instance's
14
+ // (vanity redirects, sentinels, its runtime-chrome switch) and none of it belongs in content
15
+ // that ships to every workspace on every instance. Then `__manifests/<space>.json` is read
16
+ // back, every file it names is inlined base64, and the two seed documents that are not
17
+ // files — `threads.json` and the connect-page slot — are carried beside it.
18
+ //
19
+ // WHAT IS STRIPPED, deliberately. build.js stamps every authored file with git's answer to
20
+ // "who last changed this" (`by`, a one-way person id; `editedAt`). For the seed that would
21
+ // be the engine author's id, written into every customer's workspace as the author of the
22
+ // welcome content — the exact leak the reserved actor namespace exists to prevent. The
23
+ // pack carries neither; provisioning stamps `editedAt` with its own instant and no `by`.
24
+ //
25
+ // WHAT IS ASSERTED, because the failure it catches is silent. A prototype folder the build
26
+ // did not discover (nested one level wrong — the trap `augur init` documents) builds a pack
27
+ // with five prototypes instead of six and reports success; the connect page losing its
28
+ // `CONNECT_COMMAND` slot builds a pack that provisioning can no longer fill. Both throw here.
29
+
30
+ import fs from "node:fs";
31
+ import os from "node:os";
32
+ import path from "node:path";
33
+ import { execFileSync } from "node:child_process";
34
+ import { fileURLToPath } from "node:url";
35
+ import { SEED_CONNECT_SLOT, SEED_PACK_FORMAT, validateSeedPack } from "../../src/seed-pack.mjs";
36
+
37
+ const ENGINE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
38
+
39
+ /** The page carrying the connect slot, as the site path the build emits it at. */
40
+ export const SEED_CONNECT_FILE = "/start-here/connect-your-terminal/index.html";
41
+
42
+ /** Every prototype the seed tree declares: `<project>/prototypes/<name>/index.html`. */
43
+ export function seedPrototypes(seedRoot) {
44
+ const out = [];
45
+ for (const proj of fs.readdirSync(seedRoot, { withFileTypes: true })) {
46
+ if (!proj.isDirectory()) continue;
47
+ const protos = path.join(seedRoot, proj.name, "prototypes");
48
+ if (!fs.existsSync(protos)) continue;
49
+ for (const p of fs.readdirSync(protos, { withFileTypes: true })) {
50
+ if (p.isDirectory() && fs.existsSync(path.join(protos, p.name, "index.html"))) {
51
+ out.push(`/${proj.name}/${p.name}/index.html`);
52
+ }
53
+ }
54
+ }
55
+ return out.sort();
56
+ }
57
+
58
+ /**
59
+ * Compose `seed/` with the real build and return the pack document.
60
+ * @param {{engineRoot?: string, seedRoot?: string, env?: object, node?: string}} opts
61
+ */
62
+ export function buildSeedPack({ engineRoot = ENGINE_ROOT, seedRoot = null, env = process.env, node = process.execPath } = {}) {
63
+ const seed = seedRoot || path.join(engineRoot, "seed");
64
+ const spaceJson = JSON.parse(fs.readFileSync(path.join(seed, "space.json"), "utf8"));
65
+ const spaceId = String(spaceJson.id || "");
66
+ if (!/^[a-z0-9-]+$/.test(spaceId)) throw new Error(`seed/space.json has no usable id (${JSON.stringify(spaceJson.id)})`);
67
+
68
+ const out = fs.mkdtempSync(path.join(os.tmpdir(), "augur-seed-pack-"));
69
+ try {
70
+ // Nothing GV_/AUGUR_-shaped is inherited: the parent's flags describe the parent's
71
+ // build. The child is told exactly two things — where the seed is and where to write.
72
+ const childEnv = {};
73
+ for (const [k, v] of Object.entries(env)) if (!/^(GV_|AUGUR_)/.test(k)) childEnv[k] = v;
74
+ childEnv.GV_SPACES_ROOT = seed;
75
+ childEnv.GV_DIST = out;
76
+ childEnv.GV_SEED_PACK = "0"; // never recurse
77
+ execFileSync(node, [path.join(engineRoot, "build.js")], {
78
+ cwd: engineRoot, env: childEnv, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
79
+ maxBuffer: 64 * 1024 * 1024,
80
+ });
81
+
82
+ const manifestPath = path.join(out, "__manifests", `${spaceId}.json`);
83
+ if (!fs.existsSync(manifestPath)) throw new Error(`the seed build emitted no manifest for space "${spaceId}"`);
84
+ const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
85
+
86
+ const files = {};
87
+ for (const [p, f] of Object.entries(m.files || {})) {
88
+ const bytes = fs.readFileSync(path.join(out, p.slice(1)));
89
+ const { by: _by, editedAt: _editedAt, ...entry } = f;
90
+ files[p] = { ...entry, s: bytes.length, b64: bytes.toString("base64") };
91
+ }
92
+
93
+ // Every prototype the tree declares is in the pack, at the path the site serves it.
94
+ const declared = seedPrototypes(seed);
95
+ const missing = declared.filter((p) => !(p in files));
96
+ if (!declared.length) throw new Error("seed/ declares no prototypes");
97
+ if (missing.length) throw new Error(`the seed build did not emit ${missing.join(", ")} — is the folder at <project>/prototypes/<name>/?`);
98
+
99
+ // The connect page still carries the slot provisioning fills, exactly once.
100
+ const connect = files[SEED_CONNECT_FILE];
101
+ if (!connect) throw new Error(`the seed build did not emit ${SEED_CONNECT_FILE}`);
102
+ const html = Buffer.from(connect.b64, "base64").toString("utf8");
103
+ const slots = html.split(SEED_CONNECT_SLOT).length - 1;
104
+ if (slots !== 1) throw new Error(`${SEED_CONNECT_FILE} carries the CONNECT_COMMAND slot ${slots} times; provisioning fills exactly one`);
105
+
106
+ const threadsPath = path.join(seed, "threads.json");
107
+ const threads = fs.existsSync(threadsPath) ? JSON.parse(fs.readFileSync(threadsPath, "utf8")) : {};
108
+ delete threads._comment;
109
+ for (const p of Object.keys(threads)) {
110
+ if (!p.startsWith("/") || !Array.isArray(threads[p])) throw new Error(`seed/threads.json: "${p}" is not a page path with a list of threads`);
111
+ if (!(`${p}index.html` in files)) throw new Error(`seed/threads.json names ${p}, which the seed build did not emit`);
112
+ }
113
+
114
+ const pack = {
115
+ format: SEED_PACK_FORMAT,
116
+ builtAt: new Date().toISOString(),
117
+ engine: (m.builtWith && m.builtWith.engine) || null,
118
+ space: m.space || { id: spaceId },
119
+ routing: m.routing || {},
120
+ ...(m.builtWith ? { builtWith: m.builtWith } : {}),
121
+ connectCommandFile: SEED_CONNECT_FILE,
122
+ files,
123
+ threads,
124
+ };
125
+ const why = validateSeedPack(pack);
126
+ if (why) throw new Error(`the built seed pack does not validate: ${why}`);
127
+ return pack;
128
+ } finally {
129
+ fs.rmSync(out, { recursive: true, force: true });
130
+ }
131
+ }
132
+
133
+ /** Build and write the pack to `<dist>/__seed/pack.json`. Returns the path written. */
134
+ export function writeSeedPack(dist, opts = {}) {
135
+ const pack = buildSeedPack(opts);
136
+ const dir = path.join(dist, "__seed");
137
+ fs.mkdirSync(dir, { recursive: true });
138
+ const file = path.join(dir, "pack.json");
139
+ fs.writeFileSync(file, JSON.stringify(pack));
140
+ return { file, pack };
141
+ }
@@ -0,0 +1,68 @@
1
+ // state-compare.mjs — how `augur migrate` judges whether a family arrived.
2
+ //
3
+ // Split out of migrate.mjs so the judgement can be tested without spawning the runner:
4
+ // the script is a top-level program with two instances on the other end of it, and the
5
+ // comparison is a pure function whose edge cases are exactly the ones a live migration
6
+ // finds.
7
+ //
8
+ // ⚠️ THE COMPARISON IS STRUCTURAL, NOT BYTEWISE, AND THAT IS NOT LENIENCY. The two sides
9
+ // of a migration answer the SAME documents in DIFFERENT KEY ORDERS: a KV-backed export
10
+ // hands a family back in insertion order, and the workspace object hands it back sorted
11
+ // (its read is a SELECT). `JSON.stringify(a) === JSON.stringify(b)` therefore reported
12
+ // "differ" on a correct copy, and a correct migration failed its own verification. So
13
+ // both sides are rendered as canonical JSON — object keys sorted at every depth — before
14
+ // they are compared. Arrays are left in order, because order in an array IS content: a
15
+ // comment thread with its messages reversed is a different thread.
16
+ import { inventoryEntry } from "../../src/state-inventory.mjs";
17
+
18
+ /** JSON with object keys sorted at every depth. `undefined` renders as `null`. */
19
+ export function canonicalJson(value) {
20
+ return JSON.stringify(canonicalise(value === undefined ? null : value));
21
+ }
22
+
23
+ function canonicalise(v) {
24
+ if (Array.isArray(v)) return v.map(canonicalise);
25
+ if (v && typeof v === "object") {
26
+ const out = {};
27
+ for (const k of Object.keys(v).sort()) {
28
+ if (v[k] === undefined) continue; // JSON.stringify would drop it too
29
+ out[k] = canonicalise(v[k]);
30
+ }
31
+ return out;
32
+ }
33
+ return v;
34
+ }
35
+
36
+ // ⚠️ EMPTY AND ABSENT ARE THE SAME ANSWER FOR ONE KIND OF FAMILY AND NOT FOR THE OTHER, and
37
+ // a comparison that does not know the difference fails on correct data in one direction and
38
+ // passes over a blind copy in the other. Both happened.
39
+ //
40
+ // A `key` FAMILY is one document. It is there or it is not, and "not there" is exactly
41
+ // "holds nothing" — there is no third state either end could be in. So absent on one side
42
+ // and `{}` on the other is a MATCH, and refusing it is refusing a workspace where nobody
43
+ // has ever set a status.
44
+ //
45
+ // A `prefix` FAMILY is a set of documents, and an empty set is `{}`. Absent therefore does
46
+ // NOT mean empty here: it means that export could not enumerate the family at all, which
47
+ // is a copy nobody can judge and the one report that must fail. This is not hypothetical
48
+ // tidying — `pins:` reported absent from the workspace-object backing whether it held two
49
+ // sidebars or none, so every KV→object migration failed this step on correct data, ABOVE
50
+ // the board move, which is the step that reads a board from the room that owns it.
51
+ // The export keeps the invariant now (see `exportState`); this refuses rather than
52
+ // assuming, because the next family to break it would otherwise be copied blind.
53
+ //
54
+ // AND NOTHING WITH CONTENT IN IT IS EVER FLATTENED. Both sides have to hold nothing before
55
+ // the kind is even consulted, so no amount of leniency here can make two different families
56
+ // compare equal — which is the failure mode of "just make it pass".
57
+ export const holdsNothing = (v) => v === null || v === undefined
58
+ || (Array.isArray(v) ? v.length === 0
59
+ : typeof v === "object" && Object.keys(v).length === 0);
60
+
61
+ /** `match` — the two agree. `differ` — they do not. `blind` — both empty, but one side could not enumerate it. */
62
+ export function compareFamily(id, a, b, lookup = inventoryEntry) {
63
+ if (canonicalJson(a) === canonicalJson(b)) return "match";
64
+ if (!holdsNothing(a) || !holdsNothing(b)) return "differ";
65
+ const entry = lookup(id);
66
+ if (entry && entry.kind === "key") return "match";
67
+ return "blind";
68
+ }
@@ -0,0 +1,122 @@
1
+ // store.mjs — how a CLI script finds the instance it should talk to, and how it
2
+ // talks to the bundle store's HTTP API.
3
+ //
4
+ // Extracted from publish.mjs so publish / export / restore / status all resolve the
5
+ // same target the same way. Credential resolution that drifts between tools is how
6
+ // you end up backing up one instance and restoring another.
7
+ //
8
+ // Target origin, in order: AUGUR_ORIGIN env · the engine's .env.deploy · the deploy
9
+ // shell's deploy.config.json `siteOrigin` · the cwd space's space.json `siteOrigin`
10
+ // (the collaborator layout: a lone space clone with no shell anywhere — the one
11
+ // public fact a space repo knows about its instance).
12
+ //
13
+ // Token, in order: AUGUR_TOKEN env · .env.deploy · the credential `augur login`
14
+ // saved for this origin.
15
+
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { deployConfig, originHost } from "./instance.mjs";
21
+
22
+ export const ENGINE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
23
+
24
+ export function readEnvFile(p) {
25
+ const out = {};
26
+ try {
27
+ for (const line of readFileSync(p, "utf8").split("\n")) {
28
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
29
+ if (m) out[m[1]] = m[2].trim();
30
+ }
31
+ } catch (e) {}
32
+ return out;
33
+ }
34
+
35
+ export function resolveOrigin(root = ENGINE_ROOT) {
36
+ const env = readEnvFile(path.join(root, ".env.deploy"));
37
+ let cwdSpaceOrigin = "";
38
+ try {
39
+ cwdSpaceOrigin = JSON.parse(readFileSync(path.join(process.cwd(), "space.json"), "utf8")).siteOrigin || "";
40
+ } catch (e) {}
41
+ return (process.env.AUGUR_ORIGIN || env.AUGUR_ORIGIN ||
42
+ deployConfig(root, originHost(cwdSpaceOrigin)).siteOrigin || cwdSpaceOrigin || "")
43
+ .replace(/\/+$/, "");
44
+ }
45
+
46
+ export function resolveToken(origin, root = ENGINE_ROOT) {
47
+ const env = readEnvFile(path.join(root, ".env.deploy"));
48
+ let token = process.env.AUGUR_TOKEN || env.AUGUR_TOKEN || "";
49
+ if (!token && origin) {
50
+ try {
51
+ const saved = JSON.parse(readFileSync(path.join(os.homedir(), ".config", "augur", "tokens.json"), "utf8"));
52
+ token = (saved[new URL(origin).host] || {}).token || "";
53
+ } catch (e) {}
54
+ }
55
+ return token;
56
+ }
57
+
58
+ // Both, with the failure messages a human can act on. `needToken: false` for the
59
+ // read-only paths that only touch the public build stamp.
60
+ export function target({ root = ENGINE_ROOT, needToken = true } = {}) {
61
+ const origin = resolveOrigin(root);
62
+ if (!origin) {
63
+ throw new Error('no target origin — set AUGUR_ORIGIN, or add "siteOrigin" to space.json.');
64
+ }
65
+ const token = resolveToken(origin, root);
66
+ if (needToken && !token) {
67
+ throw new Error("no publish token — run `augur connect` (or `augur login`, which uses your web credentials) once.");
68
+ }
69
+ return { origin, token };
70
+ }
71
+
72
+ // One fetch wrapper for the whole publish API: bearer auth, non-2xx throws with
73
+ // enough of the body to diagnose, 204 tolerated.
74
+ export function apiClient(origin, token) {
75
+ const headers = { Authorization: `Bearer ${token}` };
76
+ return async function req(pathPart, init = {}) {
77
+ const url = `${origin}/__publish/${pathPart}`;
78
+ const r = await fetch(url, { ...init, headers: { ...headers, ...(init.headers || {}) } });
79
+ if (!r.ok && r.status !== 204) {
80
+ const body = await r.text().catch(() => "");
81
+ throw new Error(`${init.method || "GET"} ${url} → ${r.status} ${body.slice(0, 300)}`);
82
+ }
83
+ return r;
84
+ };
85
+ }
86
+
87
+ // The public build stamp — the one read that needs no credential at all. Always
88
+ // cache-busted: the CDN serves this stale for a minute or two after a publish, and
89
+ // a stale read is how you "confirm" the previous state.
90
+ export async function buildStamp(origin) {
91
+ const r = await fetch(`${origin}/_build.json?t=${Date.now()}`, { headers: { Accept: "application/json" } });
92
+ if (!r.ok) throw new Error(`GET ${origin}/_build.json → ${r.status}`);
93
+ return r.json();
94
+ }
95
+
96
+ // Every publishable id at this instance: the spaces plus the engine-chrome
97
+ // pseudo-space, which the stamp reports separately.
98
+ export const idsFromStamp = (stamp) => [...Object.keys(stamp.spaces || {}).sort(), "_engine"];
99
+
100
+ export const ENGINE_ID = "_engine";
101
+
102
+ // The publish protocol this CLI speaks, declared on every commit as `clientProtocol`.
103
+ // One source for every client that commits (publish, restore) — two copies would drift,
104
+ // and a client that MISDECLARES its protocol is worse than one that declares nothing:
105
+ // the server would wave through guards it cannot actually honour.
106
+ //
107
+ // Bump this when the CLI learns a new commit-side guard, not when the worker does.
108
+ // 1 the original digest protocol
109
+ // 2 unpublish guard (`allowUnpublish`)
110
+ // 3 revert guard (`baseVersion` + per-unit reconciliation)
111
+ // 4 safe adoption: the reconcile writes AUTHORED bytes back into the tree (full
112
+ // build-decoration peel) and never deletes internal files live can't testify
113
+ // about. A protocol-3 reconcile wrote dist-baked pages into a space repo as one
114
+ // person's authorship and deleted research material — an instance that has seen
115
+ // that once sets `minClientProtocol: 4` and old clients self-update on contact.
116
+ // 5 composed publish: the live manifest is the base and the client ships per-unit
117
+ // fast-forwards only (git evidence), so adoption and tree writes are GONE — a
118
+ // stale checkout can no longer revert, unpublish, or fork what it never edited.
119
+ // The protocol-4 reconcile still mass-forked under a cache base (the 2026-08-22
120
+ // cascade: 392 false -conflict- units live); a pre-5 client re-litters, so the
121
+ // reference instance floors at 5.
122
+ export const CLIENT_PROTOCOL = 5;
@@ -0,0 +1,84 @@
1
+ // login.mjs — `augur login`: trade your existing web credentials for a publish
2
+ // token, saved to ~/.config/augur/tokens.json (keyed by origin host). Run once;
3
+ // `augur publish` picks the token up automatically after that.
4
+ //
5
+ // augur login [--origin https://…]
6
+ //
7
+ // Credentials: AUGUR_EMAIL / AUGUR_PASSWORD env vars, else an interactive
8
+ // prompt (password input is muted). The token is never printed.
9
+
10
+ import { createInterface } from "node:readline";
11
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { deployConfig, originHost } from "./lib/instance.mjs";
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const log = (msg) => console.error(`\x1b[32m[login]\x1b[0m ${msg}`);
19
+ const opt = (f) => { const i = process.argv.indexOf(f); return i > 0 ? process.argv[i + 1] : null; };
20
+
21
+ const cwdSpaceOrigin = (() => {
22
+ try { return JSON.parse(readFileSync(path.join(process.cwd(), "space.json"), "utf8")).siteOrigin || ""; }
23
+ catch (e) { return ""; }
24
+ })();
25
+ const ORIGIN = (opt("--origin") || process.env.AUGUR_ORIGIN ||
26
+ deployConfig(ROOT, originHost(cwdSpaceOrigin)).siteOrigin || cwdSpaceOrigin || "").replace(/\/+$/, "");
27
+ if (!ORIGIN) { log("no origin — pass --origin https://<your instance> (or add \"siteOrigin\" to space.json)"); process.exit(1); }
28
+ // Said up front, because the usual reader of this line is an agent deciding which door to
29
+ // use: this one wants a password typed into a terminal, and the other one does not.
30
+ log("login is for CI and scripts. People and agents run `augur connect` instead — it pairs a signed-in browser and never sees a password.");
31
+
32
+ function ask(question, mute) {
33
+ return new Promise((resolve) => {
34
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
35
+ if (mute) {
36
+ // Mute the echo after the prompt prints: overwrite the output hook.
37
+ const write = rl._writeToOutput.bind(rl);
38
+ rl._writeToOutput = (s) => { if (s.includes(question)) write(s); };
39
+ }
40
+ rl.question(question, (answer) => { rl.close(); if (mute) process.stderr.write("\n"); resolve(answer.trim()); });
41
+ });
42
+ }
43
+
44
+ const email = process.env.AUGUR_EMAIL || (await ask("Email: "));
45
+ const password = process.env.AUGUR_PASSWORD || (await ask("Password: ", true));
46
+ if (!email || !password) { log("email + password required."); process.exit(1); }
47
+
48
+ const r = await fetch(`${ORIGIN}/__publish/_login/token`, {
49
+ method: "POST",
50
+ headers: { "content-type": "application/json" },
51
+ body: JSON.stringify({ email, password }),
52
+ });
53
+ if (!r.ok) {
54
+ const err = await r.text().catch(() => "");
55
+ // The server distinguishes "wrong password" from "this account was reset". Surface the
56
+ // second as prose — otherwise a reset user reads `bad-credentials` and goes hunting for
57
+ // a typo in a password that no longer exists.
58
+ let parsed = null;
59
+ try { parsed = JSON.parse(err); } catch {}
60
+ if (parsed && parsed.message) log(parsed.message);
61
+ else log(`login failed (${r.status}): ${err.slice(0, 200)}`);
62
+ process.exit(1);
63
+ }
64
+ const { token, space, expiresAt } = await r.json();
65
+
66
+ const dir = path.join(os.homedir(), ".config", "augur");
67
+ mkdirSync(dir, { recursive: true });
68
+ const file = path.join(dir, "tokens.json");
69
+ let all = {};
70
+ try { all = JSON.parse(readFileSync(file, "utf8")); } catch (e) {}
71
+ // `expiresAt` is stored as well as printed: a line in a terminal is gone by the time it
72
+ // matters, and the file is what a later command can read to say "this ran out yesterday"
73
+ // instead of "forbidden". An older instance sends none, and none means it does not expire.
74
+ all[new URL(ORIGIN).host] = {
75
+ token, space, email, at: new Date().toISOString(),
76
+ ...(expiresAt ? { expiresAt } : {}),
77
+ };
78
+ writeFileSync(file, JSON.stringify(all, null, 2), { mode: 0o600 });
79
+ log(`signed in as ${email} — publish access: ${space === "*" ? "all spaces" : space}`);
80
+ console.log(`ready — \`augur publish\` will now use this login for ${ORIGIN}`);
81
+ if (expiresAt) {
82
+ const days = Math.max(0, Math.round((Date.parse(expiresAt) - Date.now()) / 86400000));
83
+ console.log(`\x1b[2mit expires in ${days} days (${expiresAt.slice(0, 10)}) — run \`augur login\` again then\x1b[0m`);
84
+ }