@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,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);
|