@smartsoft001-mobilems/claude-plugins 2.88.0 → 2.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +27 -2
  2. package/package.json +1 -1
  3. package/plugins/mobilems/.claude-plugin/README.md +21 -2
  4. package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
  5. package/plugins/mobilems/skills/mobilems-articles/SKILL.md +52 -20
  6. package/plugins/mobilems/skills/mobilems-articles/references/module-and-routes.md +24 -0
  7. package/plugins/mobilems/skills/mobilems-articles/scripts/check-translations.js +90 -0
  8. package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +53 -0
  9. package/plugins/mobilems/skills/mobilems-devops-config/SKILL.md +141 -0
  10. package/plugins/mobilems/skills/mobilems-devops-config/references/docker.md +58 -0
  11. package/plugins/mobilems/skills/mobilems-devops-config/references/env-and-lint.md +59 -0
  12. package/plugins/mobilems/skills/mobilems-devops-config/references/github-hygiene.md +54 -0
  13. package/plugins/mobilems/skills/mobilems-devops-config/references/tooling.md +105 -0
  14. package/plugins/mobilems/skills/mobilems-devops-config/references/workflows.md +410 -0
  15. package/plugins/mobilems/skills/mobilems-devops-config/scripts/detect-foundation.sh +47 -0
  16. package/plugins/mobilems/skills/mobilems-devops-config/scripts/verify-devops.sh +96 -0
  17. package/plugins/mobilems/skills/mobilems-events/SKILL.md +27 -1
  18. package/plugins/mobilems/skills/mobilems-events/references/module-and-routes.md +24 -0
  19. package/plugins/mobilems/skills/mobilems-events/scripts/verify-module.sh +43 -0
  20. package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +46 -14
  21. package/plugins/mobilems/skills/mobilems-object-collections/references/lib-contents.md +11 -0
  22. package/plugins/mobilems/skills/mobilems-object-collections/references/module-and-routes.md +26 -0
  23. package/plugins/mobilems/skills/mobilems-object-collections/scripts/check-translations.js +90 -0
  24. package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +54 -0
  25. package/plugins/mobilems/skills/mobilems-objects/SKILL.md +42 -10
  26. package/plugins/mobilems/skills/mobilems-objects/references/module-and-routes.md +24 -0
  27. package/plugins/mobilems/skills/mobilems-objects/scripts/check-translations.js +90 -0
  28. package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +53 -0
  29. package/plugins/mobilems/skills/mobilems-partners/SKILL.md +30 -2
  30. package/plugins/mobilems/skills/mobilems-partners/references/module-and-routes.md +24 -0
  31. package/plugins/mobilems/skills/mobilems-partners/scripts/check-translations.js +90 -0
  32. package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +53 -0
  33. package/plugins/mobilems/skills/mobilems-project-init/SKILL.md +68 -8
  34. package/plugins/mobilems/skills/mobilems-project-init/references/branding-reset.md +112 -0
  35. package/plugins/mobilems/skills/mobilems-project-init/references/claude-config.md +7 -0
  36. package/plugins/mobilems/skills/mobilems-project-init/references/config-providers.md +1 -1
  37. package/plugins/mobilems/skills/mobilems-project-init/references/qa-hardening.md +30 -30
  38. package/plugins/mobilems/skills/mobilems-project-init/references/shared-lib.md +42 -5
  39. package/plugins/mobilems/skills/mobilems-project-init/references/tailwind-styles.md +33 -23
  40. package/plugins/mobilems/skills/mobilems-project-init/scripts/verify-init.sh +62 -9
  41. package/plugins/mobilems/skills/mobilems-thematic-publications/SKILL.md +25 -1
  42. package/plugins/mobilems/skills/mobilems-thematic-publications/references/lib-contents.md +11 -0
  43. package/plugins/mobilems/skills/mobilems-thematic-publications/references/module-and-routes.md +26 -0
  44. package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/verify-module.sh +43 -0
@@ -62,6 +62,30 @@ import { routes } from './objects.routes';
62
62
  export class MuseumObjectsModule {}
