@starci/hfs 4.7.12 → 4.7.14

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 (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/release/gate.mjs +388 -378
  5. package/release/lint-report.mjs +52 -0
  6. package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
  7. package/runtime/knowledge/hfs/ci-standard.yaml +1 -1
  8. package/runtime/knowledge/hfs/rules.yaml +7 -6
  9. package/runtime/knowledge/patterns/be/cli.yaml +1 -0
  10. package/runtime/knowledge/patterns/be/realtime.yaml +1 -0
  11. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -0
  12. package/runtime/modules/kernel/failure-codes.yaml +24 -2
  13. package/runtime/packages/cli/src/catalog.generated.mjs +12 -6
  14. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  15. package/runtime/scripts/hfs/architecture/hfs.mjs +19 -4
  16. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +6 -4
  17. package/runtime/scripts/hfs/architecture/transport-owner.mjs +1 -1
  18. package/runtime/scripts/hfs/ci-declaration.mjs +5 -2
  19. package/runtime/scripts/hfs/db-baseline-verb.mjs +5 -2
  20. package/runtime/scripts/hfs/doc-commands.mjs +2 -1
  21. package/runtime/scripts/hfs/rule-catalog.mjs +5 -2
  22. package/runtime/scripts/hfs/rules/coverage-below.mjs +5 -3
  23. package/runtime/scripts/hfs/rules/database-access.mjs +1 -1
  24. package/runtime/scripts/hfs/rules/database-migrations.mjs +13 -3
  25. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +1 -1
  26. package/runtime/scripts/hfs/rules/database-queues.mjs +2 -2
  27. package/runtime/scripts/hfs/rules/deploy-scan.mjs +50 -6
  28. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +11 -1
  29. package/runtime/scripts/hfs/rules/doc-fresh.mjs +18 -3
  30. package/runtime/scripts/hfs/rules/docker-health.mjs +4 -1
  31. package/runtime/scripts/hfs/rules/env-parity.mjs +8 -7
  32. package/runtime/scripts/hfs/rules/env-value-shape.mjs +2 -4
  33. package/runtime/scripts/hfs/rules/fe-build-args.mjs +1 -1
  34. package/runtime/scripts/hfs/rules/release-provenance.mjs +8 -5
  35. package/runtime/scripts/hfs/rules/script-hygiene.mjs +23 -7
  36. package/runtime/scripts/hfs/rules/services.mjs +19 -5
  37. package/runtime/scripts/hfs/rules/stack-tooling.mjs +5 -2
  38. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +5 -5
  39. package/runtime/scripts/hfs/rules/staged-specs.mjs +26 -0
  40. package/runtime/scripts/hfs/rules/supply-chain.mjs +3 -3
  41. package/runtime/scripts/hfs/rules/unused-symbol-properties.mjs +1 -1
  42. package/runtime/scripts/hfs/rules/unused-symbol.mjs +8 -4
  43. package/runtime/scripts/hfs/sealed-members.mjs +1 -1
  44. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +1 -1
  45. package/runtime/scripts/hfs/secret-files.mjs +1 -1
  46. package/runtime/scripts/hfs/secret-sops.mjs +1 -1
  47. package/runtime/scripts/hfs/stack-deploy-config.mjs +19 -2
  48. package/runtime/scripts/lib/env.mjs +2 -1
  49. package/runtime/scripts/lib/git-hooks-dir.mjs +30 -0
  50. package/runtime/scripts/lib/path-key.mjs +1 -1
  51. package/runtime/scripts/lib/roots.mjs +3 -3
  52. package/runtime/scripts/lib/secret-patterns.mjs +1 -1
  53. package/runtime/scripts/lib/swarm-stack.mjs +4 -2
  54. package/src/main.mjs +2 -2
  55. package/sync/ci-commands.mjs +275 -275
  56. package/sync/hygiene.mjs +19 -4
  57. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -0
@@ -176,7 +176,10 @@ function configs(c, block, probe) {
176
176
  out.push({ name: entry.name, file: entry.file, env: entry.env });
177
177
  });
178
178
  const seen = new Set();
179
- for (const entry of out) { if (seen.has(entry.name)) c.problem('deploy.configs', `config name ${entry.name} is declared twice`); seen.add(entry.name); }
179
+ for (const entry of out) {
180
+ if (seen.has(entry.name)) c.problem('deploy.configs', `config name ${entry.name} is declared twice`);
181
+ seen.add(entry.name);
182
+ }
180
183
  return out;
181
184
  }
182
185
 
@@ -242,7 +245,7 @@ function tasks(c, block, services) {
242
245
  return out;
243
246
  }
244
247
 
245
- const REGISTRY_HOST = /^[a-z0-9][a-z0-9.-]*(?::[0-9]{1,5})?$/u;
248
+ const REGISTRY_HOST = /^[a-z0-9][a-z0-9.-]*(?::\d{1,5})?$/u;
246
249
  const VISIBILITY = ['public', 'private'];
