synthesisui 0.16.74 → 0.16.75
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/dist/commands/add.js +19 -1
- package/dist/commands/import.js +1 -64
- package/dist/rule-filter.js +62 -0
- package/dist/skill-import.js +16 -0
- package/dist/stack.js +77 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -6,6 +6,8 @@ import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.
|
|
|
6
6
|
import { buildGuide } from "../guide.js";
|
|
7
7
|
import { body as line, section, snippet } from "../output.js";
|
|
8
8
|
import { fetchDesignSystem } from "../registry.js";
|
|
9
|
+
import { describeFiltered, rulesForProject } from "../rule-filter.js";
|
|
10
|
+
import { detectStack } from "../stack.js";
|
|
9
11
|
async function readRootLock(path) {
|
|
10
12
|
try {
|
|
11
13
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -70,7 +72,19 @@ export async function add(slug, opts) {
|
|
|
70
72
|
await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
71
73
|
// 5b. governance rules (personal DS) → rules.md at the slug root (stable path,
|
|
72
74
|
// highest authority; the GUIDE tells the agent to read it first)
|
|
73
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The structured set wins when the registry sent one, because only this side
|
|
77
|
+
* knows what the project is built with - a rule that names `next` has no
|
|
78
|
+
* business in a Vue app. Falling back to the flat strings keeps an older
|
|
79
|
+
* registry working unchanged.
|
|
80
|
+
*/
|
|
81
|
+
const stack = await detectStack(projectRoot);
|
|
82
|
+
const rules = payload.ruleSet && payload.ruleSet.length > 0
|
|
83
|
+
? rulesForProject(payload.ruleSet, stack)
|
|
84
|
+
: (payload.rules ?? []);
|
|
85
|
+
const leftOut = payload.ruleSet
|
|
86
|
+
? describeFiltered(payload.ruleSet, stack)
|
|
87
|
+
: null;
|
|
74
88
|
if (rules.length > 0) {
|
|
75
89
|
const body = `# ${payload.name} - Rules\n\n` +
|
|
76
90
|
"> Accumulated rules for this design system. **Max authority - follow these first.**\n" +
|
|
@@ -79,6 +93,10 @@ export async function add(slug, opts) {
|
|
|
79
93
|
.join("\n")}\n`;
|
|
80
94
|
await writeFile(join(slugDir, "rules.md"), body, "utf8");
|
|
81
95
|
}
|
|
96
|
+
// NO SILENT DROPS. A rule removed because it belongs to another environment is
|
|
97
|
+
// a thing the person should hear once, not discover by its absence.
|
|
98
|
+
if (leftOut)
|
|
99
|
+
console.log(line(leftOut));
|
|
82
100
|
// 5c. structured philosophy (personal DS) → philosophy.md at the slug root.
|
|
83
101
|
// Narrative guidance (mission, principles, voice, motion doctrine…); the
|
|
84
102
|
// GUIDE points the agent here right after rules.md.
|
package/dist/commands/import.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
|
13
13
|
import { buildTable } from "../doctor/tokens.js";
|
|
14
14
|
import { rootClasses, rootTag, transcribe, transcribeParts, } from "../doctor/transcribe.js";
|
|
15
15
|
import { body, paint, section } from "../output.js";
|
|
16
|
+
import { detectStack } from "../stack.js";
|
|
16
17
|
import { walk, walkAll } from "./doctor.js";
|
|
17
18
|
/**
|
|
18
19
|
* How many distinct values travel, PER KIND.
|
|
@@ -93,70 +94,6 @@ async function siblingProjects(root) {
|
|
|
93
94
|
.map((w) => w.rel),
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Dependencies as the project actually resolves them - which means reading
|
|
98
|
-
* ANCESTOR package.json files too.
|
|
99
|
-
*
|
|
100
|
-
* Measured on our own repo: `import --dir apps/web` reported "plain css" for a
|
|
101
|
-
* Next + React + Tailwind app, because a workspace hoists those to the root and
|
|
102
|
-
* the leaf package.json lists only its own icons. Three levels up covers every
|
|
103
|
-
* pnpm/npm workspace layout without wandering into someone's home directory.
|
|
104
|
-
*/
|
|
105
|
-
async function resolveDeps(root) {
|
|
106
|
-
const deps = {};
|
|
107
|
-
let dir = root;
|
|
108
|
-
for (let up = 0; up < 4; up++) {
|
|
109
|
-
const raw = await readFile(join(dir, "package.json"), "utf8").catch(() => null);
|
|
110
|
-
if (raw) {
|
|
111
|
-
try {
|
|
112
|
-
const p = JSON.parse(raw);
|
|
113
|
-
// The nearest package.json wins on a version clash; we only ever ask
|
|
114
|
-
// whether a name is present, so first-seen is enough.
|
|
115
|
-
for (const [k, v] of Object.entries({
|
|
116
|
-
...p.dependencies,
|
|
117
|
-
...p.devDependencies,
|
|
118
|
-
})) {
|
|
119
|
-
if (deps[k] == null)
|
|
120
|
-
deps[k] = String(v);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// unreadable manifest costs the detection, not the run
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const parent = join(dir, "..");
|
|
128
|
-
if (parent === dir)
|
|
129
|
-
break;
|
|
130
|
-
dir = parent;
|
|
131
|
-
}
|
|
132
|
-
return deps;
|
|
133
|
-
}
|
|
134
|
-
async function detectStack(root) {
|
|
135
|
-
const stack = [];
|
|
136
|
-
const has = async (f) => (await readFile(join(root, f), "utf8").catch(() => null)) !== null;
|
|
137
|
-
const deps = await resolveDeps(root);
|
|
138
|
-
if (await has("components.json"))
|
|
139
|
-
stack.push("shadcn/ui");
|
|
140
|
-
if (deps.next)
|
|
141
|
-
stack.push("next");
|
|
142
|
-
else if (deps.vite)
|
|
143
|
-
stack.push("vite");
|
|
144
|
-
if (deps.react)
|
|
145
|
-
stack.push("react");
|
|
146
|
-
else if (deps.vue)
|
|
147
|
-
stack.push("vue");
|
|
148
|
-
else if (deps.svelte)
|
|
149
|
-
stack.push("svelte");
|
|
150
|
-
if (deps.tailwindcss)
|
|
151
|
-
stack.push("tailwind");
|
|
152
|
-
if (await has("tokens.json"))
|
|
153
|
-
stack.push("tokens.json");
|
|
154
|
-
// Nothing recognised is itself a finding: plain CSS is a supported entrance,
|
|
155
|
-
// and saying so beats an empty list that reads like a failed detection.
|
|
156
|
-
if (stack.length === 0)
|
|
157
|
-
stack.push("plain css");
|
|
158
|
-
return stack;
|
|
159
|
-
}
|
|
160
97
|
/**
|
|
161
98
|
* Their own vocabulary, read from stylesheets - the same harvest the doctor runs
|
|
162
99
|
* when nothing of ours is installed.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH RULES BELONG IN *THIS* PROJECT.
|
|
3
|
+
*
|
|
4
|
+
* A rule travels with the design system, and the system gets installed places it
|
|
5
|
+
* was never written for. "The state arrives through this hook" is true about Next
|
|
6
|
+
* and actively wrong about Vue - so a rule carries the environments it holds in,
|
|
7
|
+
* and the filtering happens HERE, because the consumer's stack is the one thing
|
|
8
|
+
* the platform cannot know (dono, 01/08).
|
|
9
|
+
*
|
|
10
|
+
* THE DEFAULT WHEN WE CANNOT TELL IS TO KEEP IT. A rule whose condition does not
|
|
11
|
+
* match is removed, because obeying it would be a mistake. A rule we cannot
|
|
12
|
+
* evaluate - the stack came back empty, or it names something we do not detect -
|
|
13
|
+
* stays, because a rule nobody can judge is safer present than silently gone.
|
|
14
|
+
* Those are two different situations and they get two different answers.
|
|
15
|
+
*/
|
|
16
|
+
const norm = (v) => v.trim().toLowerCase();
|
|
17
|
+
/**
|
|
18
|
+
* True when this rule holds in a project built with `stack`.
|
|
19
|
+
*
|
|
20
|
+
* `stack` is what `detectStack` reported, so `plain css` and an unrecognised
|
|
21
|
+
* entry both read as "we could not tell" rather than as a contradiction.
|
|
22
|
+
*/
|
|
23
|
+
export function ruleApplies(rule, stack) {
|
|
24
|
+
const when = (rule.when ?? []).map(norm).filter(Boolean);
|
|
25
|
+
if (when.length === 0)
|
|
26
|
+
return true;
|
|
27
|
+
const here = new Set(stack.map(norm));
|
|
28
|
+
// Nothing recognised: we are not in a position to rule anything out.
|
|
29
|
+
if (here.size === 0 || (here.size === 1 && here.has("plain css")))
|
|
30
|
+
return true;
|
|
31
|
+
return when.some((w) => here.has(w));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The lines for `rules.md`, in the order they were written.
|
|
35
|
+
*
|
|
36
|
+
* A scoped rule names its components, so an agent reading the file knows whether
|
|
37
|
+
* the line is about the thing it has open - and a rule about a RELATION names
|
|
38
|
+
* both, which is the whole reason the field exists.
|
|
39
|
+
*/
|
|
40
|
+
export function rulesForProject(rules, stack) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const rule of rules) {
|
|
43
|
+
if (!ruleApplies(rule, stack))
|
|
44
|
+
continue;
|
|
45
|
+
const text = rule.text?.trim();
|
|
46
|
+
if (!text)
|
|
47
|
+
continue;
|
|
48
|
+
const scope = rule.applies?.length > 0
|
|
49
|
+
? `${rule.applies.map((a) => `ds-${a}`).join(" + ")}: `
|
|
50
|
+
: "";
|
|
51
|
+
out.push(`${scope}${text}`);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** What was left out, and why - said out loud rather than subtracted quietly. */
|
|
56
|
+
export function describeFiltered(rules, stack) {
|
|
57
|
+
const dropped = rules.filter((r) => !ruleApplies(r, stack));
|
|
58
|
+
if (dropped.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const envs = [...new Set(dropped.flatMap((r) => r.when.map(norm)))].sort();
|
|
61
|
+
return `${dropped.length} rule${dropped.length === 1 ? "" : "s"} left out - ${dropped.length === 1 ? "it belongs" : "they belong"} to ${envs.join(", ")} and this project is ${stack.join(" · ")}`;
|
|
62
|
+
}
|
package/dist/skill-import.js
CHANGED
|
@@ -139,6 +139,7 @@ Open the project and answer the questions below. Then add a \`reading\` object t
|
|
|
139
139
|
"text": "A RootWrapper is what makes the app-shell components position correctly - never render a Sidebar or Topbar outside one",
|
|
140
140
|
"applies": ["RootWrapper", "Sidebar"],
|
|
141
141
|
"kind": "implementation",
|
|
142
|
+
"when": ["next"],
|
|
142
143
|
"files": 3,
|
|
143
144
|
"evidence": "all three app shells wrap them; none renders them loose"
|
|
144
145
|
},
|
|
@@ -213,6 +214,21 @@ kind limit a boundary, which the doctor can measure
|
|
|
213
214
|
The relation is the valuable one. \`[RootWrapper, Sidebar]\` says something neither name says
|
|
214
215
|
alone, and it is exactly what an agent needs in order not to assemble it wrongly.
|
|
215
216
|
|
|
217
|
+
**\`when\` - the environments a rule is true in.** Leave it out and the rule holds everywhere.
|
|
218
|
+
|
|
219
|
+
This exists because the system gets installed places it was never written for. *"The state arrives
|
|
220
|
+
through this hook"* is true about Next and **actively wrong** about Vue, so a rule that depends on
|
|
221
|
+
the framework has to say so - and the install filters against the project it lands in, because
|
|
222
|
+
only the project knows what it is built with.
|
|
223
|
+
|
|
224
|
+
One dimension: a member of the stack (\`next\`, \`vue\`, \`react\`, \`svelte\`, \`tailwind\`, \`shadcn/ui\`).
|
|
225
|
+
A rule may list several. It cannot combine conditions - \`next && !tailwind\` would be a query
|
|
226
|
+
language, and then somebody maintains an interpreter forever.
|
|
227
|
+
|
|
228
|
+
Use it only when the rule genuinely depends on the environment. Most rules do not: *"never render
|
|
229
|
+
a Sidebar loose"* is true about their design regardless of framework, and pinning it to \`next\`
|
|
230
|
+
would quietly drop it the day they add a second app.
|
|
231
|
+
|
|
216
232
|
**Report \`files\` honestly - it decides whether the rule governs.** Three or more files is a
|
|
217
233
|
habit and the rule arrives active; one file is a coincidence and it arrives as a candidate,
|
|
218
234
|
inactive, waiting for the person to promote it. You do not make that call; you report the
|
package/dist/stack.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THIS PROJECT IS BUILT WITH - and who needs to know.
|
|
3
|
+
*
|
|
4
|
+
* The census reports it so a system knows where it came from. The INSTALL reads
|
|
5
|
+
* it so a rule that only applies to Next does not land in a Vue project: a rule
|
|
6
|
+
* travels WITH the design system and is conditional on the consumer (dono, 01/08
|
|
7
|
+
* - "se o ambiente que estiver instalado esse DS for next, use isso").
|
|
8
|
+
*
|
|
9
|
+
* One implementation, because two detectors that disagreed would put a rule in a
|
|
10
|
+
* project the census said was something else.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
/**
|
|
15
|
+
* Dependencies as the project actually resolves them - which means reading
|
|
16
|
+
* ANCESTOR package.json files too.
|
|
17
|
+
*
|
|
18
|
+
* Measured on our own repo: `import --dir apps/web` reported "plain css" for a
|
|
19
|
+
* Next + React + Tailwind app, because a workspace hoists those to the root and
|
|
20
|
+
* the leaf package.json lists only its own icons. Three levels up covers every
|
|
21
|
+
* pnpm/npm workspace layout without wandering into someone's home directory.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveDeps(root) {
|
|
24
|
+
const deps = {};
|
|
25
|
+
let dir = root;
|
|
26
|
+
for (let up = 0; up < 4; up++) {
|
|
27
|
+
const raw = await readFile(join(dir, "package.json"), "utf8").catch(() => null);
|
|
28
|
+
if (raw) {
|
|
29
|
+
try {
|
|
30
|
+
const p = JSON.parse(raw);
|
|
31
|
+
// The nearest package.json wins on a version clash; we only ever ask
|
|
32
|
+
// whether a name is present, so first-seen is enough.
|
|
33
|
+
for (const [k, v] of Object.entries({
|
|
34
|
+
...p.dependencies,
|
|
35
|
+
...p.devDependencies,
|
|
36
|
+
})) {
|
|
37
|
+
if (deps[k] == null)
|
|
38
|
+
deps[k] = String(v);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// unreadable manifest costs the detection, not the run
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const parent = join(dir, "..");
|
|
46
|
+
if (parent === dir)
|
|
47
|
+
break;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
return deps;
|
|
51
|
+
}
|
|
52
|
+
export async function detectStack(root) {
|
|
53
|
+
const stack = [];
|
|
54
|
+
const has = async (f) => (await readFile(join(root, f), "utf8").catch(() => null)) !== null;
|
|
55
|
+
const deps = await resolveDeps(root);
|
|
56
|
+
if (await has("components.json"))
|
|
57
|
+
stack.push("shadcn/ui");
|
|
58
|
+
if (deps.next)
|
|
59
|
+
stack.push("next");
|
|
60
|
+
else if (deps.vite)
|
|
61
|
+
stack.push("vite");
|
|
62
|
+
if (deps.react)
|
|
63
|
+
stack.push("react");
|
|
64
|
+
else if (deps.vue)
|
|
65
|
+
stack.push("vue");
|
|
66
|
+
else if (deps.svelte)
|
|
67
|
+
stack.push("svelte");
|
|
68
|
+
if (deps.tailwindcss)
|
|
69
|
+
stack.push("tailwind");
|
|
70
|
+
if (await has("tokens.json"))
|
|
71
|
+
stack.push("tokens.json");
|
|
72
|
+
// Nothing recognised is itself a finding: plain CSS is a supported entrance,
|
|
73
|
+
// and saying so beats an empty list that reads like a failed detection.
|
|
74
|
+
if (stack.length === 0)
|
|
75
|
+
stack.push("plain css");
|
|
76
|
+
return stack;
|
|
77
|
+
}
|
package/package.json
CHANGED