63
63
  ```
64
64
 
65
+ > **The `Masonry` provider exists only to feed the framework's `MasonryGridComponent`.** It is DI
66
+ > plumbing, not a grid — never call `new Masonry(...)`, keep a `MasonryInstance`, or re-implement the
67
+ > layout/`reloadItems` dance in a page component. A hand-rolled CSS-grid or column layout is the same
68
+ > defect. `scripts/verify-module.sh` fails the run on both.
69
+
70
+ The list page renders the grid like this — copy this shape, it is the required one:
71
+
72
+ ```html
73
+ <smart-mobilems-masonry-grid [list]="list()" [options]="{ gutter: 24 }">
74
+ <ng-template #masonryContentTpl>
75
+ <div class="masonry-grid-sizer"></div>
76
+ @for (item of list(); track item.id) {
77
+ <div class="masonry-item"><smart-objects-grid-item [item]="item" /></div>
78
+ }
79
+ </ng-template>
80
+ </smart-mobilems-masonry-grid>
81
+ ```
82
+
83
+ `.masonry-grid-sizer` and `.masonry-item` are required — they are the component's default
84
+ `columnWidth` / `itemSelector`. The card (shared-components.md) is the only module-specific part. Inputs,
85
+ public methods and the SSR provider are documented in the framework catalog,
86
+ [`packages/shared/angular/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md#masonrygridcomponent)
87
+ (installed as `node_modules/@smartsoft001-mobilems/angular/README.md`).
88
+
65
89
  ## `objects.routes.ts`
66
90
 
