@starci/eslint-canon-fe 9.13.5 → 9.13.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.13.7 - 2026-11-04
4
+
5
+ - Changed: the runtime copy of the canon pins names `@starci/hfs` 5.8.1.
6
+
7
+ ## 9.13.6 - 2026-11-04
8
+
9
+ - Changed: the runtime copies carry the module shape of `be.integrations.passport` in the package slot manifest and the canon pins of `@starci/hfs` 5.8.0.
10
+
3
11
  ## 9.13.5 - 2026-11-04
4
12
 
5
13
  - Changed: the runtime copy of the canon pins names `@starci/hfs` 5.7.1 and the release-7a versions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "9.13.5",
3
+ "version": "9.13.7",
4
4
  "description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@ pins:
38
38
  source: packages/next-data/package.json
39
39
  why: 'the SWR data helpers of every front end (fe-stack data, owner:next-data-package): pollingRestart, the viewer and tenant key helpers, viewerSwrConfig and the viewer cache; each operation hook stays in the app and the package holds no component.'
40
40
  '@starci/grammar':
41
- version: 0.21.22
41
+ version: 0.21.24
42
42
  group: starci
43
43
  install: registry
44
44
  side: fe
@@ -52,28 +52,28 @@ pins:
52
52
  source: packages/next-i18n/package.json
53
53
  why: 'the next-intl stack of every front end (fe-stack i18n, owner:next-i18n-package): defineI18n over the locales of the app, the locale-negotiating proxy, the locale-aware navigation, the request config that loads the catalogs of the app and the locale read of the [locale] layout; the app keeps its message catalogs and writes none of the stack.'
54
54
  '@starci/eslint-canon-be':
55
- version: 3.15.5
55
+ version: 3.16.1
56
56
  group: starci
57
57
  install: registry
58
58
  side: be
59
59
  source: packages/eslint-canon-be/package.json
60
60
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
61
61
  '@starci/eslint-canon-fe':
62
- version: 9.13.5
62
+ version: 9.13.7
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
66
66
  source: packages/eslint-canon-fe/package.json
67
67
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
68
68
  '@starci/eslint-grammar-core':
69
- version: 0.21.22
69
+ version: 0.21.24
70
70
  group: starci
71
71
  install: registry
72
72
  side: fe
73
73
  source: packages/eslint-grammar-core/package.json
74
74
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
75
75
  '@starci/eslint-grammar-offset-pop':
76
- version: 0.21.22
76
+ version: 0.21.24
77
77
  group: starci
78
78
  install: registry
79
79
  side: fe
@@ -155,7 +155,7 @@ pins:
155
155
  source: packages/mail/package.json
156
156
  why: 'outgoing mail of a back end: a NestJS module and the Mailer port over SMTP (connect, greeting, socket and total timeouts, STARTTLS required by default, recipient, message and attachment checks, a shared SMTP target policy, coded errors, typed escaping render helpers returning { subject, text, html }), with @starci/mail/testing for its specs; the only importer of nodemailer (R90).'
157
157
  '@starci/hfs':
158
- version: 5.7.1
158
+ version: 5.8.1
159
159
  group: starci
160
160
  install: registry
161
161
  side: both
@@ -2044,8 +2044,8 @@ slots:
2044
2044
  tracked: tracked
2045
2045
  tier: integrations
2046
2046
  owner: true
2047
- requires: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.registration.ts] # Secrets, no defaults (R44); options and named injectors (R83); the kit and the module over @starci/auth
2048
- allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.registration.ts, errors/]
2047
+ requires: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.module-definition.ts, passport.module.ts] # Secrets, no defaults (R44); options and named injectors (R83); the kit and the module over @starci/auth
2048
+ allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.module-definition.ts, passport.module.ts, errors/]
2049
2049
  logic: [passport.kit.ts] # the verifier mapping over the package: measured at 100 per file
2050
2050
  tests: unit-beside
2051
2051
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INFRA_OWNER, BE_AUTH_PASSPORT_OWNER, BE_AUTH_JWT_OWN_JWKS, BE_AUTH_REFRESH_COOKIE]
@@ -5,6 +5,13 @@ import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs
5
5
  import { byCodeUnit } from '../lib/list.mjs';
6
6
  /** Shape problems of the optional hfs.json `db` block of a lite app: the environment variable naming its database, its image, forced RLS and its application role. */
7
7
  const isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
