@struq/skill 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@struq/skill",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Install the free Struq skill into Claude Code, Cursor, Codex and other AI coding tools: websites at the level of a design studio, built by your own AI.",
5
5
  "license": "MIT",
6
6
  "author": "Struq (https://struq.nl)",
@@ -45,6 +45,9 @@ connected); the rest are judged by eye. The fix is beside each.
45
45
  - Emoji and the cliche icons (sparkles, rocket, lightning, bulb, wand). Fix: Struq Icons, functional only. (check)
46
46
  - Invented facts: testimonials, client logos, stats, awards, prices, addresses, hours, bios, quotes the brief did not
47
47
  give. Fix: leave them out and list them under Assumptions.
48
+ - **Notes to the client on the page:** "het e-mailadres volgt zodra de studio het aanlevert", "coming soon", "[telefoon]",
49
+ "to be confirmed". Honest, but a visitor reads it as an unfinished site. Fix: leave the missing fact out of the page
50
+ (drop the line, the column or the link) and list it under Assumptions in STRUQ.md. (check)
48
51
 
49
52
  ## Judged by eye
50
53
 
@@ -154,6 +154,22 @@ const plan = read("STRUQ.md");
154
154
  }
155
155
  if (brokenPhotos.length) add(G, "FAIL", "These image files are not real images (an error page saved as .webp?). Delete them and run images.mjs again.", brokenPhotos.map(rel));
156
156
 
157
+ // A file whose size differs from the one images.mjs recorded is not the photo its alt and caption
158
+ // describe: a run of another pack, or a hand copy, left it under the same name.
159
+ const manifestFile = join(pub, "images", "struq-pack.json");
160
+ if (existsSync(manifestFile)) {
161
+ let recorded = [];
162
+ try {
163
+ recorded = JSON.parse(readFileSync(manifestFile, "utf8")).images ?? [];
164
+ } catch {
165
+ add(G, "WARN", "public/images/struq-pack.json is not readable JSON. Run images.mjs again.");
166
+ }
167
+ const mismatched = recorded
168
+ .filter((m) => typeof m.bytes === "number" && existsSync(join(root, m.file)) && statSync(join(root, m.file)).size !== m.bytes)
169
+ .map((m) => m.file);
170
+ if (mismatched.length) add(G, "FAIL", "These photos are not the ones their alt and caption describe (left by an earlier pack or copied by hand). Run images.mjs again with the pack you chose.", mismatched);
171
+ }
172
+
157
173
  // Each pack reference (images.card[2], images.card.slice(0, 5)) or own image tag counts once.