67
91
  ```ts
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-translations.js "<GROUP GROUP …>" [reference-root]
4
+ *
5
+ * Run from the workspace root (verify-module.sh cd's there). Verifies the
6
+ * module's translation groups in apps/web/public/translations.json:
7
+ * 1. each group exists and is non-empty in BOTH pl and en,
8
+ * 2. every literal GROUP.* key referenced in libs/ or apps/web/src exists
9
+ * in both languages (dead references = defect),
10
+ * 3. with a reference root: every reference key of the group exists in the
11
+ * generated file — missing keys are listed (the module is incomplete).
12
+ * Prints PASS/FAIL lines in the verify-module.sh idiom; exit 1 on any FAIL.
13
+ */
14
+ const fs = require("fs");
15
+
16
+ const [groupsArg, refRoot] = process.argv.slice(2);
17
+ const groups = (groupsArg || "").split(/\s+/).filter(Boolean);
18
+ if (!groups.length) { console.log(" FAIL: no translation groups given"); process.exit(1); }
19
+
20
+ const TRANS = "apps/web/public/translations.json";
21
+ let fail = 0;
22
+ const pass = (m) => console.log(` PASS: ${m}`);
23
+ const bad = (m) => { console.log(` FAIL: ${m}`); fail = 1; };
24
+
25
+ const flat = (obj, prefix) => Object.entries(obj || {}).flatMap(([k, v]) =>
26
+ v && typeof v === "object" ? flat(v, `${prefix}.${k}`) : [`${prefix}.${k}`]);
27
+
28
+ const readTrans = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf8")).data.translations; } catch { return null; } };
29
+
30
+ const gen = readTrans(TRANS);
31
+ if (!gen) { bad(`cannot read ${TRANS}`); process.exit(1); }
32
+
33
+ // 1. groups present + non-empty in both languages
34
+ const genKeys = { pl: new Set(), en: new Set() };
35
+ for (const lang of ["pl", "en"]) {
36
+ for (const g of groups) {
37
+ const keys = flat(gen[lang]?.[g], g);
38
+ keys.forEach((k) => genKeys[lang].add(k));
39
+ if (keys.length) pass(`${g} present in ${lang} (${keys.length} keys)`);
40
+ else bad(`${g} missing or empty in ${lang}`);
41
+ }
42
+ }
43
+
44
+ // 2. dead references — literal GROUP.* keys used in sources must exist.
45
+ // Dynamic tails (`GROUP.${x}`) don't match the literal regex, and a matched
46
+ // PREFIX that is a parent of existing keys counts as present.
47
+ const walk = (dir) => {
48
+ let out = [];
49
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
51
+ const p = `${dir}/${e.name}`;
52
+ if (e.isDirectory()) out = out.concat(walk(p));
53
+ else if (/\.(ts|html)$/.test(e.name)) out.push(p);
54
+ }
55
+ return out;
56
+ };
57
+ let src = "";
58
+ for (const d of ["libs", "apps/web/src"]) {
59
+ if (fs.existsSync(d)) src += walk(d).map((f) => { try { return fs.readFileSync(f, "utf8"); } catch { return ""; } }).join("\n");
60
+ }
61
+ const re = new RegExp(`\\b(?:${groups.join("|")})(?:\\.[A-Za-z0-9_]+)+`, "g");
62
+ const used = new Set(src.match(re) || []);
63
+ const dead = { pl: [], en: [] };
64
+ for (const key of used) {
65
+ for (const lang of ["pl", "en"]) {
66
+ const ks = genKeys[lang];
67
+ if (!ks.has(key) && ![...ks].some((k) => k.startsWith(`${key}.`))) dead[lang].push(key);
68
+ }
69
+ }
70
+ for (const lang of ["pl", "en"]) {
71
+ if (dead[lang].length) bad(`${lang}: keys referenced in code but missing in ${TRANS}: ${dead[lang].slice(0, 8).join(", ")}${dead[lang].length > 8 ? ` (+${dead[lang].length - 8})` : ""}`);
72
+ else pass(`no dead ${lang} references (${used.size} literal keys checked)`);
73
+ }
74
+
75
+ // 3. completeness vs reference
76
+ if (refRoot) {
77
+ const ref = readTrans(`${refRoot}/apps/web/public/translations.json`);
78
+ if (!ref) bad(`cannot read reference translations under ${refRoot}`);
79
+ else {
80
+ for (const lang of ["pl", "en"]) {
81
+ const missing = groups.flatMap((g) => flat(ref[lang]?.[g], g)).filter((k) => !genKeys[lang].has(k));
82
+ if (missing.length) bad(`${lang}: ${missing.length} reference keys missing: ${missing.slice(0, 8).join(", ")}${missing.length > 8 ? " …" : ""}`);
83
+ else pass(`${lang}: all reference keys covered`);
84
+ }
85
+ }
86
+ } else {
87
+ console.log(" SKIP: no reference root given — completeness vs reference not checked");
88
+ }
89
+
90
+ process.exit(fail);
@@ -8,6 +8,7 @@ set -eu
8
8
 
9
9
  ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
10
10
  PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
12
  cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
12
13
 
13
14
  fail=0
@@ -46,6 +47,58 @@ echo "== Deps =="
46
47
  grep -q "@smartsoft001-mobilems/objects-domain" package.json && pass "objects-domain dep" || bad "objects-domain dep missing"
47
48
  grep -q "online-3d-viewer" package.json && pass "online-3d-viewer dep" || bad "online-3d-viewer dep missing"
48
49
 
50
+ echo "== Framework first (packages/shared/angular/README.md) =="
51
+ LIBDIR="libs/museum-objects/shell/angular/src/lib"
52
+ # 1. masonry belongs to the framework's MasonryGridComponent — no module-level instances
53
+ MAS="$(grep -rlnE "new Masonry\(|MasonryInstance" libs apps/web/src 2>/dev/null || true)"
54
+ if [ -n "$MAS" ]; then
55
+ bad "hand-rolled masonry — render grids with <smart-mobilems-masonry-grid> (MasonryGridComponent):"
56
+ printf ' %s\n' $MAS >&2
57
+ else
58
+ pass "no hand-rolled masonry instances"
59
+ fi
60
+ # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
61
+ # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
62
+ # which the `new Masonry(` grep above cannot see.
63
+ if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
64
+ if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
65
+ pass "grids render via <smart-mobilems-masonry-grid>"
66
+ else
67
+ bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
68
+ fi
69
+ fi
70
+ # 3. a list page must render through the framework grid
71
+ LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
72
+ if [ -n "$LISTP" ]; then
73
+ if grep -rq "smart-mobilems-masonry-grid" $LISTP 2>/dev/null; then
74
+ pass "list page renders via <smart-mobilems-masonry-grid>"
75
+ else
76
+ bad "list page does not render <smart-mobilems-masonry-grid> (README.md#masonrygridcomponent)"
77
+ fi
78
+ fi
79
+ # 4. filters must EXTEND the framework base class — `context.` alone proves nothing, and a
80
+ # differently-named dir must not silently skip the check
81
+ FILT="$(find "$LIBDIR/components" -maxdepth 1 -type d \( -name 'filters*' -o -name 'search*' -o -name 'facet*' \) 2>/dev/null || true)"
82
+ if [ -n "$FILT" ]; then
83
+ if grep -rqE "FiltersBaseComponent" $FILT 2>/dev/null; then
84
+ pass "filters use the framework filter API"
85
+ else
86
+ bad "filters do not extend FiltersBaseComponent — no ad-hoc query building:"
87
+ printf ' %s\n' $FILT >&2
88
+ fi
89
+ else
90
+ pass "no filters component in this module"
91
+ fi
92
+
93
+ echo "== Translations (REF_ROOT=${REF_ROOT:-unset}) =="
94
+ # Groups: present in pl+en, no dead references, complete vs the reference repo
95
+ # (export REF_ROOT=<reference-repo-root> to enable the completeness diff).
96
+ if node "$SCRIPT_DIR/check-translations.js" "OBJECTS DETAILS_PAGE GALLERY" "${REF_ROOT:-}"; then
97
+ pass "translation groups complete"
98
+ else
99
+ bad "translation gaps — fix the FAIL lines above"
100
+ fi
101
+
49
102
  echo
50
103
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
51
104
  exit "$fail"
@@ -23,7 +23,31 @@ REUSES the Articles shared module (like the thematic-paths module) but with its
23
23
  Run `scripts/detect-init.sh <workspace-root>`. It confirms the MobilEMS foundation is present
24
24
  (`SharedModule.forRoot` in `app.config.ts`, a populated shared-lib barrel). If not, tell the user to
25
25
  run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`; reference uses
26
- `@msr`) and the reference repo path (same one init used).
26
+ own) and the reference repo path (same one init used).
27
+
28
+ ## Framework first — MANDATORY
29
+
30
+ Read these before writing ANY component, service, pipe, directive or helper, and reuse what they
31
+ list — re-implementing framework code is a defect, not a shortcut:
32
+
33
+ 0. [**The Framework API index**](https://github.com/smartsoft001/mobilems-framework/blob/main/README.md#-framework-api) — the map of every package to
34
+ its catalog. Start here when you do not already know which package owns what.
35
+ 1. The framework API catalog — `packages/shared/angular/README.md` in this repo, installed as
36
+ `node_modules/@smartsoft001-mobilems/angular/README.md` in a museum app ([browse](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md)).
37
+ This is the path `scripts/verify-module.sh` names when it fails.
38
+ 2. [`packages/articles/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/articles/README.md) +
39
+ [`packages/objects/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/objects/README.md) — the packages this module reuses
40
+ 3. `libs/shared/angular/README.md` — the app's own shared lib (built by `mobilems-project-init`)
41
+
42
+ | Do not hand-roll | Use |
43
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
44
+ | a masonry grid (`new Masonry(...)`, per-page masonry JS/CSS) | `<smart-mobilems-masonry-grid>` (`MasonryGridComponent`) with a `#masonryContentTpl`; the `Masonry` provider only feeds it |
45
+ | filter state / hand-built `query` arrays | `FiltersBaseComponent<MuseumPartner>` + its `FiltersContext` accessors |
46
+ | list/detail plumbing (facade wiring, id-from-route) | the `ArticlesListComponent` / `ArticlesItemComponent` base classes |
47
+ | media URLs, meta/OG tags, filter dictionaries, modals, WCAG | `FileUrlService`, `SeoService`/`MetaService`, `DictionaryService`, `ModalService`, `WcagService` |
48
+
49
+ Anything the framework genuinely lacks goes into `libs/shared/angular` **once** (and into its
50
+ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on hand-rolled masonry.
27
51
 
28
52
  ## Steps
29
53
 
@@ -48,7 +72,11 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
48
72
  `partnerzy` route to the `appRoutes` array in `apps/web/src/app/app.routes.ts` as a top-level
49
73
  route, and a matching child under the `wyszukaj/:text` search route (partners appears in site
50
74
  search).
51
- 4. **Verify** — run `scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib + entity
75
+ 4. **Translations** — copy the module's translation groups (`PARTNERS`) from the reference
76
+ repo's `apps/web/public/translations.json` into the app's file, for BOTH `pl` and `en`.
77
+ Complete group = every reference key present; every key referenced in the module's code
78
+ must exist in both languages — a raw `GROUP.key` rendered in the UI is a defect.
79
+ 5. **Verify** — run `REF_ROOT=<reference-repo> scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib + entity
52
80
  exist, both tsconfig paths are added, `ArticlesSharedModule.forFeature` is wired with
53
81
  `entity: 'partners'` + `reducerFactory: getReducer`, the `+state/partners.reducer.ts` exports
54
82
  `getReducer`, the `components/{grid-item,list-item}` cards are present in the partners lib, the
@@ -66,6 +66,30 @@ import { SERVICES } from './services';
66
66
  export class MuseumPartnersModule {}
67
67
  ```
68
68
 
69
+ > **The `Masonry` provider exists only to feed the framework's `MasonryGridComponent`.** It is DI
70
+ > plumbing, not a grid — never call `new Masonry(...)`, keep a `MasonryInstance`, or re-implement the
71
+ > layout/`reloadItems` dance in a page component. A hand-rolled CSS-grid or column layout is the same
72
+ > defect. `scripts/verify-module.sh` fails the run on both.
73
+
74
+ The list page renders the grid like this — copy this shape, it is the required one:
75
+
76
+ ```html
77
+ <smart-mobilems-masonry-grid [list]="list()" [options]="{ gutter: 24 }">
78
+ <ng-template #masonryContentTpl>
79
+ <div class="masonry-grid-sizer"></div>
80
+ @for (item of list(); track item.id) {
81
+ <div class="masonry-item"><app-partners-grid-item [item]="item" /></div>
82
+ }
83
+ </ng-template>
84
+ </smart-mobilems-masonry-grid>
85
+ ```
86
+
87
+ `.masonry-grid-sizer` and `.masonry-item` are required — they are the component's default
88
+ `columnWidth` / `itemSelector`. The card (lib-contents.md) is the only module-specific part. Inputs,
89
+ public methods and the SSR provider are documented in the framework catalog,
90
+ [`packages/shared/angular/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md#masonrygridcomponent)
91
+ (installed as `node_modules/@smartsoft001-mobilems/angular/README.md`).
92
+
69
93
  > Contrast with `mobilems-objects` (`ObjectsSharedModule.forFeature` over `MsObject`, no
70
94
  > `reducerFactory`): here the module reuses `ArticlesSharedModule`, supplies `type: Article` with
71
95
  > `reducerFactory: getReducer`, and there is deliberately **no `baseQuery`** in the config.
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-translations.js "<GROUP GROUP …>" [reference-root]
4
+ *
5
+ * Run from the workspace root (verify-module.sh cd's there). Verifies the
6
+ * module's translation groups in apps/web/public/translations.json:
7
+ * 1. each group exists and is non-empty in BOTH pl and en,
8
+ * 2. every literal GROUP.* key referenced in libs/ or apps/web/src exists
9
+ * in both languages (dead references = defect),
10
+ * 3. with a reference root: every reference key of the group exists in the
11
+ * generated file — missing keys are listed (the module is incomplete).
12
+ * Prints PASS/FAIL lines in the verify-module.sh idiom; exit 1 on any FAIL.
13
+ */
14
+ const fs = require("fs");
15
+
16
+ const [groupsArg, refRoot] = process.argv.slice(2);
17
+ const groups = (groupsArg || "").split(/\s+/).filter(Boolean);
18
+ if (!groups.length) { console.log(" FAIL: no translation groups given"); process.exit(1); }
19
+
20
+ const TRANS = "apps/web/public/translations.json";
21
+ let fail = 0;
22
+ const pass = (m) => console.log(` PASS: ${m}`);
23
+ const bad = (m) => { console.log(` FAIL: ${m}`); fail = 1; };
24
+
25
+ const flat = (obj, prefix) => Object.entries(obj || {}).flatMap(([k, v]) =>
26
+ v && typeof v === "object" ? flat(v, `${prefix}.${k}`) : [`${prefix}.${k}`]);
27
+
28
+ const readTrans = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf8")).data.translations; } catch { return null; } };
29
+
30
+ const gen = readTrans(TRANS);
31
+ if (!gen) { bad(`cannot read ${TRANS}`); process.exit(1); }
32
+
33
+ // 1. groups present + non-empty in both languages
34
+ const genKeys = { pl: new Set(), en: new Set() };
35
+ for (const lang of ["pl", "en"]) {
36
+ for (const g of groups) {
37
+ const keys = flat(gen[lang]?.[g], g);
38
+ keys.forEach((k) => genKeys[lang].add(k));
39
+ if (keys.length) pass(`${g} present in ${lang} (${keys.length} keys)`);
40
+ else bad(`${g} missing or empty in ${lang}`);
41
+ }
42
+ }
43
+
44
+ // 2. dead references — literal GROUP.* keys used in sources must exist.
45
+ // Dynamic tails (`GROUP.${x}`) don't match the literal regex, and a matched
46
+ // PREFIX that is a parent of existing keys counts as present.
47
+ const walk = (dir) => {
48
+ let out = [];
49
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
51
+ const p = `${dir}/${e.name}`;
52
+ if (e.isDirectory()) out = out.concat(walk(p));
53
+ else if (/\.(ts|html)$/.test(e.name)) out.push(p);
54
+ }
55
+ return out;
56
+ };
57
+ let src = "";
58
+ for (const d of ["libs", "apps/web/src"]) {
59
+ if (fs.existsSync(d)) src += walk(d).map((f) => { try { return fs.readFileSync(f, "utf8"); } catch { return ""; } }).join("\n");
60
+ }
61
+ const re = new RegExp(`\\b(?:${groups.join("|")})(?:\\.[A-Za-z0-9_]+)+`, "g");
62
+ const used = new Set(src.match(re) || []);
63
+ const dead = { pl: [], en: [] };
64
+ for (const key of used) {
65
+ for (const lang of ["pl", "en"]) {
66
+ const ks = genKeys[lang];
67
+ if (!ks.has(key) && ![...ks].some((k) => k.startsWith(`${key}.`))) dead[lang].push(key);
68
+ }
69
+ }
70
+ for (const lang of ["pl", "en"]) {
71
+ if (dead[lang].length) bad(`${lang}: keys referenced in code but missing in ${TRANS}: ${dead[lang].slice(0, 8).join(", ")}${dead[lang].length > 8 ? ` (+${dead[lang].length - 8})` : ""}`);
72
+ else pass(`no dead ${lang} references (${used.size} literal keys checked)`);
73
+ }
74
+
75
+ // 3. completeness vs reference
76
+ if (refRoot) {
77
+ const ref = readTrans(`${refRoot}/apps/web/public/translations.json`);
78
+ if (!ref) bad(`cannot read reference translations under ${refRoot}`);
79
+ else {
80
+ for (const lang of ["pl", "en"]) {
81
+ const missing = groups.flatMap((g) => flat(ref[lang]?.[g], g)).filter((k) => !genKeys[lang].has(k));
82
+ if (missing.length) bad(`${lang}: ${missing.length} reference keys missing: ${missing.slice(0, 8).join(", ")}${missing.length > 8 ? " …" : ""}`);
83
+ else pass(`${lang}: all reference keys covered`);
84
+ }
85
+ }
86
+ } else {
87
+ console.log(" SKIP: no reference root given — completeness vs reference not checked");
88
+ }
89
+
90
+ process.exit(fail);
@@ -10,6 +10,7 @@ set -eu
10
10
 
11
11
  ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
12
12
  PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
14
  cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
14
15
 
15
16
  fail=0
@@ -54,6 +55,58 @@ grep -q "@smartsoft001-mobilems/articles-shell-angular" package.json && pass "ar
54
55
  grep -q "@smartsoft001-mobilems/articles-shell-dtos" package.json && pass "articles-shell-dtos dep" || bad "articles-shell-dtos dep missing"
55
56
  grep -q "@smartsoft001-mobilems/objects-domain" package.json && pass "objects-domain dep" || bad "objects-domain dep missing"
56
57
 
58
+ echo "== Framework first (packages/shared/angular/README.md) =="
59
+ LIBDIR="libs/museum-partners/shell/angular/src/lib"
60
+ # 1. masonry belongs to the framework's MasonryGridComponent — no module-level instances
61
+ MAS="$(grep -rlnE "new Masonry\(|MasonryInstance" libs apps/web/src 2>/dev/null || true)"
62
+ if [ -n "$MAS" ]; then
63
+ bad "hand-rolled masonry — render grids with <smart-mobilems-masonry-grid> (MasonryGridComponent):"
64
+ printf ' %s\n' $MAS >&2
65
+ else
66
+ pass "no hand-rolled masonry instances"
67
+ fi
68
+ # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
69
+ # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
70
+ # which the `new Masonry(` grep above cannot see.
71
+ if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
72
+ if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
73
+ pass "grids render via <smart-mobilems-masonry-grid>"
74
+ else
75
+ bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
76
+ fi
77
+ fi
78
+ # 3. a list page must render through the framework grid
79
+ LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
80
+ if [ -n "$LISTP" ]; then
81
+ if grep -rq "smart-mobilems-masonry-grid" $LISTP 2>/dev/null; then
82
+ pass "list page renders via <smart-mobilems-masonry-grid>"
83
+ else
84
+ bad "list page does not render <smart-mobilems-masonry-grid> (README.md#masonrygridcomponent)"
85
+ fi
86
+ fi
87
+ # 4. filters must EXTEND the framework base class — `context.` alone proves nothing, and a
88
+ # differently-named dir must not silently skip the check
89
+ FILT="$(find "$LIBDIR/components" -maxdepth 1 -type d \( -name 'filters*' -o -name 'search*' -o -name 'facet*' \) 2>/dev/null || true)"
90
+ if [ -n "$FILT" ]; then
91
+ if grep -rqE "FiltersBaseComponent" $FILT 2>/dev/null; then
92
+ pass "filters use the framework filter API"
93
+ else
94
+ bad "filters do not extend FiltersBaseComponent — no ad-hoc query building:"
95
+ printf ' %s\n' $FILT >&2
96
+ fi
97
+ else
98
+ pass "no filters component in this module"
99
+ fi
100
+
101
+ echo "== Translations (REF_ROOT=${REF_ROOT:-unset}) =="
102
+ # Groups: present in pl+en, no dead references, complete vs the reference repo
103
+ # (export REF_ROOT=<reference-repo-root> to enable the completeness diff).
104
+ if node "$SCRIPT_DIR/check-translations.js" "PARTNERS" "${REF_ROOT:-}"; then
105
+ pass "translation groups complete"
106
+ else
107
+ bad "translation gaps — fix the FAIL lines above"
108
+ fi
109
+
57
110
  echo
58
111
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
59
112
  exit "$fail"
@@ -29,6 +29,42 @@ and `mobilems-thematic-publications`, each of which adds its own library, shared
29
29
  package, and routes. Keeping feature code out of the foundation is the layering rule that lets the
30
30
  foundation build standalone (its shared library imports no feature package).
31
31
 
32
+ ## Framework first — MANDATORY
33
+
34
+ The museum app builds **on** the framework, not beside it. Before writing or copying any component,
35
+ service, pipe, directive or helper, read:
36
+
37
+ 0. [**The Framework API index**](https://github.com/smartsoft001/mobilems-framework/blob/main/README.md#-framework-api) —
38
+ the map of every package to its catalog. Start here when you do not already know which package
39
+ owns what.
40
+ 1. The framework API catalog — `packages/shared/angular/README.md` in this repo, installed as
41
+ `node_modules/@smartsoft001-mobilems/angular/README.md` in a museum app
42
+ ([browse](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md)).
43
+ This is the path `scripts/verify-init.sh` names when it fails.
44
+ 2. The [models catalog](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/models/README.md).
45
+
46
+ Reuse what they list. Re-implementing framework code is a defect, not a shortcut.
47
+
48
+ This applies with force in **reference mode**: the reference repo predates parts of the framework, so
49
+ some of its shared components duplicate framework exports. When a reference file re-implements
50
+ something the catalog already provides, **use the framework export instead of copying the file**:
51
+
52
+ | Reference pattern to drop | Framework export to use |
53
+ | ---------------------------------------------------------------------------- | ----------------------------------------------------------- |
54
+ | a hand-rolled masonry grid (`new Masonry(...)`, per-page `.masonry-item` JS) | `MasonryGridComponent` (`<smart-mobilems-masonry-grid>`) |
55
+ | bespoke filter state / hand-built `query` arrays | `FiltersBaseComponent<T>` + `FiltersContext<T>` |
56
+ | an ad-hoc media/thumbnail URL builder | `FileUrlService.get(...)` (`'cache' \| 'big' \| 'noCache'`) |
57
+ | local meta/OG tag writers | `SeoService` / `MetaService` |
58
+ | a local dictionary fetcher for filters | `DictionaryService` (lowercase keys) |
59
+ | a local WCAG/contrast state store | `WcagService` / `ContrastService` / `StyleService` |
60
+ | a bespoke modal host | `ModalService.open()` + `ModalRef` |
61
+ | a static-page loader | `PageStaticComponent` base class |
62
+
63
+ Everything the framework does **not** provide goes into the app's shared lib
64
+ (`libs/shared/angular`) exactly once — pipes, tools, directives, interceptors (see
65
+ `references/shared-lib.md`) — and every module reuses it from there. If you find a shared-lib item
66
+ that clearly belongs in the framework, name it in the final report so it can be upstreamed.
67
+
32
68
  ## Steps
33
69
 
34
70
  ### 0. Precondition — confirm a scaffold-nx-workspace base
@@ -42,7 +78,8 @@ run `/smart:scaffold-nx-workspace` first — do not try to create the Nx workspa
42
78
 
43
79
  The base already derived/confirmed/threaded the import-alias prefix. Read it from
44
80
  `tsconfig.base.json` `paths` — the key `@<PREFIX>/angular`. Hold `<PREFIX>`; thread it into every
45
- copied file (the reference repo uses `@msr`, so replacing `@msr` → `@<PREFIX>` is the main rewrite).
81
+ copied file (replacing the reference repo's own alias with `@<PREFIX>` is the main rewrite; read it
82
+ from that repo's `tsconfig.base.json`).
46
83
 
47
84
  ### 2. Choose mode
48
85
 
@@ -100,11 +137,33 @@ the static pages (`home`, `page`, `contact`, `search`, `not-found`) + their reso
100
137
  catch-all). Each module skill adds its own feature route to the `appRoutes` array when it runs
101
138
  (`article-preview` is articles-coupled — `mobilems-articles` adds it, not the foundation).
102
139
 
103
- ### 9. QA hardening — SEO, i18n, WCAG, branding
140
+ ### 9. Branding + SEO reset — strip the reference museum's identity
141
+
142
+ **Mandatory in BOTH modes.** Step 4 already wrote the neutral token set, so a foundation-only app is
143
+ brand-clean by construction and this step only applies the client's spec if there is one. In
144
+ reference mode it does the real work: everything copied so far carries the _source project's_ brand
145
+ — its fonts, palette, logos and social image, its SEO/meta strings and its domain. Apply
146
+ `references/branding-reset.md` before QA hardening — it enumerates every carrier (styles, assets,
147
+ SEO, translations, configs) and how to reset it.
148
+
149
+ Ask the user (`AskUserQuestion`) once, up front:
150
+
151
+ - **Brand spec available?** → apply it (fonts, palette, logos, social image, site name/domain).
152
+ - **No spec (default)** → apply the **neutral token set**: full-latin-ext fonts (Playfair Display for
153
+ headings, Inter for body), a neutral grey/accent palette with every text pair ≥4.5:1, placeholder
154
+ logo + `no-image` + social image, generic site name/description keys.
155
+
156
+ Either way this is a **token-only** change, and there are exactly two files that carry the tokens:
157
+ `tailwind.config.js` (`theme.extend.colors` + `theme.extend.fontFamily`) and
158
+ `apps/web/src/styles.scss` (`@font-face` + the metric-override fallbacks). There is no CSS-custom-property
159
+ theme layer in this stack — do not go looking for one. Never touch layout classes, DOM structure,
160
+ component inputs, API field mapping, dictionary keys or route paths. The app must look re-skinned,
161
+ not re-built.
162
+
163
+ ### 10. QA hardening — SEO, i18n, WCAG, branding
104
164
 
105
- Apply `references/qa-hardening.md` — a checklist distilled from real QA bug reports on live museum
106
- deployments. The copied foundation carries the _reference museum's_ identity, so this step removes
107
- leftovers and closes the recurring gaps QA always finds:
165
+ Apply `references/qa-hardening.md`. The copied foundation carries the _source project's_ identity,
166
+ so this step removes leftovers and closes the recurring SEO/i18n/WCAG gaps:
108
167
 
109
168
  - dynamic `/sitemap.xml` + `/robots.txt` SSR endpoints driven by `SITE_URL` (never static copies —
110
169
  they shipped another museum's domain twice),
@@ -116,12 +175,12 @@ leftovers and closes the recurring gaps QA always finds:
116
175
  - warning-clean build & console (NG8107/NG8112/NG8113/NG0100/NG0913, Swiper loop, duplicate API
117
176
  calls).
118
177
 
119
- ### 10. .claude extension
178
+ ### 11. .claude extension
120
179
 
121
180
  Apply `references/claude-config.md`: add `flow@mobilems` to `enabledPlugins`, grow the
122
181
  MobilEMS-specific permissions / MCP allow-lists, and seed `tools/specs` + `tools/prompts` pointers.
123
182
 
124
- ### 11. Verify
183
+ ### 12. Verify
125
184
 
126
185
  Run `scripts/verify-init.sh <workspace-root> <PREFIX>`. It asserts the delta landed (Tailwind,
127
186
  multimediaUrl, MobilEMS providers, populated shared-lib barrel), the QA-hardening basics (sitemap +
@@ -135,7 +194,8 @@ robots endpoints, favicon, non-empty heading font), AND the foundation is self-c
135
194
  - **Reference mode (default):** copies the shared lib + pages from a real MobilEMS repo and threads
136
195
  the prefix. This is the only mode that regenerates the full foundation, because the shared lib is
137
196
  not in npm.
138
- - **Foundation-only (fallback):** do steps 1, 3, 4, 5, 9 (deps, Tailwind, env, .claude) and scaffold a
197
+ - **Foundation-only (fallback):** do steps 1, 3, 4, 5, 9 and 11 (prefix, deps, Tailwind, env,
198
+ branding reset, .claude) and scaffold a
139
199
  reduced `app.config.ts` that does NOT import local shared-lib code (omit the interceptors + the
140
200
  `App` shell that need it). Leave a `MOBILEMS-FOUNDATION-TODO.md` listing what reference mode would
141
201
  have copied (shared lib, pages, full app.config, app shell) so the user can supply a reference