247
250
 
248
251
  /** The image registry: host (default ghcr.io) and the visibility the app's packages are meant to have (default public, the canon scaffold writes it and the app owns it afterwards). */
@@ -311,3 +314,17 @@ export function normalizeDeploy(block, { project, probe }) {
311
314
  if (block.stack === undefined && !NAME.test(String(project))) c.problem('deploy.stack', `the stack name ${project} must match ${NAME}`);
312
315
  return { config: c.problems.length === 0 ? config : null, problems: c.problems };
313
316
  }
317
+
318
+ /**
319
+ * The component-to-app ties a stack declaration states: every `environments.<env>.deploy.images.<service>` entry with a string
320
+ * `app`, as `[{ environment, service, app }]`. Read without validating the rest of the block (R294 judges that), so a defect
321
+ * elsewhere in a deploy block never hides the tie R165 reads.
322
+ */
323
+ export function deployImageApps(doc) {
324
+ const ties = [];
325
+ for (const [environment, spec] of Object.entries(plain(doc?.environments) ? doc.environments : {})) {
326
+ const block = plain(spec) && plain(spec.deploy) && plain(spec.deploy.images) ? spec.deploy.images : {};
327
+ for (const [service, entry] of Object.entries(block)) if (plain(entry) && typeof entry.app === 'string') ties.push({ environment, service, app: entry.app });
328
+ }
329
+ return ties;
330
+ }
@@ -15,6 +15,7 @@ export const isSpecRun = (env = process.env) => Boolean(env.NODE_TEST_CONTEXT);
15
15
  * would pass red specs from inside a test process. The runner marks the spec files it starts itself, so isSpecRun stays true inside them.
16
16
  */
17
17
  export function withoutTestRunner(env = process.env) {
18
- const { NODE_TEST_CONTEXT: _enclosing, ...rest } = env;
18
+ const rest = { ...env };
19
+ delete rest.NODE_TEST_CONTEXT;
19
20
  return rest;
20
21
  }
@@ -0,0 +1,30 @@
1
+ // git-hooks-dir.mjs - whether a checkout's hooks folder holds installed hooks. The one judgement read by `starci npm ci` and
2
+ // `starci npm install` (scripts/machine/npm-hooks.mjs, which runs the app's hook installer when it fails) and by the HFS check
3
+ // HFS_HOOKS_NOT_INSTALLED (scripts/hfs/architecture/hfs.mjs). husky's `.husky/_` is generated per checkout: a linked worktree or
4
+ // an install whose lifecycle scripts were ignored has none, or only the runtime's own wrapped hooks, so git runs no pre-commit gate.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ /** husky's generated hooks folder; every hook file in it sources the dispatcher `h`, which runs the tracked `.husky/<hook>`. */
9
+ const HUSKY_GENERATED = /^\.husky\/_\/?$/u;
10
+ const HUSKY_DISPATCHER = 'h';
11
+
12
+ /**
13
+ * The state of the hooks folder `hooksPath` (a core.hooksPath value, relative to the checkout `root` or absolute):
14
+ * `{ state, dir }` with state `installed`, `missing` (no such folder), `empty` (no hook file; for husky's `.husky/_`, no
15
+ * dispatcher `h`) or `unreadable` (with `error`).
16
+ */
17
+ export function hooksDirState(root, hooksPath) {
18
+ const value = String(hooksPath ?? '').trim().replaceAll('\\', '/');
19
+ const dir = path.resolve(root, value);
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(dir, { withFileTypes: true });
23
+ } catch (error) {
24
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return { state: 'missing', dir };
25
+ return { state: 'unreadable', dir, error: String(error?.message ?? error) };
26
+ }
27
+ const files = entries.filter((entry) => entry.isFile() && entry.name !== '.gitignore').map((entry) => entry.name);
28
+ const installed = HUSKY_GENERATED.test(value) ? files.includes(HUSKY_DISPATCHER) : files.length > 0;
29
+ return { state: installed ? 'installed' : 'empty', dir };
30
+ }
@@ -44,7 +44,7 @@ export const pathsOverlap = (a, b) => sameOrUnder(a, b) || sameOrUnder(b, a);
44
44
  * @param {{trim?: boolean, collapse?: boolean, dot?: boolean, glob?: string|null, fold?: boolean}} [options]
45
45
  */