8
+ /** One owner ruling on a migration's destructive DDL: the exact file, the exact objects, the reason and the date the owner ruled it. */
9
+ const isDestructiveRuling = (ruling) => isPlainObject(ruling)
10
+ && typeof ruling.migration === 'string' && /^db\/migrations\/\d{14}_[a-z0-9_-]+\.sql$/.test(ruling.migration)
11
+ && isStringList(ruling.objects) && ruling.objects.length > 0
12
+ && typeof ruling.reason === 'string' && ruling.reason.trim() !== ''
13
+ && typeof ruling.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(ruling.date)
14
+ && Object.keys(ruling).every((key) => ['migration', 'objects', 'reason', 'date'].includes(key));
8
15
  function dbBlockProblems(block) {
9
16
  if (block === undefined)
10
17
  return [];
@@ -12,7 +19,7 @@ function dbBlockProblems(block) {
12
19
  return ['db must be an object'];
13
20
  const bad = [];
14
21
  for (const key of Object.keys(block))
15
- if (!['urlEnv', 'image', 'forceRls', 'forceRlsExemptions', 'appRole'].includes(key))
22
+ if (!['urlEnv', 'image', 'forceRls', 'forceRlsExemptions', 'appRole', 'destructiveRulings'].includes(key))
16
23
  bad.push(`db has unknown key ${key}`);
17
24
  if (block.urlEnv !== undefined && (typeof block.urlEnv !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(block.urlEnv)))
18
25
  bad.push('db.urlEnv must name an UPPER_CASE environment variable');
@@ -24,6 +31,8 @@ function dbBlockProblems(block) {
24
31
  bad.push('db.appRole must name a role');
25
32
  if (block.forceRlsExemptions !== undefined && !Array.isArray(block.forceRlsExemptions))
26
33
  bad.push('db.forceRlsExemptions must be a list of { table, reason }');
34
+ if (block.destructiveRulings !== undefined && !(Array.isArray(block.destructiveRulings) && block.destructiveRulings.every(isDestructiveRuling)))
35
+ bad.push('db.destructiveRulings must be a list of { migration, objects, reason, date } naming one db/migrations file, the objects it destroys, why and the date the owner ruled it (YYYY-MM-DD)');
27
36
  return bad;
28
37
  }
29
38
  function appShapeProblems(apps, at, side, names) {
@@ -11,6 +11,8 @@ export const DB_ARCHIVE_MUTATED = 'DB_ARCHIVE_MUTATED';
11
11
  export const DB_PGMQ_DECLARED = 'DB_PGMQ_DECLARED';
12
12
  export const DB_EXTENSION_ALLOWED = 'DB_EXTENSION_ALLOWED';
13
13
  export const DB_MIGRATION_DESTRUCTIVE = 'DB_MIGRATION_DESTRUCTIVE';
14
+ /** An owner ruling (hfs.json db.destructiveRulings) whose destroyed object is still referenced, or that names an object or migration it cannot prove. */
15
+ export const DB_DESTRUCTIVE_RULING_UNPROVEN = 'DB_DESTRUCTIVE_RULING_UNPROVEN';
14
16
  export const HFS_SEED_PII = 'HFS_SEED_PII';
15
17
  export const DB_VAULT_WRITE = 'DB_VAULT_WRITE';
16
18
 
@@ -7,8 +7,8 @@ import { tag } from '../../api/git/tag.mjs';
7
7
  import { withoutGitLocalEnv } from '../../lib/git.mjs';
8
8
  import { eachInOrder, findInOrder } from '../../lib/in-order.mjs';
9
9
  import { sameText } from '../../lib/same-text.mjs';
10
- import { found, readText } from './read.mjs';
11
- import { DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
10
+ import { found, readJson, readText } from './read.mjs';
11
+ import { DB_DESTRUCTIVE_RULING_UNPROVEN, DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
12
12
  import path from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { loadTypescript } from '../../lib/package-at.mjs';
@@ -579,18 +579,42 @@ function migrationDestructiveObjects(text) {
579
579
  return destructiveObjects(statement).filter((object) => !alterOfNewColumn(object, added));
580
580
  });
581
581
  }
582
- function destructiveFileFindings(file, text, references) {
583
- const contract = /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/.test(file);
582
+ const CONTRACT_MIGRATION = /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/;
583
+ /**
584
+ * The owner rulings of hfs.json `db.destructiveRulings`, by migration file: the exact objects each may destroy outside a contract migration,
585
+ * held only while the same proof a contract migration gets finds them unreferenced. The declaration shape judges reason and date.
586
+ */
587
+ function destructiveRulings(repoRoot) {
588
+ const listed = readJson(repoRoot, 'hfs.json')?.db?.destructiveRulings;
589
+ const rulings = new Map();
590
+ for (const ruling of Array.isArray(listed) ? listed : []) {
591
+ if (typeof ruling?.migration === 'string' && Array.isArray(ruling.objects))
592
+ rulings.set(ruling.migration, new Set(ruling.objects.filter((name) => typeof name === 'string')));
593
+ }
594
+ return rulings;
595
+ }
596
+ /** A ruled object is accepted while no be/src source references it; a reference makes the ruling unproven. */
597
+ function ruledObjectFinding(file, object, references) {
598
+ const reference = references.find(({ scopes }) => referencesObject(scopes, object));
599
+ return reference ? found(DB_DESTRUCTIVE_RULING_UNPROVEN, file, `${file}:${object.line} destroys ${object.name} under hfs.json db.destructiveRulings, but ${reference.file} still references it; the ruling holds only while its targets are unreferenced`, { line: object.line, object: object.name, reference: reference.file }) : null;
600
+ }
601
+ function destructiveFileFindings(file, text, references, ruled = new Set()) {
602
+ const contract = CONTRACT_MIGRATION.test(file);
584
603
  const seen = new Set();
585
604
  const objects = migrationDestructiveObjects(text);
586
- return objects.flatMap((object) => {
605
+ const findings = objects.flatMap((object) => {
587
606
  const key = `${object.line}:${object.kind}:${object.name}`;
588
607
  if (seen.has(key))
589
608
  return [];
590
609
  seen.add(key);
591
- const finding = destructiveObjectFinding(file, object, contract, references);
610
+ const finding = !contract && ruled.has(object.name) ? ruledObjectFinding(file, object, references) : destructiveObjectFinding(file, object, contract, references);
592
611
  return finding ? [finding] : [];
593
612
  });
613
+ for (const name of ruled) {
614
+ if (!objects.some((object) => object.name === name))
615
+ findings.push(found(DB_DESTRUCTIVE_RULING_UNPROVEN, file, `hfs.json db.destructiveRulings names ${name} for ${file}, which the migration does not destroy; a ruling names exactly the objects its migration destroys`, { object: name }));
616
+ }
617
+ return findings;
594
618
  }
595
619
  const BE_SQL_SOURCE = /^be\/src\/.+\.(?:sql|entity)\.[cm]?tsx?$/;
596
620
  /** The tag glob of a release (release.tagPrefix defaults to v). */
@@ -605,7 +629,8 @@ const RELEASE_TAG_GLOB = 'v*';
605
629
  * DB_MIGRATION_DESTRUCTIVE (R259), a hard blocker: ship the code change in one release and its contract migration in the next.
606
630
  */
607
631
  export async function contractReleaseFindings({ repoRoot, migrations, git }) {
608
- const contracts = migrations.filter((file) => /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/.test(file));
632
+ const rulings = destructiveRulings(repoRoot);
633
+ const contracts = migrations.filter((file) => CONTRACT_MIGRATION.test(file) || rulings.has(file));
609
634
  if (!contracts.length)
610
635
  return [];
611
636
  const previous = await runGit(git, repoRoot, ['previous-release', RELEASE_TAG_GLOB]);
@@ -631,9 +656,11 @@ export async function contractReleaseFindings({ repoRoot, migrations, git }) {
631
656
  const text = readText(repoRoot, file);
632
657
  if (text === null)
633
658
  return [];
634
- return migrationDestructiveObjects(text).flatMap((object) => {
659
+ // a ruled migration is judged here for its ruled objects only: its other objects are refused at HEAD already
660
+ const ruled = CONTRACT_MIGRATION.test(file) ? undefined : rulings.get(file);
661
+ return migrationDestructiveObjects(text).filter((object) => ruled === undefined || ruled.has(object.name)).flatMap((object) => {
635
662
  const reference = references.find(({ scopes }) => referencesObject(scopes, object));
636
- return reference ? [found(DB_MIGRATION_DESTRUCTIVE, file, `${file}:${object.line} destroys ${object.name}, which the previous release ${release} still references in ${reference.file}; the contract migration runs right after the accept while that release is the rollback target: ship the code change in one release and this contract migration in the next`, { line: object.line, object: object.name, release, reference: reference.file })] : [];
663
+ return reference ? [found(ruled === undefined ? DB_MIGRATION_DESTRUCTIVE : DB_DESTRUCTIVE_RULING_UNPROVEN, file, `${file}:${object.line} destroys ${object.name}, which the previous release ${release} still references in ${reference.file}; the contract migration runs right after the accept while that release is the rollback target: ship the code change in one release and this contract migration in the next`, { line: object.line, object: object.name, release, reference: reference.file })] : [];
637
664
  });
638
665
  });
639
666
  }
@@ -644,8 +671,14 @@ export function destructiveMigrationFindings({ repoRoot, migrations, files }) {
644
671
  if (sources.length && !ts)
645
672
  return [found(DB_MIGRATION_DESTRUCTIVE, 'hfs.json', 'destructive migration checks require the TypeScript parser to read be/src references')];
646
673
  const references = ts ? sources.map((file) => ({ file, scopes: sourceNameScopes(ts, readText(repoRoot, file) ?? '', file) })) : [];
647
- return migrations.flatMap((file) => {
674
+ const rulings = destructiveRulings(repoRoot);
675
+ const findings = migrations.flatMap((file) => {
648
676
  const text = readText(repoRoot, file);
649
- return text === null ? [] : destructiveFileFindings(file, text, references);
677
+ return text === null ? [] : destructiveFileFindings(file, text, references, rulings.get(file));
650
678
  });
679
+ for (const migration of rulings.keys()) {
680
+ if (!migrations.includes(migration))
681
+ findings.push(found(DB_DESTRUCTIVE_RULING_UNPROVEN, 'hfs.json', `hfs.json db.destructiveRulings names ${migration}, which is no active migration; a ruling names the exact file it covers`, { migration }));
682
+ }
683
+ return findings;
651
684
  }