@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,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * no-foreign-vocabulary — the engine may not carry a workspace's private vocabulary.
4
+ *
5
+ * WHY THIS EXISTS. `check.yml` already greps for instance and product words, and it
6
+ * caught NOTHING through the whole de-client extraction: nine customer names, a
7
+ * customer's slug prefixes and a customer's CSS-variable namespace sat in the shared
8
+ * engine through every one of those runs. The reason is structural, not a gap in the
9
+ * list — that grep is a DENYLIST of names, so it can only ever catch a name somebody
10
+ * already thought to write down. The next client is, by definition, not on it.
11
+ *
12
+ * WHY NOT JUST WIDEN THE LIST. A denylist of client names, in a public repo, IS a
13
+ * client list in a public repo — the problem restated, and worse, because it would be
14
+ * the canonical one. The list would also have to name cities, products and codenames
15
+ * that mean nothing out of context, so nobody could maintain it and every miss would
16
+ * look like a decision. Rejected on both counts.
17
+ *
18
+ * WHAT THIS DOES INSTEAD. It names no client and no city. Each rule below states a
19
+ * SHAPE that only foreign vocabulary has, and every allowlist in this file is standard
20
+ * web vocabulary — CSS keywords, HTML meta names — which leaks nothing about who runs
21
+ * an instance. A rule earns its place by two proofs, both recorded in the commit that
22
+ * adds it: it FIRES on the pre-extraction tree, and it is SILENT on HEAD.
23
+ *
24
+ * WHAT THIS DELIBERATELY DOES NOT DO — read this before assuming you are covered:
25
+ * · A workspace's SLUG PREFIXES (`fo-`, `bo-`, a retired page's slug in a Set) are
26
+ * not checkable here. They have exactly the shape of the engine's own hyphenated
27
+ * names — CSS classes, attributes, route segments, MIME types — and no regex over
28
+ * source can tell `"bo-project-phase"` from `"stroke-linejoin"` without knowing
29
+ * which one is compared against a directory entry. That needs dataflow, not grep.
30
+ * · A short word smuggled into a general-vocabulary list (an entry in ACRONYMS that
31
+ * is one team's initials rather than an acronym) is not checkable EITHER, for the
32
+ * same reason inverted: the honest entries and the dishonest one are the same
33
+ * shape. That one is a review question, and the comment on that Set says so.
34
+ * Both are real gaps. A guard that pretends otherwise is worse than one that says
35
+ * where it stops.
36
+ *
37
+ * Run: node scripts/no-foreign-vocabulary.mjs (exit 1 on any finding)
38
+ */
39
+ import fs from "node:fs";
40
+ import path from "node:path";
41
+ import { fileURLToPath } from "node:url";
42
+
43
+ const ROOT = process.argv[2] || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
44
+
45
+ // The SHIPPED engine: what a pin bump pushes into every instance. Vendored trees, build
46
+ // output and the repo's own git data are not ours to lint.
47
+ const SCAN_DIRS = ["src", "scripts", "agents"];
48
+ const SCAN_FILES = ["build.js"];
49
+ const SKIP_DIRS = new Set([".git", "node_modules", "dist", "coverage"]);
50
+ const EXT = new Set([".js", ".mjs", ".cjs"]);
51
+ // This file necessarily writes down the shapes it forbids.
52
+ const SELF = "scripts/no-foreign-vocabulary.mjs";
53
+
54
+ function* walk(dir) {
55
+ let entries;
56
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
57
+ for (const e of entries) {
58
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) yield* walk(path.join(dir, e.name)); }
59
+ else if (EXT.has(path.extname(e.name))) yield path.join(dir, e.name);
60
+ }
61
+ }
62
+
63
+ function sources() {
64
+ const out = [];
65
+ for (const f of SCAN_FILES) { const p = path.join(ROOT, f); if (fs.existsSync(p)) out.push(p); }
66
+ for (const d of SCAN_DIRS) out.push(...walk(path.join(ROOT, d)));
67
+ return out.filter((p) => path.relative(ROOT, p) !== SELF);
68
+ }
69
+
70
+ const findings = [];
71
+ const report = (rule, file, lineNo, line, detail) =>
72
+ findings.push({ rule, file: path.relative(ROOT, file), lineNo, line: line.trim().slice(0, 160), detail });
73
+
74
+ // Every quoted string literal on a line, single/double/backtick, value only.
75
+ const stringsOn = (line) =>
76
+ [...line.matchAll(/"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|`([^`\\$]*)`/g)]
77
+ .map((m) => m[1] ?? m[2] ?? m[3]).filter((s) => s !== undefined);
78
+
79
+ const HEX = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
80
+
81
+ // A proper noun: capitalised words, optionally several, optionally with the marks real
82
+ // place and organisation names carry. NOT an ALL-CAPS constant, not camelCase, not a
83
+ // sentence (a sentence has a lowercase function word in it, which this rejects by
84
+ // requiring every word to be capitalised).
85
+ const PROPER_NOUN = /^[A-ZÀ-Þ][a-zß-ÿ'’]+(?:[ \u00A0''-][A-ZÀ-Þa-zß-ÿ][a-zß-ÿ'’]*)*$/;
86
+ // Capitalised words that are ordinary CSS/web vocabulary, not somebody's name. Standard
87
+ // vocabulary only — nothing here identifies a person, a city or a company.
88
+ const CSS_WORDS = new Set([
89
+ "Inter", "Menlo", "Consolas", "Helvetica", "Arial", "Georgia", "Courier", "Roboto",
90
+ "Segoe", "Cambria", "Verdana", "Tahoma", "Times", "Symbol", "Noto", "Liberation",
91
+ "Auto", "None", "Normal", "Bold", "Italic", "Regular", "Medium", "Light", "Black",
92
+ "White", "Center", "Left", "Right", "Top", "Bottom", "Solid", "Dashed", "Dotted",
93
+ "Small", "Large", "Default", "Custom", "Other", "Unknown", "Untitled", "New",
94
+ ]);
95
+
96
+ /* ── Rule 1 — a proper noun standing next to a colour is somebody's brand ──────
97
+ *
98
+ * The engine's own colours are ANONYMOUS: a hex in engine source is a swatch in a
99
+ * palette array, a fill in an inline SVG, or a value in a CSS declaration, and none of
100
+ * those has a name beside it. The moment a hex is quoted next to a capitalised name,
101
+ * the engine has stopped styling itself and started holding a TABLE OF SOMEBODY'S
102
+ * BRANDS — which is exactly the shape the nine-customer theme list had, and exactly
103
+ * the shape the tenth would have.
104
+ *
105
+ * This is why the rule pairs the two rather than banning either: hex literals are
106
+ * ordinary and proper nouns are ordinary; a hex literal AS DATA, labelled with a name,
107
+ * is not.
108
+ *
109
+ * QUOTED is load-bearing, and it is the second half of why this is precise. CSS writes
110
+ * a colour BARE — `color: #16171a;` — and the engine's chrome CSS does so on hundreds
111
+ * of lines, thirteen of which also carry a quoted font stack. A colour that has been
112
+ * put in QUOTES has stopped being a declaration and become a VALUE: something the code
113
+ * carries around, which is the only way a customer's brand colour can be stored. So a
114
+ * bare hex is invisible to this rule by construction, not by exception, and no
115
+ * allowlist of the engine's own stylesheets is needed or wanted. */
116
+ function ruleBrandTable(file, lines) {
117
+ lines.forEach((line, i) => {
118
+ const strs = stringsOn(line);
119
+ if (!strs.some((s) => HEX.test(s.trim()))) return;
120
+ for (const s of strs) {
121
+ const v = s.trim();
122
+ if (HEX.test(v)) continue;
123
+ if (!PROPER_NOUN.test(v)) continue;
124
+ if (v.split(/[\s-]+/).every((w) => CSS_WORDS.has(w))) continue;
125
+ report("brand-table", file, i + 1, line,
126
+ `the quoted name ${JSON.stringify(v)} sits beside a colour literal — the engine holds no organisation's brand colour`);
127
+ return;
128
+ }
129
+ });
130
+ }
131
+
132
+ /* ── Rule 2 — the token namespace comes from the workspace, never from here ────
133
+ *
134
+ * A design system's CSS custom properties are named in ITS vocabulary, and the engine
135
+ * is told what that vocabulary is: the UI skill declares `cssPrefixes` in skill.json,
136
+ * buildGraph() resolves it, and it rides out on the composition graph precisely so
137
+ * that anything downstream reads it instead of re-deriving one. So a literal
138
+ * `--<something>-` anchored in engine source cannot be anything but one workspace's
139
+ * private prefix, hardcoded — there is no other way it could have got there.
140
+ *
141
+ * An INTERPOLATED prefix (`^--(?:${PFX})-…`) is the correct construction and passes:
142
+ * the shape being caught is the literal, not the anchor. */
143
+ function ruleTokenNamespace(file, lines) {
144
+ lines.forEach((line, i) => {
145
+ // Skip comments: the docs have to be able to SAY "--acme-*" to explain the feature.
146
+ const code = line.replace(/^\s*(?:\/\/|\*|\/\*).*$/, "");
147
+ if (!code) return;
148
+ for (const m of code.matchAll(/\^--([A-Za-z][\w-]*)-/g)) {
149
+ report("token-namespace", file, i + 1, line,
150
+ `anchors on the literal custom-property prefix "--${m[1]}-" — the token vocabulary is the skill's ` +
151
+ `cssPrefixes, carried on the graph; interpolate it instead of naming one workspace's`);
152
+ return;
153
+ }
154
+ });
155
+ }
156
+
157
+ /* ── Rule 3 — a meta tag the engine reads must be in a namespace the engine owns ──
158
+ *
159
+ * A prototype is a workspace's own HTML, so a `<meta name="…">` the ENGINE goes
160
+ * looking for is a contract between them, and the engine gets to name its half of it.
161
+ * A meta name in someone else's namespace means the contract was written in their
162
+ * words and every other workspace has to learn them — which is how a page taxonomy
163
+ * belonging to one product ended up classifying every page of every workspace.
164
+ *
165
+ * The allowlist is standard web vocabulary plus the engine's own `augur-` namespace.
166
+ * It names no product: if a name is neither a W3C/OGP one nor the engine's, it is
167
+ * somebody's, and that is the finding. */
168
+ const STD_META = new Set([
169
+ "description", "viewport", "robots", "theme-color", "color-scheme", "author",
170
+ "generator", "keywords", "referrer", "application-name", "format-detection",
171
+ "mobile-web-app-capable", "apple-mobile-web-app-capable",
172
+ "apple-mobile-web-app-status-bar-style", "apple-mobile-web-app-title", "msapplication-TileColor",
173
+ ]);
174
+ const stdMetaOk = (n) =>
175
+ STD_META.has(n) || /^(?:og|twitter|fb|al|article|product|profile|music|video|book):/i.test(n)
176
+ || n.startsWith("augur-");
177
+
178
+ function ruleMetaNamespace(file, lines) {
179
+ lines.forEach((line, i) => {
180
+ // Only where a <meta …> tag is actually in play, so ordinary `name=` form fields
181
+ // and function parameters are out of scope.
182
+ if (!/<meta\b/i.test(line)) return;
183
+ // Between `name=` and the name itself, skip whatever quoting the site uses: a plain
184
+ // quote in emitted HTML, an escaped one in a JS string, or a character class in the
185
+ // REGEX that reads the tag — `name=["']gv-surface["']`. Missing the character-class
186
+ // form would mean catching only the comment that documents the read and never the
187
+ // read, which is the wrong way round.
188
+ for (const m of line.matchAll(/name=\s*\\?[\["'\]]*\s*([A-Za-z][\w:.-]*)/g)) {
189
+ const n = m[1];
190
+ if (stdMetaOk(n)) continue;
191
+ report("meta-namespace", file, i + 1, line,
192
+ `reads or writes <meta name="${n}"> — not a standard web meta name and not in the engine's ` +
193
+ `own "augur-" namespace, so it is one workspace's vocabulary`);
194
+ return;
195
+ }
196
+ });
197
+ }
198
+
199
+ for (const file of sources()) {
200
+ const lines = fs.readFileSync(file, "utf8").split("\n");
201
+ ruleBrandTable(file, lines);
202
+ ruleTokenNamespace(file, lines);
203
+ ruleMetaNamespace(file, lines);
204
+ }
205
+
206
+ if (!findings.length) {
207
+ console.log("no-foreign-vocabulary: OK — no foreign vocabulary in the engine");
208
+ process.exit(0);
209
+ }
210
+ for (const f of findings) {
211
+ console.error(`\n${f.file}:${f.lineNo} [${f.rule}]\n ${f.detail}\n ${f.line}`);
212
+ }
213
+ console.error(`\nno-foreign-vocabulary: ${findings.length} finding(s).`);
214
+ console.error("The engine is shared by every instance. A workspace's own names belong in its");
215
+ console.error("space.json / skill.json, and reach the engine through the manifest, never as a literal.");
216
+ process.exit(1);
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * no-product-names — the engine may not name another company's product.
4
+ *
5
+ * WHY THIS EXISTS. Design-tool comparisons had colonised the engine: 50 mentions across
6
+ * 19 files, and 32 of them were fetchable by any visitor. That last number is the point
7
+ * and it is not obvious — build.js runs no minifier, so every comment in
8
+ * src/canvas/canvas.js, src/canvas/canvas.css, src/review/comments.js and in build.js's
9
+ * emitted CSS ships byte-for-byte to the browser at /__canvas/canvas.js, /__review/,
10
+ * /_chrome.*.css and /admin/. One mention was live PROSE on /changelog/. So "it is only a
11
+ * comment" was never true here, and the rule reaches the whole repo rather than the
12
+ * strings a reader was expected to notice.
13
+ *
14
+ * WHY A DENYLIST IS FINE HERE AND NOT NEXT DOOR. scripts/no-foreign-vocabulary.mjs
15
+ * argues at length that a denylist cannot work for a workspace's private vocabulary, and
16
+ * it is right: that set is OPEN (one new name per customer, so the next one is by
17
+ * definition not on the list) and writing it down would put a client list in a public
18
+ * repo. Neither leg transfers. The design-tool set is CLOSED and grows with the industry
19
+ * rather than with the business, and writing `figjam` into a public file discloses
20
+ * nothing about anybody. It is the same category as the CSS_WORDS allowlist that file
21
+ * already ships and defends on exactly that ground.
22
+ *
23
+ * But the rule is "another company's product", not "the twenty I listed", so RULE 2 is a
24
+ * shape rule that stands on its own. Measured against the pre-purge tree, rule 2 alone
25
+ * caught 31 of the 50, including every one that rule 1 had no name for.
26
+ *
27
+ * WHAT THIS DELIBERATELY DOES NOT DO — read before assuming you are covered:
28
+ * · A LOWERCASE mention of an ambiguous name in running prose ("paste it into notion")
29
+ * is caught by neither rule. Rule 1 excludes ambiguous names because `canvas`,
30
+ * `linear-gradient`, `no notion of`, `abstract motifs` and `keep sketching` are all
31
+ * legitimate here and a guard on `check` gates the deploy, so a false positive is an
32
+ * outage of the ability to ship. Rule 2 requires title case. That gap is a review
33
+ * question, and this comment is where it is written down instead of assumed away.
34
+ *
35
+ * Run: node scripts/no-product-names.mjs [ROOT] (exit 1 on any finding)
36
+ */
37
+ import fs from "node:fs";
38
+ import path from "node:path";
39
+ import { fileURLToPath } from "node:url";
40
+ import { execFileSync } from "node:child_process";
41
+
42
+ const ROOT = process.argv[2] || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
43
+
44
+ const SKIP_DIRS = new Set([".git", "node_modules", "dist", "coverage"]);
45
+ const EXT = new Set([".js", ".mjs", ".cjs", ".css", ".html", ".md", ".json", ".yml", ".yaml", ".toml", ".txt", ".sh"]);
46
+ // This file necessarily writes down the names it forbids, and its test necessarily uses
47
+ // them as fixtures.
48
+ const SELF = new Set(["scripts/no-product-names.mjs", "test/no-product-names.test.mjs"]);
49
+
50
+ // RULE 1 — names with no common-English sense, so a bare match is never a false positive.
51
+ // Ambiguous names (notion, sketch, abstract, framer, linear, canva, principle, craft,
52
+ // marvel, mural) are NOT here on purpose; see the header.
53
+ const PRODUCTS = [
54
+ "figma", "figjam", "miro", "excalidraw", "invision", "zeplin", "penpot",
55
+ "balsamiq", "axure", "protopie", "lucidchart", "whimsical", "shadcn",
56
+ "storybook", "photoshop", "illustrator", "webflow", "imessage",
57
+ ];
58
+ // `Linear` and `Notion` and `Slack` are ambiguous as words but unambiguous in the two
59
+ // frames the engine actually used them in, so they are caught by rule 2 instead.
60
+ const PRODUCT_RE = new RegExp(String.raw`\b(${PRODUCTS.join("|")})(?:['’]s|-[a-z]+)?\b`, "gi");
61
+
62
+ // RULE 2 — the comparison SHAPES. Every one of the 50 original hits sat in one of these,
63
+ // and they are what catches the tool nobody has written down.
64
+ const SHAPES = [
65
+ [/\b([A-Z][a-z]{2,}[A-Za-z]*)['’]s\s+(?:idiom|model|move|shape|order|pair|set|affordance|convention|way|parity|answer|version)\b/g, "possessive product comparison"],
66
+ [/\b([A-Z][a-z]{2,}[A-Za-z]*)-(?:parity|style|styled|class|quiet|like|ish|grade|native)\b/g, "product-as-adjective"],
67
+ [/\b(?:like|unlike|the way|the same as)\s+([A-Z][a-z]{2,}[A-Za-z]*)\b/g, "explicit comparison to a named product"],
68
+ [/\bthe\s+([A-Z][a-z]{2,}[A-Za-z]*)(?:\/[A-Z][a-z]{2,}[A-Za-z]*)*\s+(?:idiom|model|move|shape|order|pair|set|affordance|convention|way)\b/g, "attributive product comparison"],
69
+ ];
70
+
71
+ // The proper nouns the engine legitimately writes: its own vocabulary, the platforms it
72
+ // RUNS ON, and the dependencies it vendors and must attribute. Every entry here is public
73
+ // standard vocabulary that identifies nobody — the same test the sibling guard's
74
+ // allowlists have to pass. Adding a name is a one-line diff in a commit whose subject is
75
+ // "we now depend on X", which makes it a review event by construction. There is
76
+ // deliberately no per-line pragma: a written reason is what every closed leak shipped
77
+ // under, and if a mention is legitimate it is a dependency and belongs here.
78
+ const ALLOWED = new Set([
79
+ // the engine's own
80
+ "Augur", "Clawd", "Piti", "Pitis", "Base", "Tokens", "Components", "Patterns", "Pages",
81
+ "Playground", "Changelog", "Canvas", "Workspace", "Prototype", "Design", "Admin", "Help",
82
+ // platforms it runs on
83
+ "Cloudflare", "Workers", "Pages", "GitHub", "Actions", "Scaleway", "Stripe", "Migadu",
84
+ "Wrangler", "Node", "Durable", "Object", "Objects",
85
+ // browsers and rendering engines the code genuinely branches on
86
+ "Chrome", "Safari", "Firefox", "WebKit", "Blink", "Gecko",
87
+ // vendored dependencies and licences
88
+ "Lucide", "Inter", "Menlo", "Consolas", "Helvetica", "Arial", "Roboto", "Segoe",
89
+ "LentiaNova", "DSEG", "Playwright", "SIL", "ISC", "MIT", "OFL",
90
+ // ordinary capitalised English that lands in these frames
91
+ "The", "This", "That", "These", "Those", "Every", "Each", "One", "Two", "Both", "Same",
92
+ "Option", "Shift", "Control", "Command", "Escape", "Enter", "Guest", "Admin", "Viewer",
93
+ "Editor", "Owner", "Anyone", "Nobody", "Somebody", "Read", "Write", "Note", "Sticky",
94
+ ]);
95
+
96
+ function* walk(dir) {
97
+ let entries;
98
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
99
+ for (const e of entries) {
100
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) yield* walk(path.join(dir, e.name)); }
101
+ else if (EXT.has(path.extname(e.name))) yield path.join(dir, e.name);
102
+ }
103
+ }
104
+
105
+ // Prefer the tracked set — an untracked scratch file is not what a deploy ships — but a
106
+ // tarball checkout has no .git, so fall back to walking rather than passing vacuously.
107
+ function files() {
108
+ try {
109
+ const out = execFileSync("git", ["-C", ROOT, "ls-files", "-z"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
110
+ const rels = out.split("\0").filter(Boolean).filter((r) => EXT.has(path.extname(r)));
111
+ if (rels.length) return rels.map((r) => path.join(ROOT, r));
112
+ } catch { /* no git — fall through */ }
113
+ return [...walk(ROOT)];
114
+ }
115
+
116
+ const findings = [];
117
+ for (const abs of files()) {
118
+ const rel = path.relative(ROOT, abs).split(path.sep).join("/");
119
+ if (SELF.has(rel)) continue;
120
+ let src;
121
+ try { src = fs.readFileSync(abs, "utf8"); } catch { continue; }
122
+ // A binary that slipped past the extension filter. The NUL is written as an escape
123
+ // rather than embedded as a raw byte, so this file is not itself a file with a NUL
124
+ // in it — which would make git treat it as binary and every diff of it unreadable.
125
+ if (src.includes("\0")) continue;
126
+ const lines = src.split(/\r?\n/);
127
+ lines.forEach((line, i) => {
128
+ for (const m of line.matchAll(PRODUCT_RE)) {
129
+ findings.push({ rel, n: i + 1, rule: "product-denylist", detail: `"${m[0]}" is another company's product. Describe the mechanism instead.`, line });
130
+ }
131
+ for (const [re, why] of SHAPES) {
132
+ for (const m of line.matchAll(re)) {
133
+ const noun = m[1];
134
+ if (ALLOWED.has(noun)) continue;
135
+ findings.push({ rel, n: i + 1, rule: "product-comparison", detail: `${why}: "${noun}". Say what the behaviour IS, not what it is like.`, line });
136
+ }
137
+ }
138
+ });
139
+ }
140
+
141
+ if (!findings.length) {
142
+ console.log("no-product-names: OK — the engine names no other company's product");
143
+ process.exit(0);
144
+ }
145
+ // One line per finding, deduped by file+line+rule so a line with two names reads once
146
+ // per rule rather than twice per name.
147
+ const seen = new Set();
148
+ for (const f of findings) {
149
+ const key = `${f.rel}:${f.n}:${f.rule}`;
150
+ if (seen.has(key)) continue;
151
+ seen.add(key);
152
+ console.log(`${f.rel}:${f.n} [${f.rule}]`);
153
+ console.log(` ${f.detail}`);
154
+ console.log(` ${f.line.trim().slice(0, 160)}`);
155
+ }
156
+ console.log(`\n${seen.size} finding(s). The engine is public and unminified — a comparison in a comment ships to the browser.`);
157
+ process.exit(1);