46
46
  export const normPath = (p, { trim = false, collapse = false, dot = false, glob = null, fold = false } = {}) => {
47
- let s = String(p ?? '');
47
+ let s = p == null ? '' : String(p);
48
48
  if (trim) s = s.trim();
49
49
  s = slash(s);
50
50
  if (collapse) s = s.replace(/\/{2,}/g, '/');
@@ -17,10 +17,10 @@
17
17
  // directory or from the Work directory name outside this module.
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
- import { skillRoot, starciLocalRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
21
- import { tempRoot } from '../../engine/temp-root.mjs';
20
+ import { skillRoot } from '../../engine/runtime-root.mjs';
22
21
 
23
- export { tempRoot, starciLocalRoot, starciSourceRoot };
22
+ export { tempRoot } from '../../engine/temp-root.mjs';
23
+ export { starciLocalRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
24
24
 
25
25
  /** The runtime tree this code runs from. */
26
26
  const runtimeTree = skillRoot;
@@ -161,7 +161,7 @@ function readmeBadgeText(text, manifestText) {
161
161
  let url;
162
162
  try { url = new URL(address); } catch { return image; }
163
163
  if (!hosts.has(url.host) || url.username || url.password) return image;
164
- const badgePath = url.host === 'codecov.io' ? /\/graph\/badge\.svg$/.test(url.pathname) : url.pathname === '/api/project_badges/measure';
164
+ const badgePath = url.host === 'codecov.io' ? url.pathname.endsWith('/graph/badge.svg') : url.pathname === '/api/project_badges/measure';
165
165
  if (!badgePath) return image;
166
166
  const fragment = address.indexOf('#');
167
167
  const queryEnd = fragment < 0 ? address.length : fragment;
@@ -4,8 +4,10 @@ import { composeEntries } from './compose-values.mjs';
4
4
  import { repositoryOf } from './stack-services.mjs';
5
5
 
6
6
  const PINNED_IMAGE = /@sha256:[a-f0-9]{64}$/i;
7
- const VERSIONED_SECRET = /-v[1-9][0-9]*$/;
8
- const BACKING_IMAGE = /^(?:postgres(?:ql)?|postgresql-repmgr|redis(?:-stack(?:-server)?)?|(?:cp-)?kafka(?:-native)?|keycloak|minio|rabbitmq|mysql(?:-server)?|mongo(?:db)?|mongodb-community-server)$/i;
7
+ const VERSIONED_SECRET = /-v[1-9]\d*$/;
8
+ const BACKING_IMAGE_SQL_AND_CACHE = 'postgres(?:ql)?|postgresql-repmgr|redis(?:-stack(?:-server)?)?|mysql(?:-server)?|mongo(?:db)?|mongodb-community-server';
9
+ const BACKING_IMAGE_BROKERS_AND_OTHERS = '(?:cp-)?kafka(?:-native)?|keycloak|minio|rabbitmq';
10
+ const BACKING_IMAGE = new RegExp(`^(?:${BACKING_IMAGE_SQL_AND_CACHE}|${BACKING_IMAGE_BROKERS_AND_OTHERS})$`, 'i');
9
11
  const mapping = value => plain(value) ? value : {};
10
12
  const present = value => typeof value === 'string' && Boolean(value.trim());
11
13
  const configured = value => Object.keys(mapping(value)).length > 0;
package/src/main.mjs CHANGED
@@ -28,7 +28,7 @@
28
28
  // printSchema(lexicographicSortSchema) of the resolvers the app root composes; no env, no database, no network.
29
29
  // starci app sync (--check | --write) [--cwd <dir>] the managed files of the root (scripts, husky, CI, .gitignore block, sonar, prettier) and
30
30
  // of each side (tsconfig, eslint, jest, stylelint); sync/cli.mjs
31
- // starci app hygiene the pre-commit guard: staged .starciwork and .starcistacks paths, and the secrets guard over every staged file (read from the index); sync/cli.mjs
31
+ // starci app hygiene the pre-commit guard: staged .starciwork and .starcistacks paths, the secrets guard over every staged file (read from the index) and a new back-end logic file staged without its unit spec; sync/cli.mjs
32
32
  // starci app coverage merge --out <lcov> <lcov-or-dir>... the lcov reports of the sharded unit run merged into one (coverage/cli.mjs; the managed CI coverage job)
33
33
  // starci app new service <dir> <name> [--inject <Decorator>=<module>:<Type> | <Class>=<module>]... [--cwd <dir>]
34
34
  // a back-end `<name>.service.ts` and its `<name>.service.spec.ts` skeleton (scaffold/service.mjs; <dir> is
@@ -390,7 +390,7 @@ async function scaffoldMain(opts, { cwd, edition = 'full', presets, lock, stdout
390
390
  const [name, ...extra] = opts.positional;
391
391
  if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private]`');
392
392
  const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), edition, presets: presets ?? await scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
393
- out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci, then starci app lint\n`);
393
+ out(`starci app scaffold: created ${created} (${files.length} files); next: starci npm ci (the lockfile, then the git hooks through npm run prepare; after a raw npm ci --ignore-scripts run npm run prepare), then starci app lint\n`);
394
394
  return 0;
395
395
  }
396
396