@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.
- package/CANVAS.md +858 -0
- package/INSTALL.md +603 -0
- package/LICENSE +27 -0
- package/README.md +150 -0
- package/TRADEMARKS.md +72 -0
- package/agents/README.md +65 -0
- package/agents/canon-extract.md +117 -0
- package/agents/canon.md +151 -0
- package/agents/canvas.md +88 -0
- package/agents/currency.md +74 -0
- package/agents/drafts.md +92 -0
- package/agents/identity.md +148 -0
- package/agents/prototype-contract.md +69 -0
- package/agents/publishing.md +324 -0
- package/agents/review-feedback.md +76 -0
- package/agents/space-json.md +132 -0
- package/agents/ui-skill.md +101 -0
- package/agents/working-marks.md +86 -0
- package/brand/augur-eye.svg +3 -0
- package/brand/augur-icon.svg +41 -0
- package/brand/augur-mark.svg +5 -0
- package/brand/lentianova-bulky.otf +0 -0
- package/brand/lentianova-median.otf +0 -0
- package/brand/lentianova.woff2 +0 -0
- package/build.js +8409 -0
- package/changelog.md +133 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/package.json +57 -0
- package/pitis/PETS.md +121 -0
- package/pitis/README.md +117 -0
- package/pitis/index.html +266 -0
- package/pitis/piti.build.js +81 -0
- package/pitis/piti.js +929 -0
- package/pitis/roast-agent.md +185 -0
- package/realtime/src/index.js +119 -0
- package/realtime/wrangler.example.toml +44 -0
- package/scripts/adopt.mjs +106 -0
- package/scripts/board-snapshot.mjs +197 -0
- package/scripts/build-seed-pack.mjs +33 -0
- package/scripts/bundle-rekey.mjs +112 -0
- package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
- package/scripts/canon-extract.mjs +578 -0
- package/scripts/canon.mjs +599 -0
- package/scripts/canvas-screen.mjs +333 -0
- package/scripts/check-local.mjs +136 -0
- package/scripts/clawd-art.mjs +75 -0
- package/scripts/clawd-canvas.mjs +741 -0
- package/scripts/cli.mjs +100 -0
- package/scripts/clone.mjs +422 -0
- package/scripts/close.mjs +30 -0
- package/scripts/connect.mjs +103 -0
- package/scripts/deploy.mjs +157 -0
- package/scripts/dev.mjs +54 -0
- package/scripts/doc-lint.mjs +143 -0
- package/scripts/export.mjs +300 -0
- package/scripts/fork.mjs +64 -0
- package/scripts/freeze.mjs +74 -0
- package/scripts/frontdoor-parity.mjs +180 -0
- package/scripts/hook.mjs +67 -0
- package/scripts/identity-rekey.mjs +112 -0
- package/scripts/init.mjs +122 -0
- package/scripts/land.mjs +40 -0
- package/scripts/lib/adapters.mjs +156 -0
- package/scripts/lib/board-snapshot.mjs +511 -0
- package/scripts/lib/draft.mjs +385 -0
- package/scripts/lib/graduate.mjs +276 -0
- package/scripts/lib/instance.mjs +74 -0
- package/scripts/lib/marks.mjs +107 -0
- package/scripts/lib/materialize.mjs +138 -0
- package/scripts/lib/merge3.mjs +124 -0
- package/scripts/lib/offline-posture.mjs +120 -0
- package/scripts/lib/offline-respawn.mjs +18 -0
- package/scripts/lib/offline-wrangler.mjs +78 -0
- package/scripts/lib/publish-compose.mjs +12 -0
- package/scripts/lib/publish-conflict.mjs +78 -0
- package/scripts/lib/publish-evidence.mjs +144 -0
- package/scripts/lib/refine-compare.mjs +140 -0
- package/scripts/lib/refine-ledger.mjs +150 -0
- package/scripts/lib/refine-png.mjs +149 -0
- package/scripts/lib/seed-pack-build.mjs +141 -0
- package/scripts/lib/state-compare.mjs +68 -0
- package/scripts/lib/store.mjs +122 -0
- package/scripts/login.mjs +84 -0
- package/scripts/mark.mjs +112 -0
- package/scripts/migrate-board-keys.mjs +164 -0
- package/scripts/migrate.mjs +265 -0
- package/scripts/no-foreign-vocabulary.mjs +216 -0
- package/scripts/no-product-names.mjs +157 -0
- package/scripts/no-tenant-globals.mjs +1134 -0
- package/scripts/offline.mjs +247 -0
- package/scripts/og.mjs +170 -0
- package/scripts/one-tenant-resolver.mjs +146 -0
- package/scripts/open.mjs +63 -0
- package/scripts/publish.mjs +1041 -0
- package/scripts/read.mjs +31 -0
- package/scripts/refine.mjs +455 -0
- package/scripts/release-drift.mjs +138 -0
- package/scripts/restore.mjs +322 -0
- package/scripts/review.mjs +125 -0
- package/scripts/save.mjs +21 -0
- package/scripts/shell-lint.mjs +189 -0
- package/scripts/ship.mjs +460 -0
- package/scripts/shoot.mjs +264 -0
- package/scripts/smoke-invite.mjs +113 -0
- package/scripts/state-inventory.mjs +223 -0
- package/scripts/status.mjs +177 -0
- package/scripts/sync.mjs +27 -0
- package/scripts/tenant-do-rehearsal.mjs +1314 -0
- package/scripts/ui-copy-lint.mjs +202 -0
- package/scripts/watch.mjs +32 -0
- package/scripts/wrangler-preflight.mjs +246 -0
- package/seed/CANON.md +43 -0
- package/seed/README.md +119 -0
- package/seed/prototype-status.json +9 -0
- package/seed/registry.json +88 -0
- package/seed/skills/starter-ui/SKILL.md +52 -0
- package/seed/skills/starter-ui/skill.json +11 -0
- package/seed/skills/starter-ui/starter-tokens.css +73 -0
- package/seed/skills/starter-ui/starter-ui.css +318 -0
- package/seed/skills/starter-ui/starter-ui.js +50 -0
- package/seed/space.json +5 -0
- package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
- package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
- package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
- package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
- package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
- package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
- package/seed/threads.json +52 -0
- package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
- package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
- package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
- package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
- package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
- package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
- package/src/_worker.js +12668 -0
- package/src/board-key.mjs +57 -0
- package/src/board-room.mjs +881 -0
- package/src/bundle-keys.mjs +138 -0
- package/src/canon/collect-in-browser.js +215 -0
- package/src/canon/emit.mjs +232 -0
- package/src/canon/observe.mjs +260 -0
- package/src/canon/schema.mjs +451 -0
- package/src/canvas/DSEG-LICENSE.txt +95 -0
- package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
- package/src/canvas/canvas.css +885 -0
- package/src/canvas/canvas.js +5780 -0
- package/src/canvas/capture.js +485 -0
- package/src/chrome/appchrome.mjs +568 -0
- package/src/currency.mjs +203 -0
- package/src/delete-confirmation.mjs +254 -0
- package/src/drafts/drafts.js +326 -0
- package/src/entry.js +58 -0
- package/src/galleries.mjs +400 -0
- package/src/health-cron.mjs +173 -0
- package/src/identity.json +1 -0
- package/src/kv-codec.mjs +135 -0
- package/src/kv-identity.mjs +259 -0
- package/src/mail.mjs +599 -0
- package/src/provenance.mjs +74 -0
- package/src/publish-compose.mjs +363 -0
- package/src/publish-fork.mjs +221 -0
- package/src/publish-units.mjs +44 -0
- package/src/purge.mjs +81 -0
- package/src/review/COMMENTING-UX.md +306 -0
- package/src/review/cat.png +0 -0
- package/src/review/comment-cursor.svg +7 -0
- package/src/review/comments.js +1738 -0
- package/src/room-ticket.mjs +96 -0
- package/src/roster-chip.mjs +32 -0
- package/src/seed-pack.mjs +261 -0
- package/src/state-inventory.mjs +249 -0
- package/src/sw-logic.mjs +48 -0
- package/src/tenant-cache.mjs +141 -0
- package/src/tenant-context.mjs +386 -0
- package/src/tenant-do.js +2940 -0
- package/src/tenant-host.mjs +144 -0
- package/src/tenant-quotas.mjs +140 -0
- package/src/unit-core.mjs +103 -0
- package/src/unit-object.mjs +338 -0
- package/templates/README.md +67 -0
- package/templates/shell/deploy.yml +145 -0
- package/templates/shell/engine-bump.yml +142 -0
- package/templates/shell/health.yml +233 -0
- package/templates/shell/kv-backup.yml +201 -0
- package/templates/shell/roster-update.yml +110 -0
- package/templates/shell/space-preflight.yml +35 -0
- package/templates/shell/space-rebake.yml +79 -0
- package/templates/shell/store-backup.yml +152 -0
- package/templates/shell/wrangler.example.toml +266 -0
- package/templates/space/publish.yml +91 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ui-copy-lint — the empty states are a ghost and ONE short line, in a human voice.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Nothing in this repo has ever constrained the text of an empty state:
|
|
6
|
+
* no test, no script, no build assertion. Every commit that ever touched `emptyState` is
|
|
7
|
+
* build.js-only, including 607c3db6, which fixed a first sentence that described a page
|
|
8
|
+
* that does not exist. That bug was found by a person opening the page in a browser, and
|
|
9
|
+
* the fix left behind no mechanism that would find the next one. Meanwhile the copy grew:
|
|
10
|
+
* eight surfaces, two paragraphs each, a mean sentence of 49.8 words, and two of them
|
|
11
|
+
* naming another company's product. The instruction that reset it was
|
|
12
|
+
* "novels full of claudisms (em dashes, expressions, terrible)".
|
|
13
|
+
*
|
|
14
|
+
* WHAT IT CAN AND CANNOT DO. It cannot tell whether a sentence is TRUE — the false first
|
|
15
|
+
* sentence would have passed every rule below — and it cannot tell whether writing is
|
|
16
|
+
* good. It catches the specific tics that produced the last one, and the structural rule
|
|
17
|
+
* that keeps a surface from growing back into a novel: one paragraph, twenty words.
|
|
18
|
+
* Truth is still a review question, and the Tokens renderer carries a comment saying so
|
|
19
|
+
* for the three readers who land on its empty branch.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IT READS. Two closed sets. The `emptyState(...)` call sites and the `addHint:`
|
|
22
|
+
* strings in build.js, which are the copy a person meets on a blank workspace. And the
|
|
23
|
+
* copy-bearing fields of src/mail.mjs — `subject:`, `footer:`, `detail:`, `message:` and
|
|
24
|
+
* every string a function returns — which are the copy a person meets in their inbox and
|
|
25
|
+
* the copy an operator meets beside the invite link. The mail file was written after this
|
|
26
|
+
* guard and so was never in front of it. That is the failure to watch for: a copy surface
|
|
27
|
+
* this file does not name is a copy surface nothing checks.
|
|
28
|
+
*
|
|
29
|
+
* THE MAIL EXTRACTOR IS FIELD-SHAPED, NOT A PARSER, AND THAT IS THE SAFE CHOICE. mail.mjs
|
|
30
|
+
* holds a regex containing a quote character and comments full of apostrophes, so a
|
|
31
|
+
* hand-rolled string tokenizer desynchronises on it and starts reporting a developer's
|
|
32
|
+
* prose as a user's — a wrong scan is worse than a narrow one. Only the DASH rule is
|
|
33
|
+
* applied there: the semicolon rule would fire on the inline CSS in the HTML mail shell,
|
|
34
|
+
* which is markup, not writing.
|
|
35
|
+
*
|
|
36
|
+
* Run: node scripts/ui-copy-lint.mjs [ROOT] (exit 1 on any finding)
|
|
37
|
+
*/
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
|
|
42
|
+
const ROOT = process.argv[2] || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
43
|
+
const BUILD = path.join(ROOT, "build.js");
|
|
44
|
+
|
|
45
|
+
const MAX_WORDS = 20; // A house sentence runs ~11 words; 20 is the ceiling, not the target.
|
|
46
|
+
const MAX_PARAS = 1; // The ghost does the explaining. A second paragraph is the novel growing back.
|
|
47
|
+
const MAX_HINT_WORDS = 34; // A populated tab's caption may say a little more; it is read beside content.
|
|
48
|
+
|
|
49
|
+
/** Pull the template literals passed to each `emptyState(` call, with their line numbers. */
|
|
50
|
+
function emptyStateCalls(src) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const NEEDLE = "emptyState(";
|
|
53
|
+
for (let i = src.indexOf(NEEDLE); i !== -1; i = src.indexOf(NEEDLE, i + 1)) {
|
|
54
|
+
// Skip a mention inside a comment or a name like `renderEmptyState(`.
|
|
55
|
+
const prev = src[i - 1];
|
|
56
|
+
if (prev && /[A-Za-z0-9_$]/.test(prev)) continue;
|
|
57
|
+
let d = 1, j = i + NEEDLE.length;
|
|
58
|
+
const paras = [];
|
|
59
|
+
while (j < src.length && d > 0) {
|
|
60
|
+
const c = src[j];
|
|
61
|
+
if (c === "(") d++;
|
|
62
|
+
else if (c === ")") d--;
|
|
63
|
+
else if (c === "`" && d === 1) {
|
|
64
|
+
// Read to the closing backtick, honouring escapes and ${...} spans.
|
|
65
|
+
let k = j + 1, lit = "";
|
|
66
|
+
while (k < src.length) {
|
|
67
|
+
if (src[k] === "\\") { lit += src[k + 1]; k += 2; continue; }
|
|
68
|
+
if (src[k] === "`") break;
|
|
69
|
+
if (src[k] === "$" && src[k + 1] === "{") {
|
|
70
|
+
let dd = 1; k += 2;
|
|
71
|
+
while (k < src.length && dd > 0) { if (src[k] === "{") dd++; else if (src[k] === "}") dd--; k++; }
|
|
72
|
+
lit += "X"; // an interpolation stands in as one word
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
lit += src[k]; k++;
|
|
76
|
+
}
|
|
77
|
+
paras.push(lit);
|
|
78
|
+
j = k;
|
|
79
|
+
}
|
|
80
|
+
j++;
|
|
81
|
+
}
|
|
82
|
+
out.push({ line: src.slice(0, i).split("\n").length, paras });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `addHint: "..."` — the caption on a POPULATED tier. */
|
|
88
|
+
function addHints(src) {
|
|
89
|
+
const out = [];
|
|
90
|
+
const re = /addHint:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
91
|
+
for (const m of src.matchAll(re)) {
|
|
92
|
+
out.push({ line: src.slice(0, m.index).split("\n").length, text: m[1].replace(/\\"/g, '"') });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The copy-bearing fields of src/mail.mjs, with their line numbers. A `subject:`,
|
|
98
|
+
* `footer:`, `detail:` or `message:` key followed by a literal, and every literal a
|
|
99
|
+
* function returns. No comment in that file contains either shape, so nothing here needs
|
|
100
|
+
* a parser and nothing here mistakes a developer's sentence for a user's. */
|
|
101
|
+
const LIT = String.raw`("(?:[^"\\]|\\.)*"|\x60(?:[^\x60\\]|\\.)*\x60)`;
|
|
102
|
+
const MAIL_FIELDS = [
|
|
103
|
+
new RegExp(String.raw`\b(?:subject|footer|detail|message):\s*` + LIT, "g"),
|
|
104
|
+
new RegExp(String.raw`\breturn\s+` + LIT + String.raw`\s*;`, "g"),
|
|
105
|
+
];
|
|
106
|
+
function mailCopy(src) {
|
|
107
|
+
const out = [], seen = new Set();
|
|
108
|
+
for (const re of MAIL_FIELDS) {
|
|
109
|
+
for (const m of src.matchAll(re)) {
|
|
110
|
+
const line = src.slice(0, m.index).split("\n").length;
|
|
111
|
+
const key = line + "|" + m[1];
|
|
112
|
+
if (seen.has(key)) continue;
|
|
113
|
+
seen.add(key);
|
|
114
|
+
out.push({ line, text: m[1].slice(1, -1) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out.sort((a, b) => a.line - b.line);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const stripTags = (s) => s.replace(/<[^>]+>/g, " ").replace(/&[a-z]+;/gi, " ").replace(/\s+/g, " ").trim();
|
|
121
|
+
const words = (s) => stripTags(s).split(/\s+/).filter(Boolean).length;
|
|
122
|
+
|
|
123
|
+
// The tics, each one quoted from the copy this replaced.
|
|
124
|
+
const TICS = [
|
|
125
|
+
[/[—–]/, "an em or en dash. Standing instruction, given three times: use a comma or a full stop."],
|
|
126
|
+
[/;/, "a semicolon. It is the pivot the generated voice leans on, and it is close to absent from the writing this copy is meant to sound like."],
|
|
127
|
+
[/\bthe honest way\b/i, '"the honest way" — an epistemic flourish, not information.'],
|
|
128
|
+
[/\bwhich is the point\b/i, '"which is the point" — a closing clause that re-states the opening.'],
|
|
129
|
+
[/\bworth having\b/i, '"worth having".'],
|
|
130
|
+
[/\breads? as\b/i, '"reads as" — the house way of describing an impression. Say what it IS.'],
|
|
131
|
+
[/\bnot\s+[a-z][\w' ]{0,28},\s*(?:but|it is|these are|they are)\b/i, 'the "not X, it is Y" antithesis. Define positively.'],
|
|
132
|
+
[/<em>/i, "an <em>. That styling exists to make a quoted agent prompt liftable, and pasteable prompts are out."],
|
|
133
|
+
[/agents\/[\w-]+\.md|registry\.json|space\.json|component-meta\.json|skill\.json/, "a filename. This copy is for a person instructing an agent, and an agent finds the contract by reading the repo."],
|
|
134
|
+
[/“|”|“|”/, "a quoted incantation. No pasteable agent prompts in the UI."],
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const findings = [];
|
|
138
|
+
const push = (line, what, detail, text, file = "build.js") => findings.push({ line, what, detail, text, file });
|
|
139
|
+
|
|
140
|
+
const src = fs.readFileSync(BUILD, "utf8");
|
|
141
|
+
|
|
142
|
+
for (const call of emptyStateCalls(src)) {
|
|
143
|
+
if (call.paras.length > MAX_PARAS) {
|
|
144
|
+
push(call.line, "too many paragraphs",
|
|
145
|
+
`${call.paras.length} paragraphs; an empty state is ${MAX_PARAS}. The ghost above it is the explanation.`,
|
|
146
|
+
stripTags(call.paras[0]).slice(0, 80));
|
|
147
|
+
}
|
|
148
|
+
for (const p of call.paras) {
|
|
149
|
+
const n = words(p);
|
|
150
|
+
if (n > MAX_WORDS) push(call.line, "too long", `${n} words; the ceiling is ${MAX_WORDS}.`, stripTags(p).slice(0, 120));
|
|
151
|
+
for (const [re, why] of TICS) if (re.test(p)) push(call.line, "claudism", why, stripTags(p).slice(0, 120));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const h of addHints(src)) {
|
|
156
|
+
const n = words(h.text);
|
|
157
|
+
if (n > MAX_HINT_WORDS) push(h.line, "hint too long", `${n} words; the ceiling is ${MAX_HINT_WORDS}.`, stripTags(h.text).slice(0, 120));
|
|
158
|
+
for (const [re, why] of TICS) {
|
|
159
|
+
if (re.source.includes("em>") || re.source.includes("ldquo")) continue; // a hint carries no prompt
|
|
160
|
+
if (re.test(h.text)) push(h.line, "claudism", why, stripTags(h.text).slice(0, 120));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── the second surface: the mail a person receives, and the notice an operator reads ──
|
|
165
|
+
const MAIL = path.join(ROOT, "src", "mail.mjs");
|
|
166
|
+
const DASH = TICS[0];
|
|
167
|
+
let mailRead = 0;
|
|
168
|
+
if (fs.existsSync(MAIL)) {
|
|
169
|
+
const msrc = fs.readFileSync(MAIL, "utf8");
|
|
170
|
+
const strings = mailCopy(msrc);
|
|
171
|
+
mailRead = strings.length;
|
|
172
|
+
// Same refusal as below, for the same reason: a scan that found nothing because it
|
|
173
|
+
// looked at nothing is the failure this file exists to prevent.
|
|
174
|
+
if (!mailRead) {
|
|
175
|
+
console.log("ui-copy-lint: FAILED — src/mail.mjs exists but no subject/footer/detail/return copy was found in it. The extractor is broken, not the copy.");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
for (const s of strings) {
|
|
179
|
+
if (DASH[0].test(s.text)) push(s.line, "claudism", DASH[1], s.text.slice(0, 120), "src/mail.mjs");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A guard that finds nothing because it LOOKED at nothing is the failure mode this whole
|
|
184
|
+
// file exists to prevent, so say what was read and refuse to pass on an empty scan.
|
|
185
|
+
const scanned = emptyStateCalls(src).length;
|
|
186
|
+
if (!scanned) {
|
|
187
|
+
console.log("ui-copy-lint: FAILED — found no emptyState() call sites in build.js. The extractor is broken, not the copy.");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!findings.length) {
|
|
192
|
+
console.log(`ui-copy-lint: OK — ${scanned} empty state(s), ${addHints(src).length} tier hint(s) and ${mailRead} mail string(s) read, all within one paragraph and ${MAX_WORDS} words`);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
for (const f of findings) {
|
|
196
|
+
console.log(`${f.file}:${f.line} [${f.what}]`);
|
|
197
|
+
console.log(` ${f.detail}`);
|
|
198
|
+
console.log(` ${f.text}`);
|
|
199
|
+
}
|
|
200
|
+
console.log(`\n${findings.length} finding(s) across ${scanned} empty state(s).`);
|
|
201
|
+
console.log("This guard cannot tell whether a sentence is TRUE — the false first sentence that shipped here would have passed every rule above. That is still a review question.");
|
|
202
|
+
process.exit(1);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// augur watch — save this draft folder on every burst of changes, until Ctrl-C. For people
|
|
3
|
+
// editing by hand in an editor that runs no hooks. See docs/drafts-that-land.md §7.
|
|
4
|
+
import { resolveOrigin, resolveToken } from "./lib/store.mjs";
|
|
5
|
+
import { readState, unitClient, doSave, watchFolder } from "./lib/draft.mjs";
|
|
6
|
+
|
|
7
|
+
const log = (m) => console.error(`\x1b[35m[watch]\x1b[0m ${m}`);
|
|
8
|
+
const die = (m) => { console.error(`\x1b[31m[watch]\x1b[0m ${m}`); process.exit(1); };
|
|
9
|
+
const dir = process.cwd();
|
|
10
|
+
const st = readState(dir);
|
|
11
|
+
if (!st) die("not a draft folder — run `augur open <prototype>` first.");
|
|
12
|
+
const origin = st.origin || resolveOrigin();
|
|
13
|
+
const token = resolveToken(origin);
|
|
14
|
+
if (!token) die("no publish token — run `augur connect` once.");
|
|
15
|
+
const client = unitClient({ origin, token, space: st.space, session: st.session });
|
|
16
|
+
let running = false, again = false;
|
|
17
|
+
async function save() {
|
|
18
|
+
if (running) { again = true; return; }
|
|
19
|
+
running = true;
|
|
20
|
+
try {
|
|
21
|
+
const r = await doSave({ client, dir });
|
|
22
|
+
if (!r.ok) log(`save refused: ${r.error || r.status}${r.error === "stale-draft" ? " — run `augur sync`" : ""}`);
|
|
23
|
+
else if (r.changed.length) log(`${r.changed.length} file(s) live at ${origin}${st.address}`);
|
|
24
|
+
} finally {
|
|
25
|
+
running = false;
|
|
26
|
+
if (again) { again = false; save(); }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
log(`watching ${dir} — live at ${origin}${st.address} (Ctrl-C to stop)`);
|
|
30
|
+
const w = watchFolder(dir, save);
|
|
31
|
+
process.on("SIGINT", () => { w.close(); process.exit(0); });
|
|
32
|
+
await save();
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wrangler-preflight — refuse a worker config that would open the site.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS NOT A STYLE CHECK. Pages and Workers invert request precedence. Pages runs
|
|
6
|
+
* the worker first and lets it decide what is public. Workers serves a matching static
|
|
7
|
+
* asset first and only invokes the worker on a miss, unless `run_worker_first = true`.
|
|
8
|
+
*
|
|
9
|
+
* The asset directory is `dist`, and `dist/__config/instance.json` holds the instance's
|
|
10
|
+
* user roster INCLUDING SEED PASSWORDS — build.js writes `users: IDENTITY` unstripped and
|
|
11
|
+
* says so in its own comment beside the write. `dist/admin/` is the admin page. In assets
|
|
12
|
+
* mode `dist` is every built page.
|
|
13
|
+
*
|
|
14
|
+
* So a config missing one line serves the roster and the admin panel to strangers, while
|
|
15
|
+
* every test in this repo stays green, both existing deploy gates stay green, and the
|
|
16
|
+
* shell's health check still answers "healthy" — because /_build.json is a static file
|
|
17
|
+
* that a bare host serves correctly with no worker running at all. Nothing else in this
|
|
18
|
+
* repo can tell those two deploys apart. That is what this script is for.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ WHERE IT ACTUALLY RUNS, because this header used to claim otherwise: NOT in `check`.
|
|
21
|
+
* The config it judges lives in a SHELL repo, and this repo has none, so there is nothing
|
|
22
|
+
* here for a repo-wide gate to read. It runs at `templates/shell/deploy.yml`, gating each
|
|
23
|
+
* shell's plain-Worker deploy with the wrangler.toml that shell actually holds, and in
|
|
24
|
+
* `test/wrangler-preflight.test.mjs` against fixtures. The consequence worth knowing: a
|
|
25
|
+
* green `check` in the engine says nothing about whether any instance's config is honest.
|
|
26
|
+
*
|
|
27
|
+
* WHAT IT DOES NOT DO: it does not parse TOML. Node has no TOML parser and this repo
|
|
28
|
+
* carries no dependencies for tooling (scripts/shell-lint.mjs works the same way). It is
|
|
29
|
+
* line-oriented, which means it can be fooled by a config written to fool it — an inline
|
|
30
|
+
* table, a multi-line array, a key inside a string. It is a floor under an honest config,
|
|
31
|
+
* not a proof against a hostile one. `scripts/frontdoor-parity.mjs` is what actually
|
|
32
|
+
* asks a running deployment whether a gated path is gated.
|
|
33
|
+
*
|
|
34
|
+
* Run: node scripts/wrangler-preflight.mjs -c <path/to/wrangler.toml> (exit 1 on any finding)
|
|
35
|
+
*/
|
|
36
|
+
import fs from "node:fs";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
|
|
39
|
+
const argv = process.argv.slice(2);
|
|
40
|
+
const cfgArg = argv[(argv.indexOf("-c") + 1) || (argv.indexOf("--config") + 1)] || argv[0];
|
|
41
|
+
if (!cfgArg) {
|
|
42
|
+
console.error("usage: node scripts/wrangler-preflight.mjs -c <wrangler.toml>");
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
const CFG = path.resolve(cfgArg);
|
|
46
|
+
if (!fs.existsSync(CFG)) {
|
|
47
|
+
console.error(`wrangler-preflight: no such config: ${CFG}`);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
const SHELL = path.dirname(CFG);
|
|
51
|
+
const raw = fs.readFileSync(CFG, "utf8");
|
|
52
|
+
|
|
53
|
+
// Strip whole-line comments only. A `#` inside a quoted value is a legitimate character
|
|
54
|
+
// (a password in a var, a fragment in a URL) and treating it as a comment is how a
|
|
55
|
+
// 24-character secret became a 6-character one somewhere else in this project.
|
|
56
|
+
const lines = raw.split(/\r?\n/).map((l) => (/^\s*#/.test(l) ? "" : l));
|
|
57
|
+
const body = lines.join("\n");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A value, with a TRAILING comment removed and a quoted `#` left alone.
|
|
61
|
+
*
|
|
62
|
+
* Both halves are load-bearing and they pull in opposite directions. The shipped
|
|
63
|
+
* template annotates almost every line (`binding = "ASSETS" # the worker reads its own
|
|
64
|
+
* config through this`), so a reader that keeps the comment sees the binding as
|
|
65
|
+
* `"ASSETS" # the worker…` and refuses a correct config — and a guard that fires on the
|
|
66
|
+
* template it tells you to copy is a guard somebody deletes. But a `#` INSIDE a quoted
|
|
67
|
+
* value is a legitimate character, and treating it as a comment is how a 24-character
|
|
68
|
+
* secret became a 6-character one somewhere else in this project.
|
|
69
|
+
*
|
|
70
|
+
* So: a quoted value ends at its closing quote and whatever follows is a comment; an
|
|
71
|
+
* unquoted one ends at the first `#`.
|
|
72
|
+
*/
|
|
73
|
+
function stripTrailingComment(v) {
|
|
74
|
+
const q = /^\s*(["'])((?:\\.|(?!\1).)*)\1/.exec(v);
|
|
75
|
+
if (q) return q[2];
|
|
76
|
+
const hash = v.indexOf("#");
|
|
77
|
+
return (hash === -1 ? v : v.slice(0, hash)).trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A bare `key = value` at top level or inside the named table. */
|
|
81
|
+
function valueOf(key, table = null) {
|
|
82
|
+
let cur = null;
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const t = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*$/);
|
|
85
|
+
if (t) { cur = t[1].trim(); continue; }
|
|
86
|
+
if (cur !== table) continue;
|
|
87
|
+
const m = line.match(new RegExp(String.raw`^\s*${key}\s*=\s*(.+?)\s*$`));
|
|
88
|
+
if (m) return stripTrailingComment(m[1]);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// `key` because the tables do not agree: KV, R2 and queues name theirs `binding`, and
|
|
93
|
+
// Durable Objects name theirs `name`.
|
|
94
|
+
const bindings = (table, key = "binding") => lines
|
|
95
|
+
.join("\n")
|
|
96
|
+
.split(/^\s*\[\[/m)
|
|
97
|
+
.filter((chunk) => chunk.startsWith(`${table}]]`))
|
|
98
|
+
.map((chunk) => (chunk.match(new RegExp(String.raw`^\s*${key}\s*=\s*["']([^"']+)["']`, "m")) || [])[1])
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
|
|
101
|
+
const findings = [];
|
|
102
|
+
const fail = (rule, detail) => findings.push({ rule, detail });
|
|
103
|
+
|
|
104
|
+
// ── the headline rule ────────────────────────────────────────────────────────
|
|
105
|
+
const assetsDir = valueOf("directory", "assets");
|
|
106
|
+
const runWorkerFirst = valueOf("run_worker_first", "assets");
|
|
107
|
+
if (assetsDir) {
|
|
108
|
+
const abs = path.resolve(SHELL, assetsDir);
|
|
109
|
+
const sensitive = ["__config/instance.json", "__config/routing.json", "admin/index.html"]
|
|
110
|
+
.filter((p) => fs.existsSync(path.join(abs, p)));
|
|
111
|
+
if (runWorkerFirst !== "true") {
|
|
112
|
+
fail("run-worker-first",
|
|
113
|
+
`[assets] run_worker_first is ${runWorkerFirst === null ? "not set" : runWorkerFirst}. Workers serves a matching asset BEFORE the worker runs, so the gate never executes.`
|
|
114
|
+
+ (sensitive.length
|
|
115
|
+
? `\n The asset directory (${assetsDir}) currently contains: ${sensitive.join(", ")}.`
|
|
116
|
+
+ "\n __config/instance.json carries the user roster WITH SEED PASSWORDS. This config would publish it."
|
|
117
|
+
: `\n The asset directory (${assetsDir}) is not built yet, so what it will contain could not be read — build first, or fix this anyway.`));
|
|
118
|
+
}
|
|
119
|
+
if (!fs.existsSync(path.join(abs, ".assetsignore")) && fs.existsSync(path.join(abs, "_worker.js"))) {
|
|
120
|
+
fail("assetsignore",
|
|
121
|
+
`${assetsDir}/_worker.js exists with no .assetsignore beside it. wrangler refuses to upload a Pages worker as an asset, and without the ignore file the deploy fails. build.js emits it; a stale dist does not have it.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!assetsDir) fail("assets", "no [assets] directory. The worker reads its own instance config through env.ASSETS and cannot start without it.");
|
|
125
|
+
|
|
126
|
+
const nfh = valueOf("not_found_handling", "assets");
|
|
127
|
+
if (nfh === "single-page-application") {
|
|
128
|
+
fail("not-found-handling",
|
|
129
|
+
'not_found_handling = "single-page-application" answers every unknown path with the index page at status 200. dist/404.html exists precisely so a miss is a miss; this turns every typo and every unpublished URL into a page that looks real.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── bindings the worker cannot run without ───────────────────────────────────
|
|
133
|
+
const kv = bindings("kv_namespaces");
|
|
134
|
+
const r2 = bindings("r2_buckets");
|
|
135
|
+
if (valueOf("binding", "assets") !== "ASSETS") fail("binding-assets", "[assets] binding must be \"ASSETS\" — src/_worker.js reads env.ASSETS by that name.");
|
|
136
|
+
if (!kv.includes("COMMENTS")) fail("binding-comments", "no COMMENTS KV binding. Sessions, rosters, comments, pins and publish tokens all live there, and effectiveSecret fails CLOSED on a KV error, so nobody can sign in.");
|
|
137
|
+
if (!r2.includes("BUNDLES")) fail("binding-bundles", "no BUNDLES R2 binding. Published content is served from the bundle store; without it the site has nothing in it.");
|
|
138
|
+
|
|
139
|
+
// ── one workspace or many, and the pieces that have to agree ─────────────────
|
|
140
|
+
// TENANT_HOST_SUFFIX is the switch: unset, the deployment serves the one workspace its
|
|
141
|
+
// build named; set, the workspace comes from the Host header. The two halves are declared
|
|
142
|
+
// in different tables, so it is easy to add one and not the other — and each half alone
|
|
143
|
+
// fails in a way nobody would connect to this file.
|
|
144
|
+
const suffix = valueOf("TENANT_HOST_SUFFIX", "vars");
|
|
145
|
+
const dos = bindings("durable_objects.bindings", "name");
|
|
146
|
+
if (suffix !== null && suffix.trim() !== "" && !dos.includes("TENANTS")) {
|
|
147
|
+
fail("tenants-binding",
|
|
148
|
+
`TENANT_HOST_SUFFIX = "${suffix}" makes the workspace come from the Host header, but there is no TENANTS Durable Object binding for those workspaces to live in. Every hostname would resolve to a workspace with nowhere to keep anything.`);
|
|
149
|
+
}
|
|
150
|
+
// A deployment that provisions workspaces furnishes each one from the seed pack in its own
|
|
151
|
+
// asset bundle (`F-seed-pack-at-provision`). A worker deployed without one refuses every
|
|
152
|
+
// provisioning (`seed-pack-unavailable`) — loud, but a signup door that refuses everybody
|
|
153
|
+
// is still a broken signup door, and the build that forgot the pack is the one to stop.
|
|
154
|
+
// build.js emits it on every engine-only build; a hand-rolled or stale dist does not have it.
|
|
155
|
+
if (suffix !== null && suffix.trim() !== "" && dos.includes("TENANTS") && assetsDir) {
|
|
156
|
+
const abs = path.resolve(SHELL, assetsDir);
|
|
157
|
+
if (fs.existsSync(path.join(abs, "__config")) && !fs.existsSync(path.join(abs, "__seed", "pack.json"))) {
|
|
158
|
+
fail("seed-pack",
|
|
159
|
+
`${assetsDir} is built but carries no __seed/pack.json. This deployment provisions workspaces (TENANT_HOST_SUFFIX + TENANTS), and every provisioning furnishes the new workspace from that pack — without it the workspace object refuses every create with seed-pack-unavailable. Build with GV_ENGINE_ONLY=1 (which emits it), or GV_SEED_PACK=1.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (suffix !== null && suffix.trim() === "") {
|
|
163
|
+
fail("tenants-suffix-empty",
|
|
164
|
+
'TENANT_HOST_SUFFIX is set to an empty string. That reads as "multi-workspace" to a person and as "single workspace" to the resolver. Delete the line, or give it the real suffix.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── the jurisdiction a workspace object is addressed in ──────────────────────
|
|
168
|
+
// A Durable Object's jurisdiction is chosen when the object is ADDRESSED, not when the
|
|
169
|
+
// namespace is declared, so there is nothing in this file for the platform to check and
|
|
170
|
+
// nothing in the platform for this file to read: `ns.idFromName(x)` and
|
|
171
|
+
// `ns.jurisdiction("eu").idFromName(x)` are two different objects and a deployment picks
|
|
172
|
+
// which by setting this variable or not. Storage belongs to an id, so the wrong choice is
|
|
173
|
+
// not a bug that gets fixed later — it is a migration, and one nobody can do for a
|
|
174
|
+
// workspace they cannot find.
|
|
175
|
+
//
|
|
176
|
+
// THIS LIST IS THE ENGINE'S COPY OF SOMEBODY ELSE'S, and it exists HERE and not in the
|
|
177
|
+
// request path on purpose. In the worker the value is handed straight to the platform,
|
|
178
|
+
// which is the only authority on what it accepts — a copy running on every request would
|
|
179
|
+
// eventually refuse a jurisdiction that was added after it was written. A copy in a deploy
|
|
180
|
+
// gate has the opposite failure: whoever hits a stale entry is a person, holding the repo,
|
|
181
|
+
// one line from adding it, and in exchange a typo is caught before a single request rather
|
|
182
|
+
// than by an outage. Measured against the platform: it accepts these four and refuses
|
|
183
|
+
// everything else, case-sensitively, including the empty string.
|
|
184
|
+
const JURISDICTIONS = ["eu", "fedramp", "fedramp-high", "us"];
|
|
185
|
+
const jurisdiction = valueOf("TENANT_JURISDICTION", "vars");
|
|
186
|
+
if (jurisdiction !== null && jurisdiction.trim() === "") {
|
|
187
|
+
fail("tenants-jurisdiction-empty",
|
|
188
|
+
'TENANT_JURISDICTION is set to an empty string, which the engine reads as "no jurisdiction" and a person reads as "restricted". Delete the line if this deployment places no restriction, or name the jurisdiction.');
|
|
189
|
+
}
|
|
190
|
+
if (jurisdiction !== null && jurisdiction.trim() !== "") {
|
|
191
|
+
const j = jurisdiction.trim();
|
|
192
|
+
if (!JURISDICTIONS.includes(j)) {
|
|
193
|
+
fail("tenants-jurisdiction-unknown",
|
|
194
|
+
`TENANT_JURISDICTION = "${j}" is not a jurisdiction the platform accepts (${JURISDICTIONS.join(", ")}) — and it is case-sensitive, so "EU" is not "eu". A value it refuses fails every request; a value it accepts but nothing else uses creates every workspace where nothing else is looking. If the platform has added one since this list was written, add it here.`);
|
|
195
|
+
}
|
|
196
|
+
if (!dos.includes("TENANTS")) {
|
|
197
|
+
fail("tenants-jurisdiction-binding",
|
|
198
|
+
`TENANT_JURISDICTION = "${j}" says where this deployment's workspace objects live, but there is no TENANTS Durable Object binding for them to live in. The variable does nothing here, which is the dangerous kind of nothing: adding the binding later would look like the jurisdiction had been in force all along.`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── the entry ────────────────────────────────────────────────────────────────
|
|
203
|
+
const main = valueOf("main");
|
|
204
|
+
if (!main) fail("main", "no `main`. wrangler has no entry to bundle.");
|
|
205
|
+
else if (!/src\/entry\.js$/.test(main)) {
|
|
206
|
+
fail("main", `main = "${main}". The deploy entry is src/entry.js — the file scripts/no-tenant-globals.mjs scans and the only one that may export a Durable Object class. Pointing main elsewhere silently un-scans the module graph.`);
|
|
207
|
+
} else if (!fs.existsSync(path.resolve(SHELL, main))) {
|
|
208
|
+
fail("main", `main = "${main}" does not resolve from ${SHELL}. It is relative to this config, not to the engine.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── credentials that must never be a plaintext var ───────────────────────────
|
|
212
|
+
let table = null;
|
|
213
|
+
lines.forEach((line, i) => {
|
|
214
|
+
const t = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*$/);
|
|
215
|
+
if (t) { table = t[1].trim(); return; }
|
|
216
|
+
const m = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=/);
|
|
217
|
+
if (!m) return;
|
|
218
|
+
const key = m[1];
|
|
219
|
+
if (key === "PUBLISH_BOOTSTRAP_TOKEN") {
|
|
220
|
+
fail("bootstrap-token", `line ${i + 1}: PUBLISH_BOOTSTRAP_TOKEN. It is compared as a bare string and answers with star scope — a plaintext credential that can overwrite every space's published content. It has no place on a live instance at all.`);
|
|
221
|
+
}
|
|
222
|
+
if (table === "vars" && /SECRET|TOKEN|PASSWORD|API_KEY/.test(key)) {
|
|
223
|
+
fail("secret-in-vars", `line ${i + 1}: ${key} under [vars]. [vars] is plaintext in a git repo. Use \`wrangler secret put ${key}\`.`);
|
|
224
|
+
}
|
|
225
|
+
if (key === "GV_KV_TOKEN") {
|
|
226
|
+
fail("kv-token", `line ${i + 1}: GV_KV_TOKEN short-circuits the env.COMMENTS binding and talks to the KV REST API with an account credential. It is a local-development escape hatch, not a deploy setting.`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// `remote = true` sends a local dev binding to the PRODUCTION resource.
|
|
231
|
+
lines.forEach((line, i) => {
|
|
232
|
+
if (/^\s*remote\s*=\s*true\s*$/.test(line)) {
|
|
233
|
+
fail("remote-binding", `line ${i + 1}: remote = true points this binding at the live resource. A local run then writes production comments, rosters and boards.`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!findings.length) {
|
|
238
|
+
console.log(`wrangler-preflight: OK — ${path.basename(CFG)} runs the worker first, binds ASSETS + COMMENTS + BUNDLES, and carries no plaintext credential`);
|
|
239
|
+
process.exit(0);
|
|
240
|
+
}
|
|
241
|
+
for (const f of findings) {
|
|
242
|
+
console.log(`${path.relative(process.cwd(), CFG)} [${f.rule}]`);
|
|
243
|
+
console.log(` ${f.detail}`);
|
|
244
|
+
}
|
|
245
|
+
console.log(`\n${findings.length} finding(s). This is a floor under an honest config, not a proof: it is line-oriented and does not parse TOML. Ask a RUNNING deployment with scripts/frontdoor-parity.mjs.`);
|
|
246
|
+
process.exit(1);
|
package/seed/CANON.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# The canon
|
|
2
|
+
|
|
3
|
+
The screens and parts in this workspace that are meant to be **pulled by name** —
|
|
4
|
+
"build it the way `invoice-detail` is built", "pull `invoice-list`,
|
|
5
|
+
`invoice-detail` and `invoice-empty` and wire them together".
|
|
6
|
+
|
|
7
|
+
A canonical name is a directory name, in one of four places at this root:
|
|
8
|
+
|
|
9
|
+
| Where | What it holds |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `base/<name>/` | one atom, every state on one page |
|
|
12
|
+
| `components/<name>/` | one composed component |
|
|
13
|
+
| `patterns/<name>/` | an arrangement several screens repeat |
|
|
14
|
+
| `pages/<name>/` | a whole screen |
|
|
15
|
+
|
|
16
|
+
Anything else is not the canon and is never what a bare name means: a folder under
|
|
17
|
+
`<project>/prototypes/` is a working prototype, `playground/` is scratch.
|
|
18
|
+
|
|
19
|
+
Names are lowercase and hyphenated, **subject first and qualifier last** —
|
|
20
|
+
`invoice-list`, `invoice-detail`, `invoice-empty`, never `list-of-invoices` — so
|
|
21
|
+
a directory listing sorts a subject's screens together and reads as a table of
|
|
22
|
+
contents. The qualifier comes from one closed set:
|
|
23
|
+
|
|
24
|
+
`-list` · `-detail` · `-new` · `-edit` · `-empty` · `-error` · `-loading` · `-confirm` · `-success`
|
|
25
|
+
|
|
26
|
+
A name never carries a version, a date or a ticket id: it says what, never when.
|
|
27
|
+
Every entry carries one sentence in `<meta name="description">` saying what it
|
|
28
|
+
shows.
|
|
29
|
+
|
|
30
|
+
**To pull `<name>`, look for `<name>/` in those four directories.** The entry
|
|
31
|
+
page is `<tier>/<name>/index.html` and it opens on its own, from disk, with the
|
|
32
|
+
workspace's design system already on it. That is the whole resolution rule, and
|
|
33
|
+
it needs no tool.
|
|
34
|
+
|
|
35
|
+
With the engine's CLI on hand, the same thing plus the description and the
|
|
36
|
+
design-system assets each entry links:
|
|
37
|
+
|
|
38
|
+
augur canon find <name> # → the tier, the folder, the entry, what it links
|
|
39
|
+
augur canon list # → every canonical name and its description
|
|
40
|
+
augur canon save <path> # → promote a working screen into the canon
|
|
41
|
+
augur canon check # → names that will not be found the same way twice
|
|
42
|
+
|
|
43
|
+
The full rules, with the reasoning, are the engine's `agents/canon.md`.
|
package/seed/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The seed workspace
|
|
2
|
+
|
|
3
|
+
What a brand-new hosted workspace contains on the day it is created. It is a
|
|
4
|
+
complete, buildable space — a design system, six prototypes, a comment thread or
|
|
5
|
+
two — copied wholesale into a tenant at provisioning, so the first thing a person
|
|
6
|
+
sees is a working workspace rather than an empty one.
|
|
7
|
+
|
|
8
|
+
The three prototypes under `start-here/` **are** the onboarding. There is no
|
|
9
|
+
wizard to write instead: they teach the loop by being it, and the person who
|
|
10
|
+
finishes them has a connected terminal, a design system they have already
|
|
11
|
+
changed, and a comment they have already replied to.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
seed/
|
|
15
|
+
├── space.json the workspace's contract with the build
|
|
16
|
+
├── CANON.md how a canonical screen is named, for the agent that arrives cold
|
|
17
|
+
├── registry.json the design system's overlay catalog
|
|
18
|
+
├── prototype-status.json the dev-status chips
|
|
19
|
+
├── threads.json the comment threads that ship with the content
|
|
20
|
+
├── skills/starter-ui/ the design system: tokens, components, one behaviour
|
|
21
|
+
├── start-here/prototypes/
|
|
22
|
+
│ ├── connect-your-terminal the fork: one command, or fifteen minutes
|
|
23
|
+
│ ├── set-up-your-design-system the tokens, live, and how to make them yours
|
|
24
|
+
│ └── sample-with-comments a real screen with real pins on it
|
|
25
|
+
└── worked-examples/prototypes/
|
|
26
|
+
├── specimen-viewer 3D, from a pinned CDN script
|
|
27
|
+
├── slide-deck a keyboard-driven deck
|
|
28
|
+
└── field-readings charts with no chart library
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How it reaches a workspace
|
|
32
|
+
|
|
33
|
+
It is built ONCE PER ENGINE PIN, not composed per signup. Every engine-only build
|
|
34
|
+
(what a deploy shell runs) composes this tree with the real build — a child
|
|
35
|
+
`build.js` over `seed/`, exactly what `augur publish` would run over a clone of it —
|
|
36
|
+
and folds the result into one document, `dist/__seed/pack.json`, which ships inside
|
|
37
|
+
the worker's own asset bundle and is sealed from the outside like `/__config/`
|
|
38
|
+
(`scripts/lib/seed-pack-build.mjs`; `node scripts/build-seed-pack.mjs --print` shows
|
|
39
|
+
what is in it).
|
|
40
|
+
|
|
41
|
+
At provisioning, the workspace object writes that pack into its own segment of the
|
|
42
|
+
bundle store FIRST — every blob, then `versions/1.json`, then `manifest.json`, as
|
|
43
|
+
version 1 of the workspace's space — and only then commits the first admin, the
|
|
44
|
+
threads and the version row in one transaction (`src/seed-pack.mjs`; the control
|
|
45
|
+
plane asks for it with `seedPack: true` on `provision` and carries none of the
|
|
46
|
+
content). Published content and the workspace's own rows live in two stores with no
|
|
47
|
+
transaction between them, so the order is what makes it safe: a workspace the front
|
|
48
|
+
door will serve is one whose commit landed, and an object left unprovisioned by a
|
|
49
|
+
crash in between resolves to nobody, content or no content. There is no state where a
|
|
50
|
+
workspace exists with an admin and no content.
|
|
51
|
+
|
|
52
|
+
Every seed version is stamped as the platform's, never as a person's: `source` is
|
|
53
|
+
the seed sentinel (`src/provenance.mjs`), `publishedBy` is the seed actor, each
|
|
54
|
+
unit's `routing.unitSources` entry is the sentinel too, and no file carries an author
|
|
55
|
+
id — the pack builder strips the git-derived stamp, so the engine's author is not the
|
|
56
|
+
author of every workspace's welcome content.
|
|
57
|
+
|
|
58
|
+
Three things are substituted on the way in:
|
|
59
|
+
|
|
60
|
+
| What | Where | Substituted with |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
|
|
63
|
+
| Comment timestamps | `at` in `threads.json` | the provisioning time, so day-one threads do not read as months old |
|
|
64
|
+
| File timestamps | `editedAt` on every file in the manifest | the same provisioning time, all of them |
|
|
65
|
+
|
|
66
|
+
The space id and name are the pack's own (`space.json` here): the workspace IS the
|
|
67
|
+
space, and a workspace's label is its address, not its space id. The connect page
|
|
68
|
+
falls back to deriving the command from the URL it is served on, so with no
|
|
69
|
+
substitution it is never wrong, only less specific.
|
|
70
|
+
|
|
71
|
+
**Start Here has to be the first card.** The gallery orders projects
|
|
72
|
+
most-recently-worked-on first and falls back to A→Z, which is why the second
|
|
73
|
+
folder is named `worked-examples` rather than `examples`: with one timestamp
|
|
74
|
+
across the whole seed — what a single atomic write produces — Start Here leads
|
|
75
|
+
and the examples follow. Provisioning stamps every file with the one provisioning
|
|
76
|
+
instant, which is that single timestamp.
|
|
77
|
+
|
|
78
|
+
## Rules for editing it
|
|
79
|
+
|
|
80
|
+
- **Generic, always.** This ships from the engine to every workspace on every
|
|
81
|
+
instance. No instance, product, or personal names — CI scans this tree along
|
|
82
|
+
with the rest of the repo.
|
|
83
|
+
- **The prototype contract holds** (`../agents/prototype-contract.md`):
|
|
84
|
+
self-contained static HTML, no build step, opens straight from disk, and a
|
|
85
|
+
one-line `<meta name="description">` that says what the page shows.
|
|
86
|
+
- **Link the design system, do not copy it.** The canonical relative path
|
|
87
|
+
(`../../../skills/starter-ui/…`) resolves on disk and the build rewrites it for
|
|
88
|
+
the site, which is what keeps six prototypes wearing one system.
|
|
89
|
+
- **A CDN script must be pinned to an exact version.** `specimen-viewer` is the
|
|
90
|
+
worked example: an immutable versioned URL, never a moving tag, and a visible
|
|
91
|
+
fallback when the network is not there.
|
|
92
|
+
- **Copy is instruction, not decoration.** Every step says what you should see
|
|
93
|
+
when it worked, and every step that can fail carries its own way out.
|
|
94
|
+
- **`CANON.md` is written by the tool, not by hand.** It is byte-for-byte the
|
|
95
|
+
`NOTE` string in `scripts/canon.mjs` — the same one `augur init` scaffolds and
|
|
96
|
+
`augur canon save` writes into a workspace that has none, so a hosted workspace
|
|
97
|
+
and a self-hosted one describe their names identically.
|
|
98
|
+
`test/canon-naming.test.mjs` fails when any of the three drift. Edit the string
|
|
99
|
+
and regenerate this copy; the rules it summarises live in `agents/canon.md`.
|
|
100
|
+
- **The four canon tiers are ABSENT here, not empty.** There is no `base/`,
|
|
101
|
+
`components/`, `patterns/` or `pages/` in this tree, and adding one would be
|
|
102
|
+
wrong: a workspace's canon is what its own work promoted into it, so a seeded
|
|
103
|
+
canon would be six screens nobody chose claiming to be the ones worth copying.
|
|
104
|
+
`augur canon save` creates the tier directory on the first promotion. What the
|
|
105
|
+
seed ships instead is the SCHEME — `CANON.md` — so the canon a workspace grows
|
|
106
|
+
is named the same way as every other workspace's.
|
|
107
|
+
|
|
108
|
+
## Working on it locally
|
|
109
|
+
|
|
110
|
+
From this directory, with a raw engine clone around it:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
GV_SPACES_ROOT="$PWD" node ../build.js
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
That composes the workspace into the engine's `dist/`, exactly as an instance
|
|
117
|
+
would serve it — the galleries, the library tier derived from the skill, and the
|
|
118
|
+
six prototypes. Each prototype also opens on its own by double-clicking its
|
|
119
|
+
`index.html`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Dev-status baseline per prototype, keyed <project>/<prototype>. Values: in-progress | dev-ready | ignore. Live chips cycle on click (KV) over this baseline.",
|
|
3
|
+
"start-here/connect-your-terminal": "dev-ready",
|
|
4
|
+
"start-here/set-up-your-design-system": "dev-ready",
|
|
5
|
+
"start-here/sample-with-comments": "in-progress",
|
|
6
|
+
"worked-examples/specimen-viewer": "dev-ready",
|
|
7
|
+
"worked-examples/slide-deck": "dev-ready",
|
|
8
|
+
"worked-examples/field-readings": "in-progress"
|
|
9
|
+
}
|