@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,138 @@
|
|
|
1
|
+
// The bundle store's KEY SHAPE — the one place a logical store key becomes a physical one.
|
|
2
|
+
//
|
|
3
|
+
// Lifted out of src/_worker.js so the workspace object (src/tenant-do.js) can address the
|
|
4
|
+
// store for the workspace it IS, without a second key computation living beside this one:
|
|
5
|
+
// the seed pack a fresh workspace is furnished with at provisioning is written by the
|
|
6
|
+
// object, and the object has to land it under exactly the segment the front door reads
|
|
7
|
+
// (`F-seed-pack-at-provision`). The worker imports everything here and re-exports it under
|
|
8
|
+
// `__testables`, so every test and rehearsal that reads these names off the worker keeps
|
|
9
|
+
// reading them there. Nothing else changed in the move: the text below is the worker's,
|
|
10
|
+
// verbatim, including the two families that stay shared and why.
|
|
11
|
+
//
|
|
12
|
+
// ⚠️ `scripts/bundle-tenancy-rehearsal.mjs` edits a COPY of this file to run the per-family
|
|
13
|
+
// revert (`BUNDLE_TENANCY.spaces = false`); rename the constant and it says so.
|
|
14
|
+
|
|
15
|
+
// ---- The workspace segment on a bundle-store key ----------------------------
|
|
16
|
+
//
|
|
17
|
+
// `B-bundle-store-tenancy`. Not one key above carries a workspace: `config/instance.json`
|
|
18
|
+
// is one document for the whole bucket, `spaces/<id>/…` names a SPACE, and one deployment
|
|
19
|
+
// serving several workspaces therefore has them all writing the same keys. Two workspaces
|
|
20
|
+
// publishing a space under the same id write the same object, so the commit CAS, the
|
|
21
|
+
// unpublish guard and the stale-base check all evaluate against a stranger's document —
|
|
22
|
+
// and a route-level gate cannot un-collide a key. So the key gains the segment.
|
|
23
|
+
//
|
|
24
|
+
// THE SHAPE, decided rather than discovered (`DECISION-bundle-store-tenancy.md`, option 1):
|
|
25
|
+
// a tenant PREFIX in the one bucket. `t/<workspace>/spaces/…`,
|
|
26
|
+
// `t/<workspace>/config/instance.json`, `t/<workspace>/assets/…`.
|
|
27
|
+
//
|
|
28
|
+
// ⚠️ TWO FAMILIES STAY GLOBAL AND SHARED, DELIBERATELY. Both exceptions are written out
|
|
29
|
+
// below rather than left to fall out of the change, because falling out of a change is
|
|
30
|
+
// exactly how they would be got wrong.
|
|
31
|
+
//
|
|
32
|
+
// `blobs/<sha256>` — published bytes. Every write verifies the digest against the key
|
|
33
|
+
// before storing, so a workspace can only ever write bytes that hash to the name it
|
|
34
|
+
// used: an overwrite is a no-op by construction and there is nothing to poison. Dedup
|
|
35
|
+
// across workspaces is load-bearing (a migration's frozen pass uploaded 0 blobs of 854
|
|
36
|
+
// already present), and `blobGc` is written FOR a shared namespace — it reads every
|
|
37
|
+
// remaining manifest before deleting anything, because only the sweep can tell an
|
|
38
|
+
// orphan from a blob another workspace is serving. Prefixing them would break that
|
|
39
|
+
// design and buy nothing: a SHA-256 is not enumerable, so the disclosure door is the
|
|
40
|
+
// INDEX, not the bytes — and the index is `spaces/`, which is prefixed.
|
|
41
|
+
//
|
|
42
|
+
// `spaces/_engine/` — the engine chrome. ONE worker build serves every workspace on a
|
|
43
|
+
// deployment, so one chrome bundle is correct rather than a leak. Prefix it by accident
|
|
44
|
+
// and every workspace loses its chrome on the deploy that does it.
|
|
45
|
+
export const BUNDLE_TENANT_PREFIX = "t/";
|
|
46
|
+
export const ENGINE_SPACE_ID = "_engine";
|
|
47
|
+
// Which families take the segment. One word each, and flipping one back is the revert for
|
|
48
|
+
// that family alone — the shape `KV_CUTOVER` uses, for the same reason: a change that has
|
|
49
|
+
// to be reverted as a unit is a change nobody wants to make on a live instance.
|
|
50
|
+
export const BUNDLE_TENANCY = Object.freeze({
|
|
51
|
+
spaces: true, // spaces/<id>/manifest.json + spaces/<id>/versions/<n>.json
|
|
52
|
+
config: true, // config/instance.json
|
|
53
|
+
assets: true, // assets/<sha256[0:40]> — canvas image bytes
|
|
54
|
+
// blobs: NOT HERE, AND NOT AN OMISSION. See the header above.
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Which family a bundle-store key belongs to, or "" for one this scheme does not name. */
|
|
58
|
+
export function bundleFamily(key) {
|
|
59
|
+
const k = String(key || "");
|
|
60
|
+
if (k.startsWith("blobs/")) return "blobs";
|
|
61
|
+
if (k.startsWith("assets/")) return "assets";
|
|
62
|
+
if (k.startsWith("config/")) return "config";
|
|
63
|
+
if (k.startsWith("spaces/")) return "spaces";
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The physical store key for a logical one.
|
|
69
|
+
*
|
|
70
|
+
* `workspace` is the second argument and it DEFAULTS TO NONE, which is the whole of the
|
|
71
|
+
* straddle: a deployment that serves one workspace passes nothing and gets back the string
|
|
72
|
+
* it has always got back, byte for byte. Only a prefixing deployment passes a segment —
|
|
73
|
+
* see `bundleWorkspaceSegment`.
|
|
74
|
+
*/
|
|
75
|
+
export function bundleKey(key, workspace = "") {
|
|
76
|
+
if (!workspace) return key;
|
|
77
|
+
const family = bundleFamily(key);
|
|
78
|
+
if (!family || !BUNDLE_TENANCY[family]) return key;
|
|
79
|
+
// ⚠️ THE ENGINE EXCEPTION, WRITTEN OUT. `spaces/_engine/…` is the chrome one worker
|
|
80
|
+
// build serves to every workspace on this deployment. It is not this workspace's to
|
|
81
|
+
// hold and it is not another's to be kept from.
|
|
82
|
+
if (family === "spaces" && key.startsWith(`spaces/${ENGINE_SPACE_ID}/`)) return key;
|
|
83
|
+
return BUNDLE_TENANT_PREFIX + workspace + "/" + key;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The bundle store as ONE workspace sees it: the same five verbs over LOGICAL keys, with
|
|
88
|
+
* the segment applied on the way in and stripped on the way out.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ WITH NO SEGMENT THIS IS THE BINDING ITSELF — not a wrapper around it, the object.
|
|
91
|
+
* That is deliberate, and it is what makes the change additive for every instance running
|
|
92
|
+
* today: with no segment this function is the identity, so there is no new code at all
|
|
93
|
+
* between the worker and R2 and nothing to get subtly wrong on a deployment that never
|
|
94
|
+
* asked for a segment.
|
|
95
|
+
*
|
|
96
|
+
* Stripping on the way out is what lets every caller keep the key it already had: a
|
|
97
|
+
* listing hands back `spaces/x/versions/3.json`, and handing that straight back to `get`
|
|
98
|
+
* or `delete` re-applies the segment rather than double-prefixing it.
|
|
99
|
+
*/
|
|
100
|
+
export function bundleStore(env, workspace = "") {
|
|
101
|
+
const r2 = env && env.BUNDLES;
|
|
102
|
+
if (!r2 || !workspace) return r2 || null;
|
|
103
|
+
const seg = BUNDLE_TENANT_PREFIX + workspace + "/";
|
|
104
|
+
const K = (k) => bundleKey(k, workspace);
|
|
105
|
+
const un = (k) => (String(k).startsWith(seg) ? String(k).slice(seg.length) : String(k));
|
|
106
|
+
// ⚠️ A WRITE GOES TO THE SEGMENTED KEY AND NOWHERE ELSE. This view used to write the
|
|
107
|
+
// unprefixed key too, as a straddle meant to keep the per-family flag a revert rather
|
|
108
|
+
// than a rollback — and on the one kind of deployment that has a segment at all, it was
|
|
109
|
+
// never that. Where the bucket is shared an unprefixed key is unattributable: the
|
|
110
|
+
// deployment's own rule (`legacyIsOurs: false`) already refuses to READ one, and flipping
|
|
111
|
+
// a family's flag back there reads whatever was last written under the bare key by
|
|
112
|
+
// whichever workspace wrote it last — the collision this scheme exists to close, not
|
|
113
|
+
// yesterday. So the second write bought no revert, and it cost a real thing: every
|
|
114
|
+
// workspace's `config/instance.json` — its roster — and every manifest — its blob index,
|
|
115
|
+
// the disclosure door the header above names — copied to where every workspace shares.
|
|
116
|
+
// Found on a live shared deployment, attributed by content, the same second as the
|
|
117
|
+
// segmented write.
|
|
118
|
+
//
|
|
119
|
+
// Deletes never touched the unprefixed key either, for the same reason in the other
|
|
120
|
+
// direction: removing one is removing an object that may be a neighbour's. That still
|
|
121
|
+
// holds, so what predates the segment is left exactly where and as it was.
|
|
122
|
+
const store = {
|
|
123
|
+
get: (k, opts) => (opts === undefined ? r2.get(K(k)) : r2.get(K(k), opts)),
|
|
124
|
+
put: (k, v, opts) => (opts === undefined ? r2.put(K(k), v) : r2.put(K(k), v, opts)),
|
|
125
|
+
list: async (opts = {}) => {
|
|
126
|
+
const page = await r2.list({ ...opts, prefix: K(opts.prefix || "") });
|
|
127
|
+
return {
|
|
128
|
+
...page,
|
|
129
|
+
objects: (page.objects || []).map((o) => ({ ...o, key: un(o.key) })),
|
|
130
|
+
delimitedPrefixes: (page.delimitedPrefixes || []).map(un),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
if (typeof r2.head === "function") store.head = (k) => r2.head(K(k));
|
|
135
|
+
if (typeof r2.delete === "function") store.delete = (k) => r2.delete(K(k));
|
|
136
|
+
return store;
|
|
137
|
+
}
|
|
138
|
+
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canon — collect evidence from a product you are ALREADY SIGNED IN TO.
|
|
3
|
+
*
|
|
4
|
+
* Paste this whole file into your browser's developer console, on a page of your own
|
|
5
|
+
* product, while you are logged in. It reads what the page actually renders and prints
|
|
6
|
+
* an `observation` — the same object `augur canon collect <url>` produces for a public
|
|
7
|
+
* page, so everything downstream is identical whichever way you got here.
|
|
8
|
+
*
|
|
9
|
+
* WHY IT RUNS IN YOUR BROWSER. The people who need a design system extracted hold a
|
|
10
|
+
* login, not repo access, and their product is behind that login. Nothing here leaves
|
|
11
|
+
* the page: no request is made, no credential is read, no cookie is touched. It walks
|
|
12
|
+
* the DOM you are looking at, asks the browser what colour and size each element ended
|
|
13
|
+
* up, and counts. You can read every line of it before you paste it.
|
|
14
|
+
*
|
|
15
|
+
* WHAT COMES OUT is evidence, not a design system: ranked colours (weighted by how much
|
|
16
|
+
* of the screen they actually cover, which is the signal a stylesheet cannot give you),
|
|
17
|
+
* font stacks, the sizes and gaps in use, the radii, the shadows, whatever custom
|
|
18
|
+
* properties the product already declares, and the class families that are candidate
|
|
19
|
+
* components. Deciding which grey is the hairline and which colour is the one hot ink is
|
|
20
|
+
* the next step, and it is not this file's job.
|
|
21
|
+
*
|
|
22
|
+
* Run it on three or four DIFFERENT screens and keep all of them — a login page is not
|
|
23
|
+
* a design system. `augur canon collect --merge` folds them together.
|
|
24
|
+
*/
|
|
25
|
+
(function () {
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
var MAX_ELEMENTS = 12000;
|
|
29
|
+
|
|
30
|
+
var COLOR_PROPS = ["color", "backgroundColor", "borderTopColor", "borderRightColor",
|
|
31
|
+
"borderBottomColor", "borderLeftColor", "outlineColor", "fill", "stroke"];
|
|
32
|
+
/* A border colour on an edge that draws no border is the browser's default, not the
|
|
33
|
+
product's decision. Counting it puts the user-agent grey of every <table> near the
|
|
34
|
+
top of the evidence, wearing exactly the profile a hairline colour has — seen on
|
|
35
|
+
border properties and nothing else — which is the one tell the mapping relies on. */
|
|
36
|
+
var BORDER_SIDE = {
|
|
37
|
+
borderTopColor: "borderTop", borderRightColor: "borderRight",
|
|
38
|
+
borderBottomColor: "borderBottom", borderLeftColor: "borderLeft",
|
|
39
|
+
outlineColor: "outline",
|
|
40
|
+
};
|
|
41
|
+
var SPACE_PROPS = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
42
|
+
"marginTop", "marginRight", "marginBottom", "marginLeft", "rowGap", "columnGap"];
|
|
43
|
+
|
|
44
|
+
function Tally() { this.m = {}; }
|
|
45
|
+
Tally.prototype.add = function (value, where, prop, area) {
|
|
46
|
+
if (!value || value === "none" || value === "normal" || value === "auto" || value === "0px") return;
|
|
47
|
+
if (value === "rgba(0, 0, 0, 0)" || value === "transparent") return;
|
|
48
|
+
var e = this.m[value];
|
|
49
|
+
if (!e) e = this.m[value] = { value: value, count: 0, area: 0, props: {}, where: [] };
|
|
50
|
+
e.count++;
|
|
51
|
+
e.area += area || 0;
|
|
52
|
+
e.props[prop] = 1;
|
|
53
|
+
if (e.where.length < 4 && where && e.where.indexOf(where) < 0) e.where.push(where);
|
|
54
|
+
};
|
|
55
|
+
Tally.prototype.ranked = function (byArea) {
|
|
56
|
+
var out = [];
|
|
57
|
+
for (var k in this.m) if (Object.prototype.hasOwnProperty.call(this.m, k)) out.push(this.m[k]);
|
|
58
|
+
out.sort(function (a, b) {
|
|
59
|
+
if (byArea && b.area !== a.area) return b.area - a.area;
|
|
60
|
+
return b.count - a.count || (a.value < b.value ? -1 : 1);
|
|
61
|
+
});
|
|
62
|
+
return out.slice(0, 60).map(function (e) {
|
|
63
|
+
return { value: e.value, count: e.count, area: Math.round(e.area), props: Object.keys(e.props).slice(0, 6), where: e.where };
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/* A short, readable description of where a value was seen: tag plus its own classes. */
|
|
68
|
+
function whereOf(el) {
|
|
69
|
+
var cls = (el.getAttribute && el.getAttribute("class")) || "";
|
|
70
|
+
return (el.tagName || "?").toLowerCase() + (cls ? "." + cls.trim().split(/\s+/).slice(0, 2).join(".") : "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var colors = new Tally(), fontStacks = new Tally(), fontSizes = new Tally();
|
|
74
|
+
var lineHeights = new Tally(), fontWeights = new Tally(), spacings = new Tally();
|
|
75
|
+
var radii = new Tally(), shadows = new Tally(), motions = new Tally();
|
|
76
|
+
var fams = {};
|
|
77
|
+
var elements = 0;
|
|
78
|
+
|
|
79
|
+
var all = document.querySelectorAll("body *");
|
|
80
|
+
for (var i = 0; i < all.length && i < MAX_ELEMENTS; i++) {
|
|
81
|
+
var el = all[i];
|
|
82
|
+
var cs;
|
|
83
|
+
try { cs = getComputedStyle(el); } catch (e) { continue; }
|
|
84
|
+
if (!cs || cs.display === "none" || cs.visibility === "hidden") continue;
|
|
85
|
+
var rect = el.getBoundingClientRect();
|
|
86
|
+
if (!rect.width && !rect.height) continue;
|
|
87
|
+
elements++;
|
|
88
|
+
var area = Math.min(rect.width * rect.height, 4000000);
|
|
89
|
+
var where = whereOf(el);
|
|
90
|
+
var hasText = false;
|
|
91
|
+
for (var n = 0; n < el.childNodes.length; n++) {
|
|
92
|
+
if (el.childNodes[n].nodeType === 3 && el.childNodes[n].nodeValue.trim()) { hasText = true; break; }
|
|
93
|
+
}
|
|
94
|
+
/* `fill` and `stroke` are inherited SVG properties, so getComputedStyle answers for a
|
|
95
|
+
<div> too — and its answer is the initial `rgb(0, 0, 0)`. Counting those puts a
|
|
96
|
+
black nothing at the top of the evidence on any page with a few large containers. */
|
|
97
|
+
var isSvg = el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
98
|
+
for (var c = 0; c < COLOR_PROPS.length; c++) {
|
|
99
|
+
var prop = COLOR_PROPS[c];
|
|
100
|
+
if ((prop === "fill" || prop === "stroke") && !isSvg) continue;
|
|
101
|
+
var side = BORDER_SIDE[prop];
|
|
102
|
+
if (side && (cs[side + "Style"] === "none" || parseFloat(cs[side + "Width"]) === 0)) continue;
|
|
103
|
+
/* Text colour is weighted by the ink it puts on the page, not by the box: a huge
|
|
104
|
+
container inheriting a colour it never paints would otherwise outrank the body. */
|
|
105
|
+
var weight = prop === "color" ? (hasText ? Math.min(area, 40000) : 0) : area;
|
|
106
|
+
if (weight) colors.add(cs[prop], where, prop, weight);
|
|
107
|
+
}
|
|
108
|
+
fontStacks.add(cs.fontFamily, where, "font-family", hasText ? area : 0);
|
|
109
|
+
if (hasText) {
|
|
110
|
+
fontSizes.add(cs.fontSize, where, "font-size", area);
|
|
111
|
+
lineHeights.add(cs.lineHeight, where, "line-height", area);
|
|
112
|
+
fontWeights.add(cs.fontWeight, where, "font-weight", area);
|
|
113
|
+
}
|
|
114
|
+
for (var s = 0; s < SPACE_PROPS.length; s++) spacings.add(cs[SPACE_PROPS[s]], where, "spacing", 1);
|
|
115
|
+
radii.add(cs.borderTopLeftRadius, where, "border-radius", area);
|
|
116
|
+
shadows.add(cs.boxShadow, where, "box-shadow", area);
|
|
117
|
+
if (cs.transitionDuration && cs.transitionDuration !== "0s") {
|
|
118
|
+
motions.add(cs.transitionDuration + " " + cs.transitionTimingFunction, where, "transition", 1);
|
|
119
|
+
}
|
|
120
|
+
var list = el.classList;
|
|
121
|
+
for (var q = 0; list && q < list.length; q++) {
|
|
122
|
+
var name = list[q];
|
|
123
|
+
if (!/^[a-zA-Z][\w-]{2,}$/.test(name)) continue;
|
|
124
|
+
var parts = name.split(/[-_]/);
|
|
125
|
+
if (parts.length < 2) continue;
|
|
126
|
+
var root = parts.slice(0, 2).join("-");
|
|
127
|
+
var f = fams[root] || (fams[root] = { root: root, uses: 0, members: {} });
|
|
128
|
+
f.uses++;
|
|
129
|
+
f.members[name] = 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Custom properties the product already declares. A cross-origin stylesheet throws on
|
|
134
|
+
.cssRules and there is nothing to be done about it — skip it and say so, rather than
|
|
135
|
+
failing the whole collection over a stylesheet on a CDN. */
|
|
136
|
+
var props = {}, blockedSheets = 0, readSheets = 0;
|
|
137
|
+
function eatRules(rules) {
|
|
138
|
+
for (var r = 0; r < rules.length; r++) {
|
|
139
|
+
var rule = rules[r];
|
|
140
|
+
if (rule.style) {
|
|
141
|
+
for (var k = 0; k < rule.style.length; k++) {
|
|
142
|
+
var nm = rule.style[k];
|
|
143
|
+
if (nm.slice(0, 2) === "--" && props[nm] === undefined) props[nm] = rule.style.getPropertyValue(nm).trim();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (rule.cssRules) eatRules(rule.cssRules);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (var t = 0; t < document.styleSheets.length; t++) {
|
|
150
|
+
try { eatRules(document.styleSheets[t].cssRules); readSheets++; }
|
|
151
|
+
catch (e) { blockedSheets++; }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var famList = [];
|
|
155
|
+
for (var key in fams) if (Object.prototype.hasOwnProperty.call(fams, key)) {
|
|
156
|
+
var mem = Object.keys(fams[key].members);
|
|
157
|
+
if (fams[key].uses > 1) famList.push({ root: key, uses: fams[key].uses, members: mem.slice(0, 10) });
|
|
158
|
+
}
|
|
159
|
+
famList.sort(function (a, b) { return b.members.length - a.members.length || b.uses - a.uses; });
|
|
160
|
+
|
|
161
|
+
var propList = [];
|
|
162
|
+
for (var pk in props) if (Object.prototype.hasOwnProperty.call(props, pk)) propList.push({ name: pk, value: props[pk] });
|
|
163
|
+
propList.sort(function (a, b) { return a.name < b.name ? -1 : 1; });
|
|
164
|
+
|
|
165
|
+
var observation = {
|
|
166
|
+
observationVersion: 1,
|
|
167
|
+
source: {
|
|
168
|
+
url: location.href,
|
|
169
|
+
collectedAt: new Date().toISOString(),
|
|
170
|
+
how: "browser",
|
|
171
|
+
pages: [location.href],
|
|
172
|
+
title: document.title,
|
|
173
|
+
viewport: innerWidth + "x" + innerHeight,
|
|
174
|
+
blockedSheets: blockedSheets,
|
|
175
|
+
},
|
|
176
|
+
stats: { sheets: readSheets, rules: 0, declarations: 0, elements: elements },
|
|
177
|
+
colors: colors.ranked(true),
|
|
178
|
+
fontStacks: fontStacks.ranked(true),
|
|
179
|
+
fontSizes: fontSizes.ranked(true),
|
|
180
|
+
lineHeights: lineHeights.ranked(true),
|
|
181
|
+
fontWeights: fontWeights.ranked(true),
|
|
182
|
+
spacings: spacings.ranked(false),
|
|
183
|
+
radii: radii.ranked(true),
|
|
184
|
+
shadows: shadows.ranked(true),
|
|
185
|
+
motions: motions.ranked(false),
|
|
186
|
+
customProperties: propList,
|
|
187
|
+
classFamilies: famList.slice(0, 40),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
var json = JSON.stringify(observation, null, 2);
|
|
191
|
+
try { window.__augurObservation = observation; } catch (e) { /* sandboxed page */ }
|
|
192
|
+
|
|
193
|
+
/* Three ways out, because a console differs by browser and by how locked down the page
|
|
194
|
+
is. The file download is the one that always works; the rest are conveniences. */
|
|
195
|
+
try {
|
|
196
|
+
var name = "observation-" + location.hostname.replace(/[^a-z0-9]+/gi, "-") + "-" + Date.now() + ".json";
|
|
197
|
+
var a = document.createElement("a");
|
|
198
|
+
a.href = URL.createObjectURL(new Blob([json], { type: "application/json" }));
|
|
199
|
+
a.download = name;
|
|
200
|
+
a.click();
|
|
201
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); }, 4000);
|
|
202
|
+
console.log("canon: saved " + name + " to your downloads — move it into your canon folder.");
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.log("canon: could not download; copy the JSON printed below.");
|
|
205
|
+
}
|
|
206
|
+
try { if (typeof copy === "function") { copy(json); console.log("canon: also copied to your clipboard."); } } catch (e) { /* not a console */ }
|
|
207
|
+
|
|
208
|
+
console.log("canon: " + elements + " elements, " + observation.colors.length + " colours, "
|
|
209
|
+
+ observation.fontStacks.length + " font stacks, " + propList.length + " custom properties"
|
|
210
|
+
+ (blockedSheets ? ", " + blockedSheets + " stylesheet(s) unreadable (cross-origin — computed styles still counted)" : "")
|
|
211
|
+
+ ". It is on window.__augurObservation too.");
|
|
212
|
+
console.log("canon: run this on three or four DIFFERENT screens and keep every file. One page is not a design system.");
|
|
213
|
+
|
|
214
|
+
return observation;
|
|
215
|
+
})();
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canon/emit — a graded canon becomes the workspace's design system.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is deterministic text generation from `canon.json`. The judgement was
|
|
5
|
+
* spent upstream, by the user's own agent, deciding which of a product's colours is the
|
|
6
|
+
* hairline; this file only writes it down in the one shape the rest of the engine reads
|
|
7
|
+
* (agents/ui-skill.md): a tokens stylesheet whose custom properties are the roles, a
|
|
8
|
+
* components stylesheet that names nothing but those tokens, the skill manifest, and the
|
|
9
|
+
* workspace registry the comment overlay labels components from.
|
|
10
|
+
*
|
|
11
|
+
* TWO RULES MAKE IT SAFE TO RUN ON A WORKSPACE THAT ALREADY HAS SCREENS IN IT.
|
|
12
|
+
*
|
|
13
|
+
* 1. It REPLACES the tokens file and never touches the components file. The roles are
|
|
14
|
+
* the contract; a workspace born from the seed already links `<prefix>-tokens.css`
|
|
15
|
+
* from every prototype, so replacing values re-skins every screen at once and
|
|
16
|
+
* breaks none of them. `<prefix>-ui.css` is the workspace's own writing and is not
|
|
17
|
+
* the extractor's to rewrite.
|
|
18
|
+
* 2. Extracted components go in their OWN file, `<prefix>-canon.css`, and a class that
|
|
19
|
+
* already exists in the workspace is a refusal, not a silent redefinition — two
|
|
20
|
+
* rules for `.x-card` in two files is a bug that only shows up on one screen.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { ROLES, ROLE_BY_NAME, validateCanon } from "./schema.mjs";
|
|
24
|
+
|
|
25
|
+
const GROUP_TITLES = {
|
|
26
|
+
paper: ["Paper", "the surfaces, from the desk up"],
|
|
27
|
+
ink: ["Ink", "everything written on the paper"],
|
|
28
|
+
mark: ["The one hot ink", "reserve it for the thing a person is meant to act on"],
|
|
29
|
+
state: ["States", ""],
|
|
30
|
+
type: ["Type", ""],
|
|
31
|
+
space: ["Space", "every gap in the workspace is one of these"],
|
|
32
|
+
shape: ["Shape", ""],
|
|
33
|
+
measure: ["Measure", ""],
|
|
34
|
+
motion: ["Motion", ""],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const rule = (title, sub) => {
|
|
38
|
+
const head = ` /* ── ${title} ${"─".repeat(Math.max(3, 66 - title.length))} */`;
|
|
39
|
+
return sub ? `${head}\n /* ${sub} */` : head;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The tokens stylesheet: one custom property per role, in schema order. */
|
|
43
|
+
export function renderTokensCss({ prefix, tokens, extras = {}, derived = [], source = {} }) {
|
|
44
|
+
const out = [];
|
|
45
|
+
out.push(`/* Design tokens — every colour, size and rhythm this workspace uses, in one file.`);
|
|
46
|
+
out.push(` Change a value here and every screen that links this stylesheet moves with it.`);
|
|
47
|
+
out.push(``);
|
|
48
|
+
if (source.url) out.push(` Extracted from ${source.url}${source.collectedAt ? ` on ${String(source.collectedAt).slice(0, 10)}` : ""}.`);
|
|
49
|
+
out.push(` The names are roles, not descriptions: the same list every workspace fills, so a`);
|
|
50
|
+
out.push(` prototype written against one design system works against another. Edit the`);
|
|
51
|
+
out.push(` values freely — it is yours the moment you change it. */`);
|
|
52
|
+
out.push(``);
|
|
53
|
+
out.push(`:root {`);
|
|
54
|
+
let group = null;
|
|
55
|
+
const width = Math.max(...ROLES.map((r) => r.role.length)) + prefix.length + 5;
|
|
56
|
+
for (const r of ROLES) {
|
|
57
|
+
if (r.group !== group) { if (group) out.push(""); out.push(rule(...GROUP_TITLES[r.group])); group = r.group; }
|
|
58
|
+
const name = ` --${prefix}-${r.role}:`;
|
|
59
|
+
const value = String(tokens[r.role]);
|
|
60
|
+
const note = derived.includes(r.role) ? ` /* computed from what was observed */` : ` /* ${r.what} */`;
|
|
61
|
+
out.push(`${name.padEnd(width)} ${value};${value.length + name.length < 96 ? note : ""}`);
|
|
62
|
+
}
|
|
63
|
+
const extraNames = Object.keys(extras);
|
|
64
|
+
if (extraNames.length) {
|
|
65
|
+
out.push("");
|
|
66
|
+
out.push(rule("This product's own", "values the roles above do not have a slot for"));
|
|
67
|
+
for (const n of extraNames) out.push(` --${prefix}-${n}: ${extras[n]};`);
|
|
68
|
+
}
|
|
69
|
+
out.push(`}`);
|
|
70
|
+
out.push(``);
|
|
71
|
+
return out.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The extracted components, one block each, in the order the canon lists them. */
|
|
75
|
+
export function renderCanonCss({ prefix, classPrefix, components, source = {} }) {
|
|
76
|
+
const out = [];
|
|
77
|
+
out.push(`/* Components extracted from the product itself.`);
|
|
78
|
+
if (source.url) out.push(` Source: ${source.url}${source.collectedAt ? `, ${String(source.collectedAt).slice(0, 10)}` : ""}.`);
|
|
79
|
+
out.push(``);
|
|
80
|
+
out.push(` Every rule below reads a token and hard-codes nothing, which is what lets one`);
|
|
81
|
+
out.push(` edit in the tokens file move all of them. Add to this file by hand freely; the`);
|
|
82
|
+
out.push(` extractor only rewrites it when you run it again. */`);
|
|
83
|
+
out.push(``);
|
|
84
|
+
for (const c of components) {
|
|
85
|
+
out.push(`/* ── ${c.label} — ${c.description} */`);
|
|
86
|
+
out.push(String(c.css).trim());
|
|
87
|
+
out.push(``);
|
|
88
|
+
}
|
|
89
|
+
if (!components.length) out.push(`/* Nothing named yet — the canon was tokens only. */`);
|
|
90
|
+
return out.join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge the extracted components into a workspace's `registry.json` BY NAME, keeping
|
|
95
|
+
* every entry the workspace already had. The registry is the workspace's, not the
|
|
96
|
+
* extractor's: an entry a person wrote a better description for stays written.
|
|
97
|
+
*/
|
|
98
|
+
export function mergeRegistry(existing, components) {
|
|
99
|
+
const items = Array.isArray(existing?.items) ? existing.items.slice() : [];
|
|
100
|
+
const at = new Map(items.map((it, i) => [it.name, i]));
|
|
101
|
+
const added = [], updated = [];
|
|
102
|
+
for (const c of components) {
|
|
103
|
+
const entry = { name: c.name, type: c.type, classes: c.classes, label: c.label, description: c.description };
|
|
104
|
+
if (at.has(c.name)) {
|
|
105
|
+
const i = at.get(c.name);
|
|
106
|
+
// Keep the workspace's own words; only fold in classes it does not know about.
|
|
107
|
+
const classes = [...new Set([...(items[i].classes || []), ...c.classes])];
|
|
108
|
+
if (classes.length !== (items[i].classes || []).length) { items[i] = { ...items[i], classes }; updated.push(c.name); }
|
|
109
|
+
} else { items.push(entry); added.push(c.name); }
|
|
110
|
+
}
|
|
111
|
+
return { registry: { ...(existing || {}), items }, added, updated };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Add the generated files to the skill's declared inventory, and the prefixes to it. */
|
|
115
|
+
export function mergeSkillJson(existing, { prefix, classPrefix, files }) {
|
|
116
|
+
const skill = { ...(existing || {}) };
|
|
117
|
+
const assets = new Set(Array.isArray(skill.assets) ? skill.assets : []);
|
|
118
|
+
for (const f of files) assets.add(f);
|
|
119
|
+
skill.assets = [...assets];
|
|
120
|
+
const prefixes = new Set(Array.isArray(skill.cssPrefixes) ? skill.cssPrefixes : []);
|
|
121
|
+
prefixes.add(prefix);
|
|
122
|
+
if (classPrefix) prefixes.add(classPrefix);
|
|
123
|
+
skill.cssPrefixes = [...prefixes];
|
|
124
|
+
return skill;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The canon's own record: where it came from, what it names, what was computed. */
|
|
128
|
+
export function renderCanonMd({ prefix, classPrefix, tokens, derived, components, source, observation }) {
|
|
129
|
+
const out = [];
|
|
130
|
+
out.push(`# The canon`);
|
|
131
|
+
out.push(``);
|
|
132
|
+
out.push(`This workspace's design system, ${source?.url ? `extracted from ${source.url}` : "extracted from the product"}${source?.collectedAt ? ` on ${String(source.collectedAt).slice(0, 10)}` : ""}.`);
|
|
133
|
+
out.push(``);
|
|
134
|
+
out.push(`- Tokens: \`${prefix}-tokens.css\` — ${ROLES.length} roles, all filled.`);
|
|
135
|
+
if (derived?.length) out.push(`- ${derived.length} of them were COMPUTED from the ones observed, not seen in the product: ${derived.join(", ")}. Change any of them by hand if the product does have its own.`);
|
|
136
|
+
out.push(`- Components: \`${prefix}-canon.css\` — ${components.length} named, classed \`.${classPrefix}-*\`.`);
|
|
137
|
+
if (observation) {
|
|
138
|
+
out.push(`- Evidence: ${observation.source?.pages?.length || 1} page(s), ${observation.stats?.elements || 0} elements, ${(observation.colors || []).length} distinct colours seen. Collected ${observation.source?.how === "browser" ? "in the browser, signed in" : "over HTTP"}.`);
|
|
139
|
+
}
|
|
140
|
+
out.push(``);
|
|
141
|
+
out.push(`## Pulling from it`);
|
|
142
|
+
out.push(``);
|
|
143
|
+
out.push(`Reference the skill by its canonical relative path, exactly as every other`);
|
|
144
|
+
out.push(`prototype does — the build rewrites it so the page works on disk and on the site:`);
|
|
145
|
+
out.push(``);
|
|
146
|
+
out.push("```html");
|
|
147
|
+
out.push(`<link rel="stylesheet" href="../../../skills/${prefix}-ui/${prefix}-tokens.css">`);
|
|
148
|
+
out.push(`<link rel="stylesheet" href="../../../skills/${prefix}-ui/${prefix}-canon.css">`);
|
|
149
|
+
out.push("```");
|
|
150
|
+
out.push(``);
|
|
151
|
+
if (components.length) {
|
|
152
|
+
out.push(`## The vocabulary`);
|
|
153
|
+
out.push(``);
|
|
154
|
+
out.push(`| Name | Classes | What it is |`);
|
|
155
|
+
out.push(`| --- | --- | --- |`);
|
|
156
|
+
// Every class, not only the first: the modifiers are most of a vocabulary, and a
|
|
157
|
+
// table that hides them is a document that tells you the parts exist and not how to
|
|
158
|
+
// ask for the one you want.
|
|
159
|
+
for (const c of components) out.push(`| ${c.name} | ${c.classes.map((cl) => `\`.${cl}\``).join(" ")} | ${c.description} |`);
|
|
160
|
+
out.push(``);
|
|
161
|
+
}
|
|
162
|
+
out.push(`## Re-running it`);
|
|
163
|
+
out.push(``);
|
|
164
|
+
out.push(`Collect again, re-map, \`augur canon apply\`. The tokens file is regenerated and`);
|
|
165
|
+
out.push(`the components file is rewritten; nothing else in the skill is touched, so a`);
|
|
166
|
+
out.push(`component you wrote by hand in the workspace's own stylesheet survives.`);
|
|
167
|
+
out.push(``);
|
|
168
|
+
return out.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every class a stylesheet defines. Comments are stripped first, or a `/* see foo.css */`
|
|
173
|
+
* lands `.css` in the list — which the brief then hands a person as a name they may not
|
|
174
|
+
* use. ONE function, exported, because the brief promises exactly what `apply` refuses
|
|
175
|
+
* and two regexes would eventually promise something else.
|
|
176
|
+
*/
|
|
177
|
+
export function classNamesIn(css) {
|
|
178
|
+
const bare = String(css).replace(/\/\*[\s\S]*?\*\//g, "");
|
|
179
|
+
return [...new Set([...bare.matchAll(/\.(-?[a-zA-Z_][\w-]*)/g)].map((m) => m[1]))].sort();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Everything `augur canon apply` would write, as `{path, text}` — plus the refusals.
|
|
184
|
+
* Pure: the caller does the IO, which is what lets `--dry-run` be the same code path.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} a
|
|
187
|
+
* @param {object} a.canon the graded answer
|
|
188
|
+
* @param {string} a.skillDir relative path of the skill directory, e.g. `skills/x-ui`
|
|
189
|
+
* @param {string} a.skillPrefix the workspace's existing token prefix
|
|
190
|
+
* @param {object} a.existingSkillJson
|
|
191
|
+
* @param {object} a.existingRegistry
|
|
192
|
+
* @param {string} a.existingComponentCss the workspace's own `<prefix>-ui.css`, for collisions
|
|
193
|
+
* @param {object} [a.observation]
|
|
194
|
+
*/
|
|
195
|
+
export function planApply({ canon, skillDir, skillPrefix, existingSkillJson, existingRegistry, existingComponentCss = "", observation = null }) {
|
|
196
|
+
const graded = validateCanon(canon);
|
|
197
|
+
const errors = [...graded.errors];
|
|
198
|
+
const notes = [];
|
|
199
|
+
const prefix = skillPrefix || graded.prefix;
|
|
200
|
+
|
|
201
|
+
if (skillPrefix && graded.prefix !== skillPrefix) {
|
|
202
|
+
// Renaming a workspace's tokens would orphan every prototype that reads them.
|
|
203
|
+
notes.push(`the canon calls itself "${graded.prefix}" and this workspace's design system is "${skillPrefix}" — emitting as "${skillPrefix}" so the screens that already read these tokens keep working`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const components = Array.isArray(canon.components) ? canon.components : [];
|
|
207
|
+
const taken = new Set(classNamesIn(existingComponentCss));
|
|
208
|
+
for (const c of components) {
|
|
209
|
+
for (const cl of c.classes || []) {
|
|
210
|
+
if (taken.has(cl)) errors.push(`components (${c.name}): .${cl} is already defined in this workspace's own stylesheet — rename it, or the two rules will fight on one screen`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const extras = {};
|
|
215
|
+
for (const [k, v] of Object.entries(canon.tokens || {})) if (k.startsWith("x-") && v) extras[k.slice(2)] = v;
|
|
216
|
+
|
|
217
|
+
const files = [`${prefix}-tokens.css`, `${prefix}-canon.css`];
|
|
218
|
+
const { registry, added, updated } = mergeRegistry(existingRegistry, components);
|
|
219
|
+
const writes = [
|
|
220
|
+
{ path: `${skillDir}/${prefix}-tokens.css`, text: renderTokensCss({ prefix, tokens: graded.tokens, extras, derived: graded.derived, source: canon.source }) },
|
|
221
|
+
{ path: `${skillDir}/${prefix}-canon.css`, text: renderCanonCss({ prefix, classPrefix: graded.classPrefix, components, source: canon.source }) },
|
|
222
|
+
{ path: `${skillDir}/skill.json`, text: JSON.stringify(mergeSkillJson(existingSkillJson, { prefix, classPrefix: graded.classPrefix, files }), null, 2) + "\n" },
|
|
223
|
+
{ path: `registry.json`, text: JSON.stringify(registry, null, 2) + "\n" },
|
|
224
|
+
{ path: `${skillDir}/CANON.md`, text: renderCanonMd({ prefix, classPrefix: graded.classPrefix, tokens: graded.tokens, derived: graded.derived, components, source: canon.source, observation }) },
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
if (added.length) notes.push(`registry: added ${added.join(", ")}`);
|
|
228
|
+
if (updated.length) notes.push(`registry: widened ${updated.join(", ")}`);
|
|
229
|
+
if (graded.derived.length) notes.push(`${graded.derived.length} role(s) computed rather than observed: ${graded.derived.join(", ")}`);
|
|
230
|
+
|
|
231
|
+
return { ok: errors.length === 0, errors, warnings: graded.warnings, writes, notes, prefix, graded };
|
|
232
|
+
}
|