158
174
  const used = own.reduce((n, f) => n + (f.text.match(/images\.(hero|band|card|column|detail|backdrop)\b|<img\b|src=["'{`]\/images\//g) ?? []).length, 0);
159
175
  const total = Object.values(slotCount).reduce((a, b) => a + b, 0);
@@ -217,6 +233,11 @@ const plan = read("STRUQ.md");
217
233
  };
218
234
  const ownOnly = { scope: own };
219
235
 
236
+ // Copy
237
+ tell("Notes to the client on the page",
238
+ /volg(?:t|en) zodra|zodra (?:de studio|de klant|je|jullie|we)\b[^"'<>]{0,40}(?:aanlever|doorgeef|doorgeven|binnen)|nog aan te leveren|wordt nog aangevuld|lorem ipsum|\bcoming soon\b|to be confirmed|\bTBC\b|\[(?:naam|adres|telefoon|e-?mail|name|phone|address)\]/i,
239
+ "a line written to the client or the developer is not copy. Leave a missing fact out of the page and list it under Assumptions: in STRUQ.md.");
240
+
220
241
  // Type and labels
221
242
  tell("Eyebrows and uppercase labels", /\buppercase\b|text-transform:\s*uppercase|\b(eyebrow|kicker|overline)\b/i,
222
243
  "small uppercase labels above headings, uppercase marquees and tracked caps are AI tells. Delete the label; the heading, a hairline rule or a jump in scale does the wayfinding. A deliberate all-caps wordmark passes when its element carries data-wordmark.",
@@ -18,8 +18,10 @@
18
18
  // 4. If struq.nl cannot be reached, or a file fails its check, it writes a graded fallback plate
19
19
  // in the site palette for that slot instead, so no frame is ever empty, and says so.
20
20
  //
21
- // Node 18 or later, no dependencies. Safe to run again: valid files are kept.
22
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
21
+ // Node 18 or later, no dependencies. Safe to run again: a file is kept only when the last run
22
+ // downloaded it from the same source (struq-pack.json records each one); a file from another pack
23
+ // is replaced, and a slot file the new pack no longer fills is removed.
24
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
23
25
  import { join, resolve } from "node:path";
24
26
  import { appendLog } from "./log.mjs";
25
27
  import { fitImages, packForBrief, packReason } from "./packs.mjs";
@@ -201,6 +203,28 @@ ${body}
201
203
  return file;
202
204
  }
203
205
 
206
+ /** file -> source from the last run's struq-pack.json; empty when there was no run or it is unreadable. */
207
+ function readManifest(outDir) {
208
+ try {
209
+ const data = JSON.parse(readFileSync(join(outDir, "struq-pack.json"), "utf8"));
210
+ return new Map((data.images ?? []).map((i) => [i.file, i.source]));
211
+ } catch {
212
+ return new Map();
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Removes the slot files (hero-1.webp, card-3.svg, ...) this run did not write, so a photo or plate
218
+ * from an earlier pack can never sit under this pack's caption. Other files in public/images stay.
219
+ */
220
+ function removeStale(outDir, manifest) {
221
+ const written = new Set(manifest.map((m) => m.file.split("/").pop()));
222
+ const slotFile = new RegExp(`^(${SLOTS.join("|")})-\\d+\\.(webp|svg)$`);
223
+ for (const name of readdirSync(outDir)) {
224
+ if (slotFile.test(name) && !written.has(name)) unlinkSync(join(outDir, name));
225
+ }
226
+ }
227
+
204
228
  async function list() {
205
229
  const index = await getJson(API);
206
230
  console.log(`${index.packCount} packs, ${index.totalImages} images, ${index.license}\n`);
@@ -220,6 +244,7 @@ async function main() {
220
244
  const outDir = join(root, "public", "images");
221
245
  mkdirSync(outDir, { recursive: true });
222
246
  const palette = readPalette();
247
+ const previous = readManifest(outDir);
223
248
 
224
249
  let pack = null;
225
250
  let droppedImages = [];
@@ -268,7 +293,9 @@ async function main() {
268
293
  while (cursor < jobs.length) {
269
294
  const job = jobs[cursor++];
270
295
  const target = join(outDir, `${job.name}.webp`);
271
- const ok = fileIsImage(target) || (await download(imageUrl(job.img), target));
296
+ const source = imageUrl(job.img);
297
+ const kept = previous.get(`public/images/${job.name}.webp`) === source && fileIsImage(target);
298
+ const ok = kept || (await download(source, target));
272
299
  if (ok) {
273
300
  real++;
274
301
  entries.push({
@@ -282,7 +309,8 @@ async function main() {
282
309
  description: job.img.description ?? "",
283
310
  negativeSpace: job.img.negativeSpace ?? "",
284
311
  });
285
- manifest.push({ file: `public/images/${job.name}.webp`, id: job.img.id, slot: job.img.slot, source: imageUrl(job.img) });
312
+ // bytes lets check.mjs tell this file from one an earlier pack left under the same name.
313
+ manifest.push({ file: `public/images/${job.name}.webp`, id: job.img.id, slot: job.img.slot, source, bytes: statSync(target).size });
286
314
  } else {
287
315
  plates++;
288
316
  const size = [job.img.width ?? FALLBACK_SIZE[job.img.slot][0], job.img.height ?? FALLBACK_SIZE[job.img.slot][1]];
@@ -326,6 +354,7 @@ async function main() {
326
354
  }
327
355
 
328
356
  entries.sort((a, b) => SLOTS.indexOf(a.slot) - SLOTS.indexOf(b.slot) || a.order - b.order);
357
+ removeStale(outDir, manifest);
329
358
  writeFileSync(join(outDir, "struq-pack.json"), JSON.stringify({ pack: slug, images: manifest }, null, 2) + "\n");
330
359
  const module = writeModule(entries, slug, !pack, imageFit);
331
360