@starci/eslint-canon-fe 9.13.4 → 9.13.6
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 +8 -0
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +6 -6
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/scripts/hfs/declaration-shape.mjs +10 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +2 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +44 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.13.6 - 2026-11-04
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
7
|
+
## 9.13.5 - 2026-11-04
|
|
8
|
+
|
|
9
|
+
- Changed: the runtime copy of the canon pins names `@starci/hfs` 5.7.1 and the release-7a versions.
|
|
10
|
+
|
|
3
11
|
## 9.13.4 - 2026-11-04
|
|
4
12
|
|
|
5
13
|
- Changed: the runtime copy of the canon pins names the release-6 versions.
|
package/package.json
CHANGED
|
@@ -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.
|
|
41
|
+
version: 0.21.23
|
|
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.
|
|
55
|
+
version: 3.16.0
|
|
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.
|
|
62
|
+
version: 9.13.6
|
|
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.
|
|
69
|
+
version: 0.21.23
|
|
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.
|
|
76
|
+
version: 0.21.23
|
|
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.
|
|
158
|
+
version: 5.8.0
|
|
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.
|
|
2048
|
-
allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.kit.ts, passport.
|
|
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
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|