@starci/hfs 4.7.22 → 4.7.24
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 +15 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/release/candidate-ci.mjs +20 -10
- package/release/gate.mjs +37 -19
- package/release/push.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/ci-standard.yaml +5 -5
- package/runtime/knowledge/hfs/rules.yaml +17 -17
- package/runtime/knowledge/hfs/slots.yaml +20 -1
- package/runtime/knowledge/patterns/be/saga.yaml +1 -1
- package/runtime/knowledge/sonar-gate.yaml +12 -12
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +0 -3
- package/runtime/packages/cli/src/catalog.generated.mjs +67 -4
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +1 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +2 -2
- package/runtime/scripts/hfs/rules/ci-standard.mjs +24 -14
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +68 -2
- package/runtime/scripts/hfs/rules/unused-symbol-properties.mjs +1 -1
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +21 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/lib/gate-loosening.mjs +248 -0
- package/runtime/scripts/lib/managed-json.mjs +51 -0
- package/runtime/scripts/lib/uat-run-folder.mjs +118 -0
- package/stack/prepare.mjs +10 -3
- package/stack/release-store.mjs +3 -3
- package/stack/remote.mjs +9 -0
- package/sync/ci-commands.mjs +38 -90
- package/sync/index.mjs +16 -5
- package/templates/app/browser-config/browser/tsconfig.json +11 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +2 -3
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -3
- package/templates/app/hooks/husky/pre-push +4 -2
- package/templates/app/package-scripts/package.json +0 -1
- package/templates/app/package-scripts-lite/package.json +0 -1
- package/templates/app/quality-config/sonar-project.properties +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +5 -6
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -6
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +5 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +6 -2
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +4 -1
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +5 -2
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +4 -1
- package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +4 -4
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +6 -2
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +2 -6
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +4 -12
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +1 -3
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +5 -7
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +5 -2
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +8 -10
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +11 -7
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +2 -2
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +6 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +16 -10
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +4 -1
- package/templates/fe/auth/supabase-client.ts.tpl +7 -7
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +1 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +18 -3
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +3 -4
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +11 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +2 -2
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +2 -2
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +2 -1
- package/templates/fe/transport/graphql/clients/links/context-headers.ts +9 -0
- package/templates/fe/transport/graphql/clients/links/http.ts +3 -3
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -1
- package/templates/fe/transport/graphql/clients/response.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.7.24 - 2026-10-23
|
|
4
|
+
- Fixed: `starci app release push` counts no skipped conclusion as passing (owner ruling 2026-10-22: a skip is a loosening): a green candidate run with a skipped job is red, and the failing-jobs report lists skipped jobs and steps. The candidate grace is 45 minutes and the bounded wait 150 minutes from the push; a queued run is listed, so GitHub's queue never counts against the grace. Specs: `src/candidate-ci.spec.mjs`.
|
|
5
|
+
- Fixed: the local release gate always runs `db-stop` once `db-start` ran (`teardownOf`): a red database row no longer leaves the lite app's local Supabase running. Specs: `src/no-loosening-ci.spec.mjs`.
|
|
6
|
+
- Changed: the bundled canon pins select @starci/cli 1.7.24, @starci/eslint-canon-be 3.4.31, @starci/eslint-canon-fe 8.4.30, @starci/jest-preset 2.3.5, @starci/prettier-config 1.0.4, @starci/test-world 1.2.3 and @starci/tsconfig 2.0.7.
|
|
7
|
+
- Changed: the skeletons, the GraphQL client templates and the lite auth partial compile under `exactOptionalPropertyTypes` (`@starci/tsconfig` 2.0.7): an option that may be absent is spread in only when present (credential guard, outcome mapper, GraphQL error mapper, the fetch and Apollo link options, the TypeORM schema), the Apollo client options and link parameters declare `signal` as `AbortSignal | undefined`, and `ApolloOutcome.data` is `TData | null | undefined`. Fixed: the lite `supabase-client.ts.tpl` partial calls `authOf()`, the function the skeleton defines, instead of the removed `supabase()`.
|
|
8
|
+
- Changed: the bundled R280 detector (`HFS_UNUSED_SYMBOL`) resolves more catalog keys so a product needs fewer `i18n-dynamic-keys.yaml` entries: the type checker reads the standard library (a key typed as a union of literals is read through `filter`/`map` callbacks), a `??` or `||` default holds both operands, a nullish branch of a property owner is skipped, and a table of constants is read through the `readonly` fields of the interface or alias that types it. Spec: `src/unused-symbol-flow.spec.mjs`.
|
|
9
|
+
- Changed: the bundled `knowledge/sonar-gate.yaml` holds every issue severity on changed lines and a change of any size to duplication (`ignoreBelowChangedLines: 0`).
|
|
10
|
+
|
|
11
|
+
## 4.7.23 - 2026-10-22
|
|
12
|
+
- Changed: the rendered `sonar-project.properties` leaves the message catalogs (`**/messages/*.messages.ts`, `**/messages/*.json`) out of Sonar's copy-paste detection (`sonar.cpd.exclusions`, owner ruling 2026-10-22). A catalog repeats its keys once per language, which CPD read as duplicated code (35% new duplicated lines on NIVO v0.3.1 once prettier formatted the catalogs); they stay sources, so lint, analysis and every other rule still hold them. Spec: `src/sonar-cpd-messages.spec.mjs`.
|
|
13
|
+
|
|
3
14
|
## 4.7.22 - 2026-10-22
|
|
4
15
|
- Fixed: the bundled runtime carries `modules/schemas/provision-operation.schema.yaml`; 4.7.21 left it out, so `starci app check` and `starci app lint` failed with ENOENT in every app whose stack declaration has an `operations` block. The composition reads both schemas through `new URL(..., import.meta.url)`, the form the runtime copy follows. Spec: `src/provision-operation-schema-bundle.spec.mjs`.
|
|
5
16
|
|
|
6
17
|
## 4.7.21 - 2026-10-22
|
|
18
|
+
- Changed: the bundled `scripts/lib/gate-loosening.mjs` has no approval route (owner ruling loosening-always-red, 2026-10-22): `approved` and `approvalIdOf` are gone, `looseningStep` is the land gate's always-red step whose only way out is restoring the gate, and the pure released-state judgement `loosensReleased` moved here from the runtime's check library. Spec: `src/gate-loosening.spec.mjs`.
|
|
19
|
+
- Added: the bundled runtime copy of `scripts/lib/gate-loosening.mjs`, which judges whether one change loosens a gate. It is rename-aware: an allowlist or rule entry that only moves with a git rename of the paths it names (the removed line, with every renamed prefix rewritten, equals an added line) is no loosening, while an entry that also widens, or one added beside the move, still is. Spec: `src/gate-loosening.spec.mjs`.
|
|
20
|
+
- Added: the opt-in managed slot `app.browser-config` (slot manifest 3.5.0): an app that declares `browser: true` gets `browser/tsconfig.json`, rendered by `starci app sync` from `templates/app/browser-config` (extending `@starci/tsconfig/e2e.json`), the program that type-checks the browser config and journeys Playwright only transpiles. Sync renders an opt-in managed slot only for an app that enables it. The bundled `scripts/lib/managed-json.mjs` names a slot-declared template and an app's byte-equal render of it as managed canon, not authored JSON. Specs: `src/browser-config.spec.mjs`, `src/managed-json.spec.mjs`.
|
|
21
|
+
- Added: the bundled runtime copy of `scripts/lib/uat-run-folder.mjs`, the pure half of `starci uat record`: a UAT run folder holds exactly manifest.yaml, result.md, screens/*.png and videos/*.webm (each of its type by its bytes), an example's fixture store receives each verified blob once, and the work/evidence@1 document cites every file by sha256 with an outcome that is pass only when the walk exited 0 and its manifest says pass. Spec: `src/uat-run-folder.spec.mjs`.
|
|
7
22
|
- Fixed: the release gate's Sonar read retries a network error or HTTP 5xx up to 5 times with backoff inside its 600 s wait; a 4xx and every gate status keep their verdict. Spec: `src/sonar-quality-gate.spec.mjs`.
|
|
8
23
|
- Fixed: `starci app db dump` archives its parts with tar run inside the scratch directory and relative names only (no `-C`, no absolute path): GNU tar, first on PATH under Git Bash on Windows, read the absolute drive-letter archive path as a remote host and every backup failed with `DB_BACKUP_TOOL_FAILED`. Spec: `src/db-dump-archive.spec.mjs`.
|
|
9
24
|
- Fixed: the bundled stacks gate validates a declaration's `operations` block with the composed provision-operation schema instead of crashing on its cross-file `$ref`; the bundled schema lets a SonarCloud admin credential name a runtime secret.env variable (`hostConnection: sonar-runtime-secret-env`).
|
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ The managed `sonar-project.properties` carries `sonar.externalIssuesReportPaths`
|
|
|
169
169
|
reaches Sonar while the job stays failed. There is no `continue-on-error`. CI runs `npm run lint -- --sonar reports/lint.sonar.json` once at the app root, before the scan. The duplicate-block threshold (`ruleParams.<side>.duplicateBlock`) has no Sonar property for TypeScript (SonarJS detects
|
|
170
170
|
clones with its own token rule), so the machine enforces it (R21) and its findings are imported like every other.
|
|
171
171
|
|
|
172
|
-
The gate is `knowledge/sonar-gate.yaml`, the one declaration: the new-code conditions (coverage 100, duplication,
|
|
172
|
+
The gate is `knowledge/sonar-gate.yaml`, the one declaration: the new-code conditions (coverage 100, duplication, issues of every severity, hotspots) and an `overall` part (coverage 100, 0 open issues on the whole code,
|
|
173
173
|
every hotspot reviewed, duplicated lines density, cognitive complexity through the S3776 rule). A SonarQube gate condition cannot filter by engine, so the condition counts every
|
|
174
174
|
open issue, imported or native; that is stricter than the three imports alone and is intended. `starci app check` reports `HFS_SONAR_CONFIG` (R11) when the
|
|
175
175
|
properties file is not its render or the stack declaration names another gate. R20 and R21 have their Sonar enforcers as conditions of that file.
|
package/package.json
CHANGED
package/release/candidate-ci.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// candidate-ci.mjs - the candidate CI of `starci app release push` (OWNER RULING candidate-ci, 2026-10-18: "never again have main receive a release whose CI is
|
|
2
2
|
// red, with the owner confirming afterwards"). The push sends the exact release sha to refs/heads/release/candidate-<version> first (the managed ci.yml triggers on
|
|
3
3
|
// release/candidate-*), waits for the ci.yml run of THAT sha, and moves the default branch and the tag only when it is green:
|
|
4
|
-
// green the newest non-cancelled run of the sha concluded success
|
|
5
|
-
// red RELEASE_CANDIDATE_CI_RED with the run URL and the failing jobs and steps; nothing reaches the default branch. The candidate ref stays for diagnosis
|
|
6
|
-
// timeout RELEASE_CANDIDATE_CI_TIMEOUT: no run
|
|
4
|
+
// green the newest non-cancelled run of the sha concluded success and none of its jobs was skipped
|
|
5
|
+
// red RELEASE_CANDIDATE_CI_RED (a run or a job that concluded skipped is red, never a pass: owner ruling 2026-10-22, a skip is a loosening) with the run URL and the failing jobs and steps; nothing reaches the default branch. The candidate ref stays for diagnosis
|
|
6
|
+
// timeout RELEASE_CANDIDATE_CI_TIMEOUT: no run listed within the grace (a queued run is listed), or it did not end within the wait
|
|
7
7
|
// unknown RELEASE_CI_UNKNOWN: GitHub could not be read to the end; a release never proceeds blind
|
|
8
8
|
// Emergency path (documented, owner-only): the owner's `starci release ci-override --sha <sha>` record for this exact sha
|
|
9
9
|
// (<git common dir>/starci-release/<sha>.ci-override.json, schema starci/ci-override@1) lets a red candidate through, and the push says so.
|
|
@@ -13,10 +13,11 @@ import path from 'node:path';
|
|
|
13
13
|
|
|
14
14
|
const CANDIDATE_PREFIX = 'refs/heads/release/candidate-';
|
|
15
15
|
/** The wait of the candidate run: poll interval, the grace for a run to appear, the wait for it to end (an app's ci.yml jobs time out within 45 minutes). */
|
|
16
|
-
|
|
16
|
+
// A queued run is listed (running): the grace only covers a run GitHub has not created yet, and the wait, counted from the push, bounds grace, queue and run.
|
|
17
|
+
const CANDIDATE_WAIT = Object.freeze({ pollSeconds: 30, graceMinutes: 45, waitMinutes: 150 });
|
|
17
18
|
const FIELDS = 'databaseId,headSha,status,conclusion,url,event,createdAt';
|
|
18
19
|
const SHA = /^[a-f0-9]{40}$/u;
|
|
19
|
-
const PASSING = new Set(['success', 'neutral'
|
|
20
|
+
const PASSING = new Set(['success', 'neutral']);
|
|
20
21
|
|
|
21
22
|
/** The candidate ref of the release tag `tag` with the app's tag prefix: refs/heads/release/candidate-<version>. */
|
|
22
23
|
export const candidateRef = (tag, prefix = 'v') => `${CANDIDATE_PREFIX}${String(tag).startsWith(prefix) ? String(tag).slice(prefix.length) : String(tag)}`;
|
|
@@ -40,18 +41,27 @@ export function candidateVerdict(rows) {
|
|
|
40
41
|
return { state: newest.conclusion === 'success' ? 'green' : 'red', run: newest };
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
+
/** The jobs of a run ([{name, conclusion, steps}]), or null when unreadable. */
|
|
45
|
+
function runJobsOf(ctx, runRow) {
|
|
46
|
+
if (!/^\d+$/u.test(String(runRow?.databaseId ?? ''))) return null;
|
|
47
|
+
const reply = jsonOf(run(ctx, 'gh', ['run', 'view', String(runRow.databaseId), '--json', 'jobs']));
|
|
48
|
+
return Array.isArray(reply?.jobs) ? reply.jobs : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One read of the ci.yml runs of `sha`: {ok, state, run} or {ok: false}. A green run is read job by job: a skipped job makes it red. */
|
|
44
52
|
function readCandidateRun(ctx, sha) {
|
|
45
53
|
const rows = jsonOf(run(ctx, 'gh', ['run', 'list', '--workflow', 'ci.yml', '--commit', sha, '--limit', '20', '--json', FIELDS]));
|
|
46
54
|
if (!Array.isArray(rows)) return { ok: false };
|
|
47
|
-
|
|
55
|
+
const seen = candidateVerdict(rows.filter((row) => !row.headSha || row.headSha === sha));
|
|
56
|
+
if (seen.state !== 'green') return { ok: true, ...seen };
|
|
57
|
+
const jobs = runJobsOf(ctx, seen.run);
|
|
58
|
+
if (!jobs) return { ok: false };
|
|
59
|
+
return { ok: true, ...seen, state: jobs.some((job) => job.conclusion === 'skipped') ? 'red' : 'green' };
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/** The failing jobs and steps of a run: ['job (conclusion): step, step']; [] when unreadable. */
|
|
51
63
|
export function failingJobs(ctx, runRow) {
|
|
52
|
-
|
|
53
|
-
const reply = jsonOf(run(ctx, 'gh', ['run', 'view', String(runRow.databaseId), '--json', 'jobs']));
|
|
54
|
-
return (reply?.jobs ?? []).filter((job) => job.conclusion && !PASSING.has(job.conclusion)).map((job) => {
|
|
64
|
+
return (runJobsOf(ctx, runRow) ?? []).filter((job) => job.conclusion && !PASSING.has(job.conclusion)).map((job) => {
|
|
55
65
|
const steps = (job.steps ?? []).filter((step) => step.conclusion && !PASSING.has(step.conclusion)).map((step) => step.name);
|
|
56
66
|
return `${job.name} (${job.conclusion})${steps.length ? `: ${steps.join(', ')}` : ''}`;
|
|
57
67
|
});
|
package/release/gate.mjs
CHANGED
|
@@ -28,7 +28,14 @@ const PREFIX = /^[A-Za-z][A-Za-z0-9._-]{0,19}$/u;
|
|
|
28
28
|
export const VERSION = /^(\d+)\.(\d+)\.(\d+)$/u;
|
|
29
29
|
const THRESHOLD_MISS = /coverage threshold .* not met/gu;
|
|
30
30
|
const WRAPPED = new Set(['npm', 'npx']);
|
|
31
|
-
|
|
31
|
+
// Every row the managed CI runs, so a release that does not wait for its candidate CI (release.requireCi false) still ran them all locally:
|
|
32
|
+
// fast (audit, format, typecheck), lint, test + coverage (the managed `test` script is the per-file 100 threshold), build, the lite database lint and types, and Sonar.
|
|
33
|
+
const SCRIPT_GATES = ['format:check', 'typecheck', 'build:be', 'lint', 'db:lint', 'build:fe', 'test'];
|
|
34
|
+
// A full app has a test world (integration, e2e and contract projects, always present: the managed `test:e2e` script marks it), so those layers are rows of its gate;
|
|
35
|
+
// a lite app's integration project exists only where it has integration specs, so it has no world rows.
|
|
36
|
+
const WORLD_SCRIPTS = ['test:integration', 'test:e2e', 'test:contract'];
|
|
37
|
+
const DB_TYPES_SCRIPTS = ['db:start', 'db:reset', 'db:types', 'db:stop'];
|
|
38
|
+
const DB_TYPES_COMMITTED = { name: 'db-types-committed', command: ['git', 'diff', '--exit-code', '--', 'supabase/types/database.types.ts'], summary: null, env: {} };
|
|
32
39
|
const MAX_PATCH = 100;
|
|
33
40
|
|
|
34
41
|
/** True for a branch or remote name git can take as one argument (no leading dash, only path-like characters). */
|
|
@@ -41,10 +48,21 @@ const scriptGate = (script) => ({
|
|
|
41
48
|
env: {},
|
|
42
49
|
});
|
|
43
50
|
|
|
44
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The lite database-types rows of the managed CI, in its order: start Supabase, apply the migrations, regenerate the types, require them committed, stop.
|
|
53
|
+
* The stop is the teardown of the start (`teardownOf`): it runs whenever the start ran, also after a red row, so a failed database row never leaves Supabase running.
|
|
54
|
+
*/
|
|
55
|
+
function dbTypesGates(scripts) {
|
|
56
|
+
if (!DB_TYPES_SCRIPTS.every((script) => typeof scripts?.[script] === 'string')) return [];
|
|
57
|
+
const [start, reset, types, stop] = DB_TYPES_SCRIPTS.map(scriptGate);
|
|
58
|
+
return [start, reset, types, DB_TYPES_COMMITTED, { ...stop, teardownOf: start.name }];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Default gates run app-check, the enabled audit, then every managed script present in the app and the database-types rows, Sonar following when it is declared. */
|
|
45
62
|
function defaultGates(scripts, audit) {
|
|
46
63
|
const present = SCRIPT_GATES.filter((script) => typeof scripts?.[script] === 'string');
|
|
47
|
-
|
|
64
|
+
const world = typeof scripts?.['test:e2e'] === 'string' ? WORLD_SCRIPTS.filter((script) => typeof scripts?.[script] === 'string') : [];
|
|
65
|
+
return [{ name: 'app-check', command: ['npx', 'starci', 'app', 'check'], summary: null, env: {} }, ...(audit ? [audit] : []), ...present.map(scriptGate), ...world.map(scriptGate), ...dbTypesGates(scripts)];
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
/** Shared quality settings retain the release reader's audit validation, disabled mode and legacy string host compatibility. */
|
|
@@ -218,7 +236,7 @@ export function launch(argv, platform) {
|
|
|
218
236
|
return [argv[0], argv.slice(1)];
|
|
219
237
|
}
|
|
220
238
|
|
|
221
|
-
/** Sonar availability is public and bounded; HTTP uses the existing injected fetch seam. */
|
|
239
|
+
/** Sonar availability is public and bounded; HTTP uses the existing injected fetch seam. An unreachable host is a red row: Sonar is required. */
|
|
222
240
|
async function sonarAvailability(ctx, gate) {
|
|
223
241
|
const sonar = gate.provider;
|
|
224
242
|
if (!sonar) return null;
|
|
@@ -228,13 +246,10 @@ async function sonarAvailability(ctx, gate) {
|
|
|
228
246
|
up = response.ok && (await response.json()).status === 'UP';
|
|
229
247
|
} catch { up = false; }
|
|
230
248
|
if (up) return null;
|
|
231
|
-
|
|
232
|
-
? `notice: sonar skipped: ${sonar.host} not reachable (services.sonar.optional)`
|
|
233
|
-
: `${SONAR_UNREACHABLE}: ${sonar.host} did not report UP within ten seconds`;
|
|
234
|
-
return { skipped: sonar.optional === true, pass: false, detail };
|
|
249
|
+
return { pass: false, detail: `${SONAR_UNREACHABLE}: ${sonar.host} did not report UP within ten seconds` };
|
|
235
250
|
}
|
|
236
251
|
|
|
237
|
-
/**
|
|
252
|
+
/** An unreachable Sonar host stays a red row with a real log and no scan invocation. */
|
|
238
253
|
async function runAvailableGate(ctx, gate, options) {
|
|
239
254
|
const availability = await sonarAvailability(ctx, gate);
|
|
240
255
|
const log = path.join(options.dir, `${gate.name}.log`);
|
|
@@ -334,10 +349,11 @@ function inspect(ctx, config, options) {
|
|
|
334
349
|
}
|
|
335
350
|
|
|
336
351
|
function planLines(config, facts, dir, recordPath) {
|
|
337
|
-
const out = [`L4 record ${recordPath}`, `logs ${dir}`, '', 'gates (
|
|
352
|
+
const out = [`L4 record ${recordPath}`, `logs ${dir}`, '', 'gates (every gate must pass, in this order):'];
|
|
338
353
|
for (const gate of config.gates) {
|
|
339
354
|
const summary = gate.summary ? ` (summary: ${gate.summary})` : '';
|
|
340
|
-
|
|
355
|
+
const teardown = gate.teardownOf ? ` (teardown of ${gate.teardownOf}: runs whenever it ran)` : '';
|
|
356
|
+
out.push(` ${gate.name.padEnd(17)} ${gate.command.join(' ')}${summary}${teardown}`);
|
|
341
357
|
}
|
|
342
358
|
out.push(`then: write ${path.basename(recordPath)} and \`git tag -a ${facts.chosen.tag}\` on ${facts.head.slice(0, 10)}; push it with \`starci app release push\`.`);
|
|
343
359
|
return out;
|
|
@@ -353,7 +369,7 @@ function assertUnmoved(ctx, head) {
|
|
|
353
369
|
function writeRecord(ctx, { head, tag, results, recordPath, requireCi }) {
|
|
354
370
|
const record = {
|
|
355
371
|
schema: 'starci-app-l4-record/1', head, tag, createdAt: new Date(ctx.now()).toISOString(), producedBy: 'starci app release gate',
|
|
356
|
-
note: '
|
|
372
|
+
note: 'Every gate passed on a clean HEAD; every entry links its log and sha256.',
|
|
357
373
|
ci: requireCi === true ? { required: true, note: 'starci app release push waits for the candidate CI of this sha (release.requireCi=true)' } : { required: false, note: CI_NOT_REQUIRED },
|
|
358
374
|
gates: results,
|
|
359
375
|
};
|
|
@@ -361,20 +377,21 @@ function writeRecord(ctx, { head, tag, results, recordPath, requireCi }) {
|
|
|
361
377
|
fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
362
378
|
}
|
|
363
379
|
|
|
364
|
-
/**
|
|
380
|
+
/**
|
|
381
|
+
* Gates run sequentially because each failure prevents subsequent commands and record creation. A teardown row (`teardownOf`) is the exception: it runs
|
|
382
|
+
* whenever the row it tears down ran, red or green, so a red row never leaves a started service behind; its own verdict counts like any row's.
|
|
383
|
+
*/
|
|
365
384
|
async function runAll(ctx, config, options) {
|
|
366
385
|
fs.mkdirSync(options.dir, { recursive: true });
|
|
367
386
|
return config.gates.reduce(async (pending, gate) => {
|
|
368
387
|
const results = await pending;
|
|
369
|
-
if (results.some((result) => !result.pass && !result.
|
|
388
|
+
if (results.some((result) => !result.pass) && !(gate.teardownOf && results.some((result) => result.name === gate.teardownOf))) return results;
|
|
370
389
|
return [...results, await runAvailableGate(ctx, gate, options)];
|
|
371
390
|
}, Promise.resolve([]));
|
|
372
391
|
}
|
|
373
392
|
|
|
374
393
|
function summaryOf(result) {
|
|
375
|
-
|
|
376
|
-
if (result.skipped) status = 'SKIPPED';
|
|
377
|
-
return `${status} ${result.name} (${result.detail}, ${result.seconds}s) log ${result.log}`;
|
|
394
|
+
return `${result.pass ? 'PASS' : 'FAIL'} ${result.name} (${result.detail}, ${result.seconds}s) log ${result.log}`;
|
|
378
395
|
}
|
|
379
396
|
|
|
380
397
|
/**
|
|
@@ -382,7 +399,8 @@ function summaryOf(result) {
|
|
|
382
399
|
* writes the L4 record and creates the annotated tag. options: { execute, sonar, source, tagPrefix, platform }.
|
|
383
400
|
*/
|
|
384
401
|
export async function releaseGate(ctx, config, options = {}) {
|
|
385
|
-
|
|
402
|
+
// Sonar is a row of the managed CI, so it is a row of the local gate whenever the app declares it (and when --sonar asks for it); a missing token is a refusal, never a skip.
|
|
403
|
+
if (options.sonar || config.services?.sonar) config = { ...config, gates: [...config.gates.filter((gate) => gate.name !== 'sonar'), sonarGate(config.services, ctx.env)] };
|
|
386
404
|
const credentialNames = config.gates.find(gate => gate.provider)?.credentialNames ?? ['SONAR_TOKEN', config.services?.sonar?.token].filter(Boolean);
|
|
387
405
|
ctx = Object.create(ctx, {env: {value: withoutCredentials(ctx.env, credentialNames)}});
|
|
388
406
|
// The CI verdict of a release is the candidate run of its own sha, judged by `starci app release push` (candidate-ci.mjs, OWNER RULING candidate-ci 2026-10-18):
|
|
@@ -397,7 +415,7 @@ export async function releaseGate(ctx, config, options = {}) {
|
|
|
397
415
|
if (options.execute !== true) return { ok: true, lines: [...plan, '', 'dry run: no gate was run, nothing written. preconditions OK.'], data: { ...base, red: [], executed: false } };
|
|
398
416
|
const results = await runAll(ctx, config, { dir, platform: options.platform ?? process.platform, credentialNames });
|
|
399
417
|
const report = results.map(summaryOf);
|
|
400
|
-
const failed = results.filter((result) => !result.pass
|
|
418
|
+
const failed = results.filter((result) => !result.pass);
|
|
401
419
|
if (failed.length > 0) return { ok: false, lines: [...plan, ...report, `gate(s) red: ${failed.map((item) => item.name).join(', ')}. No L4 record, no tag.`], data: { ...base, executed: true, results } };
|
|
402
420
|
assertUnmoved(ctx, facts.head);
|
|
403
421
|
writeRecord(ctx, { head: facts.head, tag: facts.chosen.tag, results, recordPath, requireCi: config.requireCi });
|
package/release/push.mjs
CHANGED
|
@@ -19,7 +19,7 @@ function readRecord(file) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const greenRecord = (record, head, tag) => record?.head === head && record?.tag === tag
|
|
22
|
-
&& Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true
|
|
22
|
+
&& Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true);
|
|
23
23
|
|
|
24
24
|
/** The release gate as the hook reads it: the annotated tag on HEAD whose L4 record names head and tag. Returns { tag } or { reason }. */
|
|
25
25
|
function gateState(ctx, head, prefix) {
|
|
@@ -29,14 +29,14 @@ pins:
|
|
|
29
29
|
source: packages/grammar/package.json
|
|
30
30
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
31
31
|
'@starci/eslint-canon-be':
|
|
32
|
-
version: 3.4.
|
|
32
|
+
version: 3.4.31
|
|
33
33
|
group: starci
|
|
34
34
|
install: registry
|
|
35
35
|
side: be
|
|
36
36
|
source: packages/eslint/be/package.json
|
|
37
37
|
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.'
|
|
38
38
|
'@starci/eslint-canon-fe':
|
|
39
|
-
version: 8.4.
|
|
39
|
+
version: 8.4.30
|
|
40
40
|
group: starci
|
|
41
41
|
install: registry
|
|
42
42
|
side: fe
|
|
@@ -50,40 +50,40 @@ pins:
|
|
|
50
50
|
source: packages/stylelint/package.json
|
|
51
51
|
why: 'the front-end stylelint canon: no-class-selector refuses a compound class selector (`div.card`); the brand layer accepts `--font-sans` and `--font-mono`; status-contrast measures the HeroUI soft pairs per theme; brand-layer-shape reads the shared :root,.light,.dark block and the info soft pair; appTokens come from loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
52
52
|
'@starci/tsconfig':
|
|
53
|
-
version: 2.0.
|
|
53
|
+
version: 2.0.7
|
|
54
54
|
group: starci
|
|
55
55
|
install: registry
|
|
56
56
|
side: both
|
|
57
57
|
source: packages/tsconfig/package.json
|
|
58
58
|
'@starci/prettier-config':
|
|
59
|
-
version: 1.0.
|
|
59
|
+
version: 1.0.4
|
|
60
60
|
group: starci
|
|
61
61
|
install: registry
|
|
62
62
|
side: both
|
|
63
63
|
source: packages/prettier-config/package.json
|
|
64
64
|
'@starci/jest-preset':
|
|
65
|
-
version: 2.3.
|
|
65
|
+
version: 2.3.5
|
|
66
66
|
group: starci
|
|
67
67
|
install: registry
|
|
68
68
|
side: be
|
|
69
69
|
source: packages/jest-preset/package.json
|
|
70
70
|
why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
71
71
|
'@starci/test-world':
|
|
72
|
-
version: 1.2.
|
|
72
|
+
version: 1.2.3
|
|
73
73
|
group: starci
|
|
74
74
|
install: registry
|
|
75
75
|
side: be
|
|
76
76
|
source: packages/test-world/package.json
|
|
77
77
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
78
78
|
'@starci/cli':
|
|
79
|
-
version: 1.7.
|
|
79
|
+
version: 1.7.24
|
|
80
80
|
group: starci
|
|
81
81
|
install: registry
|
|
82
82
|
side: both
|
|
83
83
|
source: packages/cli/package.json
|
|
84
84
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
85
85
|
'@starci/hfs':
|
|
86
|
-
version: 4.7.
|
|
86
|
+
version: 4.7.24
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -465,7 +465,7 @@ pins:
|
|
|
465
465
|
version: 13.4.5
|
|
466
466
|
group: framework
|
|
467
467
|
side: be
|
|
468
|
-
why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the
|
|
468
|
+
why: 'the GraphQL transport of every back end (be.transport.graphql) that the front end calls; the full-app resolution on 2026-10-13. Stays on 13.x for alpha.18: the npm-audit advisory fixed by 14.0.3 (semver major) is a documented accepted exception, upgrade deferred to alpha.19 (owner ruling graphql-13-advisory-accepted-until-a19, 2026-10-16).'
|
|
469
469
|
'@graphql-tools/utils':
|
|
470
470
|
version: 12.1.0
|
|
471
471
|
group: framework
|
|
@@ -475,12 +475,12 @@ pins:
|
|
|
475
475
|
version: 13.4.5
|
|
476
476
|
group: framework
|
|
477
477
|
side: be
|
|
478
|
-
why: 'the Apollo driver of @nestjs/graphql; the
|
|
478
|
+
why: 'the Apollo driver of @nestjs/graphql; the full-app resolution on 2026-10-13. Stays on 13.x with @nestjs/graphql (accepted advisory, major deferred to alpha.19).'
|
|
479
479
|
'@apollo/server':
|
|
480
480
|
version: 5.5.1
|
|
481
481
|
group: framework
|
|
482
482
|
side: be
|
|
483
|
-
why: 'the Apollo server behind @nestjs/apollo; the
|
|
483
|
+
why: 'the Apollo server behind @nestjs/apollo; the full-app resolution on 2026-10-13.'
|
|
484
484
|
'@nestjs/websockets':
|
|
485
485
|
version: 11.2.5
|
|
486
486
|
group: framework
|
|
@@ -59,7 +59,7 @@ items:
|
|
|
59
59
|
check: ci-standard
|
|
60
60
|
- id: test-sharding
|
|
61
61
|
title: the unit run in shards, merged once
|
|
62
|
-
requires: "the test job is a matrix over shard (ci.jobs.test.shards, default defaults.shards); each shard runs npm test with --shard=<i>/<N
|
|
62
|
+
requires: "the test job is a matrix over shard (ci.jobs.test.shards, default defaults.shards); each shard runs npm test with --shard=<i>/<N> and STARCI_COVERAGE_SHARD=1 (the preset owns shard mode: it measures only the files the shard loads, no all-files instrumentation, no per-file threshold) and uploads its istanbul JSON, with no --passWithNoTests and no --coverageThreshold flag; the shard count never exceeds what the unit specs fill (jest gives shard i the slice of ceil(specs / N) specs, so an empty shard is impossible) ; the coverage job merges them by source location (starci app coverage merge, @starci/jest-preset/merge) and the coverage proof then requires every measured file to be present in the merge at 100 per file"
|
|
63
63
|
covers: [app-ci]
|
|
64
64
|
check: ci-standard
|
|
65
65
|
- id: lint-parallel
|
|
@@ -77,10 +77,10 @@ items:
|
|
|
77
77
|
requires: "the build job builds every fe app with that app's own public build values (static NEXT_PUBLIC_* from the declaration), never a shared env"
|
|
78
78
|
covers: [app-ci]
|
|
79
79
|
by: [render packages/hfs/sync/ci-commands.mjs, R300 FE_PUBLIC_BUILD_ARG_MISSING]
|
|
80
|
-
- id:
|
|
81
|
-
title:
|
|
82
|
-
requires: "
|
|
83
|
-
covers: [app-ci]
|
|
80
|
+
- id: required-providers
|
|
81
|
+
title: Codecov and Sonar are required, nothing continues on error
|
|
82
|
+
requires: "no step or job sets continue-on-error; a Codecov upload sets fail_ci_if_error: true; no Codecov or Sonar step has an availability probe or an `if` that reads a secret, CODECOV_TOKEN or SONAR_TOKEN (the sonar job refuses a missing token with its own failing step); a red step is red"
|
|
83
|
+
covers: [app-ci, runtime]
|
|
84
84
|
check: ci-standard
|
|
85
85
|
- id: timeouts
|
|
86
86
|
title: a timeout on every job, configurable per job
|
|
@@ -56,7 +56,7 @@ rules:
|
|
|
56
56
|
- {path: packages/eslint/fe/slot-ownership.spec.mjs, editions: [lite, full], codes: [HFS_SLOT_UNDECLARED, HFS_SLOT_AMBIGUOUS, HFS_SLOT_NOT_ENABLED]}
|
|
57
57
|
exemplifiedBy:
|
|
58
58
|
- {path: examples/lite-app/be/src/features/webhooks/calendar/transport/http/calendar.webhook.ts, edition: lite}
|
|
59
|
-
- {path: examples/
|
|
59
|
+
- {path: examples/full-app/be/src/features/webhooks/sepay/transport/http/sepay.webhook.ts, edition: full}
|
|
60
60
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
61
61
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; the package specs named in proves keep proving the lint enforcers, the runtime-code enforcers lost their deleted runtime spec"
|
|
62
62
|
- id: R02
|
|
@@ -846,7 +846,7 @@ rules:
|
|
|
846
846
|
exemplifiedBy:
|
|
847
847
|
- {path: packages/hfs/templates/be/inbox/service.spec.ts.tpl, editions: [lite, full]}
|
|
848
848
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
849
|
-
law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required, except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app hygiene` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer
|
|
849
|
+
law: "One jest config, projects `unit`, `integration`, `e2e` and `contract`, live by folder, `diagnostics: false`; unit specs are `<name>.<role>.spec.ts` beside the file they test: required for the roles of `ruleParams.be.unitRoles` (`<name>.service.spec.ts` beside a `<name>.service.ts`, `<name>.cli.spec.ts` beside a cli command `<name>.cli.ts` of `src/features/cli`), optional for any other logic role (`ruleParams.be.logicRoles`) inside a slot whose `coverage` is required, except that the commit adding a measured logic file stages its `<name>.<role>.spec.ts` beside it (owner 2026-10-20, judged by `starci app hygiene` in the pre-commit hook), and nowhere else but beside a webhook or realtime door that has no service of its own (`<provider>.webhook.spec.ts`, `<channel>.gateway.spec.ts`, `<x>.subscription.spec.ts`) (every unit-tested subject has exactly one; any other `*.spec.ts` outside `be/src/tests/{world,integration,e2e,contract}` is a finding, including specs in apps), and the unit project collects coverage from the logic roles of the measured roots only (the scope derived from the slot manifest, R204) with a per-file threshold of 100 on lines, branches, functions and statements (`test` runs `--coverage`; `importHelpers` and `tslib`); `be/src/tests/world/` is the ONLY test infrastructure: `global-setup.ts` starts the app's own stack (`.starcistacks/<env>`, named by `stack` in `test-world.config.ts`, the declaration of the test-world library in its one named form `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (a default export is not read): every service it declares runs REAL, behind toxiproxy, images and versions read from the stack at run time, never spelled in test source) or attaches to a warm one that answers, and runs the migrations once over the cli app's connections (the runner of the cli migrate command); `global-teardown.ts` stops only what its setup started; `use-test-world.ts` exports `useTestWorld({ apps } | { modules })` (`world.apps.<name>.api`, `world.db.<connection>`, `world.infra.<service>` with `latency(ms)`, `cut()` and `restore()` on the real service, `world.fake.<provider>`, `world.waitFor`) and `fakes/<provider>/` holds the network-edge fakes (`failNext`, `replayWebhook`, `delay`, payload fixtures) of external SaaS the team does not operate ONLY: a fake of a service the stack declares is refused, except stateless compute that needs special hardware or an external model (GPU inference, a self-hosted embedding model), whose `stacks` entry in `test-world.config.ts` declares `{ fakedBy: <fake>, reason }` with a non-empty `reason` (a stateful service, or one with a persistent volume, is never accepted); it alone owns containers, DataSource, migrations and `process.env`; nothing under `be/src/tests` overrides a provider or DI token; `be/src/tests/integration/<capability>/*.integration-spec.ts` (`{ modules }`, with the real peer apps its client calls beside them as `apps`; real infrastructure, no HTTP door of ours), `be/src/tests/e2e/<area>/*.e2e-spec.ts` (`{ apps }`) and `be/src/tests/contract/<provider>/*.contract-spec.ts` (provider sandboxes, skipped without sandbox config, run only by `test:contract`, never by `test` or `test:e2e`) agree folder with suffix; an integration or e2e spec passes `useTestWorld` one object literal with plain keys (no variable, spread or computed key) and calls it; the contract layer has no skip either: a contract spec writes `sandbox.describe(...)` from the test-world library's `useSandbox(...)`, and when the sandbox config is absent that describe registers one failing test naming the missing keys (the body never runs), and every `fakes/<provider>/` that serves payload fixtures (`payloads/*.json`) has a `contract/<provider>/*.contract-spec.ts` that references those fixtures and asserts `shapeOf(real)` equals `shapeOf(fixture)` (the helper `shapeOf` is named by `ruleParams.be.contractShape` of the slot manifest; fakes the test-world library provides are guarded by the library); no `.test.ts`, `int-spec` or `harness-spec`, no `live/` and no `be/src/tests/e2e/world`."
|
|
850
850
|
kinds: [check, codemod, lint]
|
|
851
851
|
gates: [land, ci]
|
|
852
852
|
failureCodes: ["BE_TEST_TOPOLOGY", "BE_CONTRACT_UNGUARDED", "BE_UNIT_SPEC_NOT_STAGED"]
|
|
@@ -2540,7 +2540,7 @@ rules:
|
|
|
2540
2540
|
enforcers:
|
|
2541
2541
|
- {kind: hfs, id: pattern-spec-missing, at: scripts/hfs/rules/event-bus.mjs, editions: [full]}
|
|
2542
2542
|
- {kind: hfs, id: lite-proof-lane, at: scripts/hfs/rules/lite-proof-lane.mjs, editions: [lite]}
|
|
2543
|
-
editionWhy: "
|
|
2543
|
+
editionWhy: "each enforcer judges the edition it emits for (pattern-spec-missing returns no findings for lite, lite-proof-lane is lite's emitter), so the split adds lite's emitter and drops no finding; ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4) gives lite its integration proof lane while full retains e2e or integration proofs."
|
|
2544
2544
|
docs: [docs/quality-bar.md#r155]
|
|
2545
2545
|
templateNa: "Pattern scenarios depend on application behavior and providers; no universal runnable scenario proof is scaffolded."
|
|
2546
2546
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
@@ -3282,7 +3282,7 @@ rules:
|
|
|
3282
3282
|
- {path: packages/hfs/templates/app/ci-workflows-lite/github/workflows/ci.yml, edition: lite}
|
|
3283
3283
|
- {path: packages/hfs/templates/app/ci-workflows/github/workflows/ci.yml, edition: full}
|
|
3284
3284
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
3285
|
-
law: "A coverage upload of a CI workflow never skips or fails silently: every step that uses codecov/codecov-action, in a root workflow, an example workflow or a template workflow of packages/hfs, authenticates by OIDC (`use_oidc: true`, with `permissions: id-token: write` on the job or the workflow), sets `fail_ci_if_error: true`, has no `continue-on-error`, and has no `if` that reads a secret or CODECOV_TOKEN, so a missing token can never turn the upload into a skipped step."
|
|
3285
|
+
law: "A coverage upload of a CI workflow never skips or fails silently: every step that uses codecov/codecov-action, in a root workflow, an example workflow or a template workflow of packages/hfs, authenticates by OIDC (`use_oidc: true`, with `permissions: id-token: write` on the job or the workflow), sets `fail_ci_if_error: true`, has no `continue-on-error`, and has no `if` that reads a secret or CODECOV_TOKEN, so a missing token can never turn the upload into a skipped step. Every step that uses SonarSource/* has no `if` that reads a secret or SONAR_TOKEN (a missing token fails the job), and no step or job of a workflow sets `continue-on-error`."
|
|
3286
3286
|
scope: runtime
|
|
3287
3287
|
kinds: [check]
|
|
3288
3288
|
gates: [runtime]
|
|
@@ -3754,7 +3754,7 @@ rules:
|
|
|
3754
3754
|
- {path: packages/eslint/be/sonar-parity-editions.spec.mjs, edition: full}
|
|
3755
3755
|
- {path: packages/eslint/fe/sonar-parity-editions.spec.mjs, edition: lite}
|
|
3756
3756
|
- {path: packages/eslint/fe/sonar-parity-editions.spec.mjs, edition: full}
|
|
3757
|
-
law: "Code carries none of the smells SonarCloud measures that a lint rule can decide: no `await` inside a loop (S9382; an endless loop is spared), no `charCodeAt`/`fromCharCode` (S7758), no string that only escapes backslashes (S7780; a Next `matcher` is spared, it is read statically), no nested conditional expression (S3358), no `void` on anything but a call (S3735), no unused import (S1128), no import that is only handed on instead of `export ... from` (S7763), no prop of a component's props type the component never reads (S6767, front end), no `<video>` or `<audio>` without a captions track unless muted (S4084, front end), and, through typescript-eslint with type information, no deprecated API (S1874), no object turned into text (S6551) and no `async` function that never awaits and returns no promise (S7503). No function takes more than seven parameters (S107), except the constructor of an injectable class, whose parameters are its injected dependencies. Every other rule of the Sonar way TypeScript profile with an ESLint equivalent that reports what the server reports on the same source (scripts/lib/sonar-way.mjs: eslint-plugin-sonarjs' Sonar way set and the core and unicorn rules it builds on) is on at error in both canons; a port that
|
|
3757
|
+
law: "Code carries none of the smells SonarCloud measures that a lint rule can decide: no `await` inside a loop (S9382; an endless loop is spared), no `charCodeAt`/`fromCharCode` (S7758), no string that only escapes backslashes (S7780; a Next `matcher` is spared, it is read statically), no nested conditional expression (S3358), no `void` on anything but a call (S3735), no unused import (S1128), no import that is only handed on instead of `export ... from` (S7763), no prop of a component's props type the component never reads (S6767, front end), no `<video>` or `<audio>` without a captions track unless muted (S4084, front end), and, through typescript-eslint with type information, no deprecated API (S1874), no object turned into text (S6551) and no `async` function that never awaits and returns no promise (S7503). No function takes more than seven parameters (S107), except the constructor of an injectable class, whose parameters are its injected dependencies. Every other rule of the Sonar way TypeScript profile with an ESLint equivalent that reports what the server reports on the same source (scripts/lib/sonar-way.mjs: eslint-plugin-sonarjs' Sonar way set and the core and unicorn rules it builds on) is on at error in both canons (S7785, top-level await, runs in the front end only: a back end compiles to CommonJS); a port that disagrees with the server is fixed in the source it reports, never left out. The files a unit or an end-to-end spec names are outside the scan, so outside these rules."
|
|
3758
3758
|
kinds: [lint, codemod]
|
|
3759
3759
|
gates: [pre-commit, settle, land, ci]
|
|
3760
3760
|
failureCodes: ["SCAN_SMELL"]
|
|
@@ -3781,10 +3781,10 @@ rules:
|
|
|
3781
3781
|
code: "RT_GATE_LOOSENING"
|
|
3782
3782
|
editions: [lite, full]
|
|
3783
3783
|
docs: [docs/debugging.md#r236]
|
|
3784
|
-
templateNa: "Runtime release
|
|
3784
|
+
templateNa: "Runtime release history is judged against the committed gates of the last release; product scaffolding has no runtime release history to generate."
|
|
3785
3785
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
3786
3786
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
3787
|
-
law: "A change that loosens a gate or check is
|
|
3787
|
+
law: "A change that loosens a gate or check is always red: a check removed from a gate file's list, a floor lowered or a ceiling raised there, an allowlist entry added, a spec deleted or weakened or skipped together with product code (modules/kernel/gate-loosening.yaml). It never lands and never passes the check; there is no approval route, the owner's included (owner:loosening-always-red), and the fix is to restore the gate. An entry that only moves with a git rename of the paths it names is no loosening."
|
|
3788
3788
|
scope: runtime
|
|
3789
3789
|
kinds: [check]
|
|
3790
3790
|
gates: [runtime]
|
|
@@ -4254,7 +4254,7 @@ rules:
|
|
|
4254
4254
|
- {"path":"packages/hfs/templates/be/inbox/webhook-door.ts.tpl","editions":["lite","full"]}
|
|
4255
4255
|
- {"path":"packages/hfs/templates/be/inbox/service.ts.tpl","editions":["lite","full"]}
|
|
4256
4256
|
- {"path":"examples/lite-app/be/src/features/webhooks/calendar/transport/http/calendar.webhook.ts","edition":"lite"}
|
|
4257
|
-
- {"path":"examples/
|
|
4257
|
+
- {"path":"examples/full-app/be/src/features/webhooks/sepay/transport/http/sepay.webhook.ts","edition":"full"}
|
|
4258
4258
|
- {"path":"packages/eslint/be/webhook-handoff-inbox.spec.mjs","editions":["lite","full"],"codes":["BE_WEBHOOK_INBOX_KEY"]}
|
|
4259
4259
|
proves:
|
|
4260
4260
|
- {"path":"packages/eslint/be/webhook-handoff-scope.spec.mjs","editions":["lite","full"],"codes":["BE_WEBHOOK_SCOPE"]}
|
|
@@ -4279,7 +4279,7 @@ rules:
|
|
|
4279
4279
|
exemplifiedBy:
|
|
4280
4280
|
- {"path":"packages/hfs/templates/be/skeleton/src/features/api/system-health/application/check-liveness.contracts.ts","editions":["lite","full"]}
|
|
4281
4281
|
- {"path":"examples/lite-app/be/src/features/api/bookings/application/create-booking.contracts.ts","edition":"lite"}
|
|
4282
|
-
- {"path":"examples/
|
|
4282
|
+
- {"path":"examples/full-app/be/src/features/api/health/application/check-health.contracts.ts","edition":"full"}
|
|
4283
4283
|
proves:
|
|
4284
4284
|
- {"path":"packages/eslint/be/feature-thin.spec.mjs","editions":["lite","full"],"codes":["HFS_FEATURE_LOGIC_ROLE"]}
|
|
4285
4285
|
witness:
|
|
@@ -4301,7 +4301,7 @@ rules:
|
|
|
4301
4301
|
- {"path":"packages/hfs/templates/be/skeleton-lite/apps/api/src/compose/platform.ts","edition":"lite"}
|
|
4302
4302
|
- {"path":"packages/hfs/templates/be/skeleton/apps/__app__/src/compose/platform.ts","edition":"full"}
|
|
4303
4303
|
- {"path":"examples/lite-app/be/apps/api/src/compose/platform.ts","edition":"lite"}
|
|
4304
|
-
- {"path":"examples/
|
|
4304
|
+
- {"path":"examples/full-app/be/apps/order/src/compose/platform.ts","edition":"full"}
|
|
4305
4305
|
proves:
|
|
4306
4306
|
- {"path":"packages/eslint/be/app-composition.spec.mjs","editions":["lite","full"],"codes":["BE_COMPOSITION_UNTYPED"]}
|
|
4307
4307
|
witness:
|
|
@@ -4453,7 +4453,7 @@ rules:
|
|
|
4453
4453
|
exemplifiedBy:
|
|
4454
4454
|
- {"path":"packages/hfs/templates/be/tool-config/jest.config.js","editions":["lite","full"]}
|
|
4455
4455
|
- {"path":"examples/lite-app/be/jest.config.js","edition":"lite"}
|
|
4456
|
-
- {"path":"examples/
|
|
4456
|
+
- {"path":"examples/full-app/be/jest.config.js","edition":"full"}
|
|
4457
4457
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
4458
4458
|
|
|
4459
4459
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
@@ -4794,7 +4794,7 @@ rules:
|
|
|
4794
4794
|
- {path: packages/eslint/fe/function-budget.spec.mjs, editions: [lite, full], codes: [HFS_FUNCTION_BUDGET]}
|
|
4795
4795
|
- {path: packages/hfs/templates/be/patterns/api/graphql.resolver.ts.tpl, editions: [lite, full]}
|
|
4796
4796
|
- {path: examples/lite-app/be/src/features/api/bookings/application/create-booking.handler.ts, editions: [lite]}
|
|
4797
|
-
- {path: examples/
|
|
4797
|
+
- {path: examples/full-app/be/src/features/api/checkout/application/add-cart-item.handler.ts, editions: [full]}
|
|
4798
4798
|
proves:
|
|
4799
4799
|
- {path: packages/eslint/be/function-budget.spec.mjs, editions: [lite, full], codes: [HFS_FUNCTION_BUDGET]}
|
|
4800
4800
|
- {path: packages/eslint/fe/function-budget.spec.mjs, editions: [lite, full], codes: [HFS_FUNCTION_BUDGET]}
|
|
@@ -4815,7 +4815,7 @@ rules:
|
|
|
4815
4815
|
exemplifiedBy:
|
|
4816
4816
|
- {path: packages/hfs/templates/be/patterns/api, editions: [lite, full]}
|
|
4817
4817
|
- {path: examples/lite-app/be/src/features/api/bookings, editions: [lite]}
|
|
4818
|
-
- {path: examples/
|
|
4818
|
+
- {path: examples/full-app/be/src/features/api/checkout, editions: [full]}
|
|
4819
4819
|
specNa: "owner:no-runtime-specs (2026-10-17): the runtime has no specs of its own; this code is enforced by runtime code and its spec proof was a deleted runtime spec"
|
|
4820
4820
|
|
|
4821
4821
|
witness: {status: unreachable, reason: "owner:no-runtime-specs (2026-10-17): the mutation witness was a deleted runtime spec or fixture", date: "2026-10-17"}
|
|
@@ -4876,7 +4876,7 @@ rules:
|
|
|
4876
4876
|
- {path: packages/hfs/templates/be/patterns/api/feature.module-definition.ts.tpl, edition: lite}
|
|
4877
4877
|
- {path: packages/hfs/templates/be/patterns/api/graphql.module-definition.ts.tpl, edition: lite}
|
|
4878
4878
|
- {path: examples/lite-app/be/src/features/api/bookings/transport/graphql/bookings-graphql.module-definition.ts, edition: lite}
|
|
4879
|
-
- {path: examples/
|
|
4879
|
+
- {path: examples/full-app/be/src/features/api/checkout/checkout.module-definition.ts, edition: full}
|
|
4880
4880
|
- {path: packages/eslint/be/module-shape.spec.mjs, editions: [lite, full]}
|
|
4881
4881
|
proves:
|
|
4882
4882
|
- {path: packages/eslint/be/module-shape.spec.mjs, editions: [lite, full], codes: [BE_MODULE_DEFINITION_MISSING]}
|
|
@@ -4895,7 +4895,7 @@ rules:
|
|
|
4895
4895
|
- {path: docs/application-stacks.md#r296, editions: [lite, full]}
|
|
4896
4896
|
exemplifiedBy:
|
|
4897
4897
|
- {path: examples/lite-app/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4898
|
-
- {path: examples/
|
|
4898
|
+
- {path: examples/full-app/.starcistacks/application-stacks.yaml, edition: full}
|
|
4899
4899
|
- {path: examples/shape-slot/.starcistacks/application-stacks.yaml, edition: full}
|
|
4900
4900
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4901
4901
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/application-stacks.yaml, edition: full}
|
|
@@ -4914,7 +4914,7 @@ rules:
|
|
|
4914
4914
|
- {path: docs/application-stacks.md#r297, editions: [lite, full]}
|
|
4915
4915
|
exemplifiedBy:
|
|
4916
4916
|
- {path: examples/lite-app/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4917
|
-
- {path: examples/
|
|
4917
|
+
- {path: examples/full-app/.starcistacks/application-stacks.yaml, edition: full}
|
|
4918
4918
|
- {path: examples/shape-slot/.starcistacks/application-stacks.yaml, edition: full}
|
|
4919
4919
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4920
4920
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/application-stacks.yaml, edition: full}
|
|
@@ -4933,7 +4933,7 @@ rules:
|
|
|
4933
4933
|
- {path: docs/application-stacks.md#r298, editions: [lite, full]}
|
|
4934
4934
|
exemplifiedBy:
|
|
4935
4935
|
- {path: examples/lite-app/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4936
|
-
- {path: examples/
|
|
4936
|
+
- {path: examples/full-app/.starcistacks/application-stacks.yaml, edition: full}
|
|
4937
4937
|
- {path: examples/shape-slot/.starcistacks/application-stacks.yaml, edition: full}
|
|
4938
4938
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml, edition: lite}
|
|
4939
4939
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/application-stacks.yaml, edition: full}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
|
|
8
8
|
# Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
|
|
9
9
|
schema: starci/hfs-slots@3
|
|
10
|
-
version: 3.
|
|
10
|
+
version: 3.5.0
|
|
11
11
|
|
|
12
12
|
versioning:
|
|
13
13
|
# MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
|
|
@@ -39,6 +39,8 @@ versioning:
|
|
|
39
39
|
# when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
|
|
40
40
|
# 3.3.0 is a minor (alpha.16): it ADDS the optional app.ci-stack-deploy slot, the managed workflow of the starci app stack verbs.
|
|
41
41
|
# 3.4.0 is a minor (alpha.25): it ADDS the opt-in be.contract.mcp-tools slot, the committed MCP tool-catalog snapshot of an app.
|
|
42
|
+
# 3.5.0 is a minor (alpha.27): it ADDS the opt-in app.browser-config slot, the managed browser/tsconfig.json that type-checks the browser
|
|
43
|
+
# journey (enabled with app.browser by `browser: true`).
|
|
42
44
|
|
|
43
45
|
# Field meaning, per slot.
|
|
44
46
|
# id stable name, never changes meaning inside a major.
|
|
@@ -617,6 +619,23 @@ slots:
|
|
|
617
619
|
tests: none
|
|
618
620
|
requires: [playwright.config.ts, journeys/]
|
|
619
621
|
allows: [playwright.config.ts, stack.mjs, "journeys/*.ts"]
|
|
622
|
+
- id: app.browser-config
|
|
623
|
+
since: 3.5.0
|
|
624
|
+
profiles: [app]
|
|
625
|
+
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): browser journeys belong to full; lite proves app boot through R272 wiring."
|
|
626
|
+
# The program that type-checks the browser journey: Playwright only transpiles the config and the journeys, so without it nothing
|
|
627
|
+
# type-checks them and the op gate finds no tsconfig.json owning a changed journey (GATE_TSC_CONFIG_MISSING). It is managed (rendered
|
|
628
|
+
# by starci app sync from templates/app/browser-config, extending @starci/tsconfig/e2e.json), never authored, and enabled with
|
|
629
|
+
# app.browser by `browser: true` in hfs.json.
|
|
630
|
+
path: browser/tsconfig.json
|
|
631
|
+
presence: opt-in
|
|
632
|
+
litePresence: forbidden
|
|
633
|
+
goesTo: "the full edition: a lite app has no browser journey to type-check"
|
|
634
|
+
tracked: tracked
|
|
635
|
+
tier: none
|
|
636
|
+
tests: none
|
|
637
|
+
managedBy: browser-config
|
|
638
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
620
639
|
- id: app.github-meta
|
|
621
640
|
since: 3.1.0
|
|
622
641
|
profiles: [app]
|