@starci/hfs 4.7.44 → 4.7.46
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 +9 -0
- package/lint/run.mjs +46 -10
- package/package.json +4 -1
- package/release/candidate-ci.mjs +7 -7
- package/release/push.mjs +3 -3
- package/runtime/config.example.yaml +9 -10
- package/runtime/engine/config.mjs +3 -3
- package/runtime/engine/release-config.mjs +5 -5
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +1 -1
- package/runtime/knowledge/grammars/starci/playbook.yaml +27 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -7
- package/runtime/knowledge/hfs/ci-standard.yaml +1 -1
- package/runtime/knowledge/hfs/fe-stack.yaml +12 -1
- package/runtime/knowledge/hfs/rules.yaml +41 -6
- package/runtime/knowledge/hfs/slots.yaml +5 -5
- package/runtime/knowledge/patterns/be/realtime.yaml +72 -12
- package/runtime/modules/kernel/command-policy.yaml +5 -5
- package/runtime/modules/kernel/failure-codes.yaml +42 -20
- package/runtime/modules/kernel/removed-vocabulary.yaml +33 -7
- package/runtime/packages/cli/src/catalog.generated.mjs +90 -37
- package/runtime/scripts/api/git/commit-tree.mjs +5 -0
- package/runtime/scripts/api/git/update-ref.mjs +8 -0
- package/runtime/scripts/api/git/write-tree.mjs +5 -0
- package/runtime/scripts/guards/command-policy.mjs +4 -4
- package/runtime/scripts/guards/push-scan.mjs +128 -0
- package/runtime/scripts/guards/release-push-gate.mjs +31 -29
- package/runtime/scripts/hfs/architecture/index.mjs +2 -1
- package/runtime/scripts/hfs/architecture/type-context.mjs +38 -23
- package/runtime/scripts/hfs/example-secrets.mjs +61 -8
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +5 -5
- package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +61 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +4 -6
- package/runtime/scripts/hfs/typescript-programs.mjs +30 -3
- package/runtime/scripts/machine/home.mjs +2 -2
- package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
- package/templates/app/hooks/husky/pre-push +3 -56
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +1 -3
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +1 -3
- package/templates/be/patterns/realtime/realtime-config.mapper.ts.tpl +32 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +0 -10
- package/runtime/scripts/api/git/merge-base-query.mjs +0 -5
- package/runtime/scripts/gates/commit-proof.mjs +0 -53
- package/runtime/scripts/guards/release-definition.mjs +0 -204
- package/runtime/scripts/guards/release-record.mjs +0 -58
- package/runtime/scripts/guards/release-suite-mode.mjs +0 -15
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +0 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.7.46 - 2026-10-26
|
|
4
|
+
|
|
5
|
+
- Changed: owner ruling push-main-after-scoped-check reaches product apps: the generated pre-push hook passes a fast-forward main push when the hooks pass and the secret scan is clean, a v* tag must be annotated, and app CI runs on pushes to main.
|
|
6
|
+
- Changed: candidate-ci keeps only catalogued failure codes.
|
|
7
|
+
|
|
8
|
+
## 4.7.45 - 2026-10-26
|
|
9
|
+
|
|
10
|
+
- Changed: the alpha.31.1 round folded into alpha.31; the bundled canon pins and runtime copies follow the final package versions. No rule changes.
|
|
11
|
+
|
|
3
12
|
## 4.7.44 - 2026-10-26
|
|
4
13
|
- Changed: the bundled canon pins follow the lint family bumps (canon-be 3.4.51, canon-fe 9.1.6, Grammar 0.12.10) and the bundled runtime copy carries the wave version. No rule changes.
|
|
5
14
|
|
package/lint/run.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// turbo `lint` task); the root `npm run lint` stays the whole app. There is no second linter: a workspace lint is this lint, scoped.
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import { createRequire
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
21
|
import { spawn } from 'node:child_process';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
23
|
import { linterReport, mergeReports, sonarReport, sourceRootsOf } from '../report/sonar.mjs';
|
|
@@ -168,17 +168,53 @@ export function canonScope(config) {
|
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/** The conditions the import below resolves with: `node` and `import`, plus `default` as exports' terminal fallback. */
|
|
172
|
+
const IMPORT_CONDITIONS = new Set(['node', 'import', 'default']);
|
|
173
|
+
|
|
174
|
+
/** The first import target of an exports entry: a string, a fallback array, or a conditions object matched in its own key order. */
|
|
175
|
+
function exportsTarget(entry) {
|
|
176
|
+
if (typeof entry === 'string') return entry;
|
|
177
|
+
if (Array.isArray(entry)) {
|
|
178
|
+
for (const item of entry) {
|
|
179
|
+
const target = exportsTarget(item);
|
|
180
|
+
if (target !== null) return target;
|
|
181
|
+
}
|
|
182
|
+
} else if (entry !== null && typeof entry === 'object') {
|
|
183
|
+
for (const [condition, target] of Object.entries(entry)) {
|
|
184
|
+
if (IMPORT_CONDITIONS.has(condition)) {
|
|
185
|
+
const resolved = exportsTarget(target);
|
|
186
|
+
if (resolved !== null) return resolved;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The import entry of a package found by the app's own node_modules lookup (the folders require.resolve.paths lists from cwd). */
|
|
194
|
+
function importEntry(require, packageName) {
|
|
195
|
+
for (const dir of require.resolve.paths(packageName) ?? []) {
|
|
196
|
+
const manifest = path.join(dir, packageName, 'package.json');
|
|
197
|
+
if (!fs.existsSync(manifest)) continue;
|
|
198
|
+
const published = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
199
|
+
if (published.exports === undefined) return path.join(dir, packageName, published.main ?? 'index.js');
|
|
200
|
+
const exports = published.exports;
|
|
201
|
+
const entry = exports !== null && typeof exports === 'object' && !Array.isArray(exports)
|
|
202
|
+
&& Object.keys(exports).some((key) => key.startsWith('.')) ? exports['.'] : exports;
|
|
203
|
+
const target = exportsTarget(entry);
|
|
204
|
+
if (target === null) throw new Error(`no "exports" import target for "." in ${manifest}`);
|
|
205
|
+
const file = path.join(dir, packageName, target);
|
|
206
|
+
if (!fs.existsSync(file)) throw new Error(`the "exports" import target of ${manifest} does not exist: ${target}`);
|
|
207
|
+
return file;
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Cannot find package '${packageName}'`);
|
|
210
|
+
}
|
|
211
|
+
|
|
171
212
|
/** Resolve one canon from this app's install; a package from an enclosing repository cannot prove its lint contract. */
|
|
172
|
-
function ownCanonEntry({ repoRoot, cwd }, packageName) {
|
|
213
|
+
export function ownCanonEntry({ repoRoot, cwd }, packageName) {
|
|
173
214
|
const require = createRequire(path.join(cwd, 'package.json'));
|
|
174
|
-
// The entry is imported below, so
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
? { ...context, conditions: [...new Set([...context.conditions.filter(condition => condition !== 'require'), 'import'])] }
|
|
178
|
-
: context) });
|
|
179
|
-
let entry;
|
|
180
|
-
try { entry = require.resolve(packageName); }
|
|
181
|
-
finally { hook.deregister(); }
|
|
215
|
+
// The entry is imported below, so the package's import condition applies; require.resolve cannot be told so on every
|
|
216
|
+
// supported Node (its registerHooks resolve hook skips require.resolve before Node 24), so "." is matched directly.
|
|
217
|
+
const entry = importEntry(require, packageName);
|
|
182
218
|
// The canon must be the app's own install: resolved inside the app root, or inside the app's node_modules when that folder is a link to the install
|
|
183
219
|
// (the runtime's examples lint in a sealed copy whose node_modules links the example's own install, scripts/hfs/example-secrets.mjs).
|
|
184
220
|
const ownInstall = [repoRoot, path.join(repoRoot, 'node_modules')].filter((dir) => fs.existsSync(dir)).map((dir) => fs.realpathSync(dir));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.46",
|
|
4
4
|
"description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"smol-toml": "1.9.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
+
"@typescript-eslint/eslint-plugin": "8.71.0",
|
|
20
|
+
"@typescript-eslint/parser": "8.71.0",
|
|
19
21
|
"@graphql-codegen/cli": "7.4.6",
|
|
20
22
|
"@graphql-codegen/client-preset": "6.2.3",
|
|
21
23
|
"@nestjs/common": "11.2.5",
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
"@nestjs/graphql": "13.4.5",
|
|
24
26
|
"@nestjs/testing": "11.2.5",
|
|
25
27
|
"class-validator": "0.15.1",
|
|
28
|
+
"eslint": "10.11.0",
|
|
26
29
|
"graphql": "16.14.2",
|
|
27
30
|
"prettier": "3.9.6",
|
|
28
31
|
"reflect-metadata": "0.2.2",
|
package/release/candidate-ci.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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
4
|
// green the newest non-cancelled run of the sha concluded success and none of its jobs was skipped
|
|
5
|
-
// red
|
|
6
|
-
// timeout
|
|
7
|
-
// unknown
|
|
5
|
+
// red "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 "candidate CI timeout": no run listed within the grace (a queued run is listed), or it did not end within the wait
|
|
7
|
+
// unknown candidate CI unknown: GitHub could not be read to the end; a release never proceeds blind
|
|
8
8
|
// There is no override: a red candidate never passes (owner ruling 2026-10-22: a loosening is always red).
|
|
9
9
|
// Every process goes through ctx.spawn (git, gh); the wait through ctx.sleep and ctx.now.
|
|
10
10
|
|
|
@@ -95,9 +95,9 @@ export async function candidateStep(ctx, { head, tag, prefix, remote, wait = CAN
|
|
|
95
95
|
if (seen.state === 'red') {
|
|
96
96
|
const failing = failingJobs(ctx, seen.run);
|
|
97
97
|
const detail = failing.length ? failing.join('; ') : 'the failing jobs could not be read';
|
|
98
|
-
return { ok: false, lines: [...lines, `
|
|
98
|
+
return { ok: false, lines: [...lines, `candidate CI red: the ci.yml run of ${head.slice(0, 10)} on ${ref} is red${url}; failing: ${detail}. Nothing reached the default branch; the candidate ref stays for diagnosis`], data: { ref, state: 'red', run: seen.run, failing } };
|
|
99
99
|
}
|
|
100
|
-
if (seen.state === 'unstarted') return { ok: false, lines: [...lines, `
|
|
101
|
-
if (seen.state === 'timeout') return { ok: false, lines: [...lines, `
|
|
102
|
-
return { ok: false, lines: [...lines, `
|
|
100
|
+
if (seen.state === 'unstarted') return { ok: false, lines: [...lines, `candidate CI timeout: no ci.yml run started for ${head.slice(0, 10)} on ${ref} within ${wait.graceMinutes} minutes (does .github/workflows/ci.yml trigger on release/candidate-*? run starci app sync)`], data: { ref, state: 'unstarted' } };
|
|
101
|
+
if (seen.state === 'timeout') return { ok: false, lines: [...lines, `candidate CI timeout: the ci.yml run of ${head.slice(0, 10)} did not end within ${wait.waitMinutes} minutes${url}`], data: { ref, state: 'timeout', run: seen.run } };
|
|
102
|
+
return { ok: false, lines: [...lines, `candidate CI unknown: GitHub could not be read for the candidate ${head.slice(0, 10)} on ${ref}; a release never proceeds blind`], data: { ref, state: 'unknown' } };
|
|
103
103
|
}
|
package/release/push.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// push.mjs - `starci app release push`: sends the default branch and the release tag to the remote in ONE atomic push through the repository's own
|
|
2
|
-
// pre-push hook (never --no-verify).
|
|
2
|
+
// pre-push hook (never --no-verify). The explicit release command requires an annotated release tag on HEAD and the L4 record of the gate for that
|
|
3
3
|
// head and tag. `git push --dry-run --atomic` always runs first (the hook's verdict is the proof) and any failure refuses the push.
|
|
4
4
|
// With --execute the exact sha first goes to its candidate ref refs/heads/release/candidate-<version> and the push waits for the ci.yml run of that sha
|
|
5
|
-
// (candidate-ci.mjs, OWNER RULING candidate-ci 2026-10-18): only a green candidate
|
|
5
|
+
// (candidate-ci.mjs, OWNER RULING candidate-ci 2026-10-18): only a green candidate moves the branch and the tag.
|
|
6
6
|
// That wait runs only for an app that sets release.requireCi: true; the default false (owner ruling release-require-ci-per-project) pushes after the
|
|
7
7
|
// local gate and the dry run alone and says so. The tag, the L4 record and the dry run are never skipped.
|
|
8
8
|
import fs from 'node:fs';
|
|
@@ -21,7 +21,7 @@ function readRecord(file) {
|
|
|
21
21
|
const greenRecord = (record, head, tag) => record?.head === head && record?.tag === tag
|
|
22
22
|
&& Array.isArray(record.gates) && record.gates.length > 0 && record.gates.every((gate) => gate?.pass === true);
|
|
23
23
|
|
|
24
|
-
/** The release gate
|
|
24
|
+
/** The explicit release command gate: the annotated tag on HEAD whose L4 record names head and tag. Returns { tag } or { reason }. */
|
|
25
25
|
function gateState(ctx, head, prefix, expected) {
|
|
26
26
|
const tags = annotatedOnHead(ctx, prefix);
|
|
27
27
|
if (!tags.includes(expected)) return { reason: `no ANNOTATED tag ${expected} (the package.json version) on HEAD${tags.length > 0 ? `, which carries ${tags.join(', ')}` : ''}: run starci app release gate --execute first` };
|
|
@@ -38,8 +38,9 @@ parallel: {gear: 1}
|
|
|
38
38
|
# kernel (docs/supervisor.md) also takes, optional, defaults in home.mjs DEFAULTS:
|
|
39
39
|
# kernel: {agent, model, effort} its agent pin (default: the kernel pin below)
|
|
40
40
|
# workers: {base, max} the adaptive [Worker] cap, 0 to 10 (0 = the Supervisor runs no fix worker)
|
|
41
|
-
# landGate: {mode: shared}
|
|
42
|
-
# fast-forwards LOCAL main
|
|
41
|
+
# landGate: {mode: shared, push: true} shared lets running lanes commit directly; exclusive = gate only. A land
|
|
42
|
+
# fast-forwards LOCAL main; main reaches the remote right after a green scoped check through
|
|
43
|
+
# `starci release push` (push: true, the default; false locks the remote). CI is the critic.
|
|
43
44
|
supervisor:
|
|
44
45
|
# mode: chat (default) - the owner's desktop chat session is the Supervisor: it owns channel 'main'
|
|
45
46
|
# and reads Decision Items; nothing starts a [Supervisor] kernel. kernel - the optional [Supervisor]
|
|
@@ -49,7 +50,7 @@ supervisor:
|
|
|
49
50
|
pollIntervalMs: null
|
|
50
51
|
repos: []
|
|
51
52
|
# frozenMinutes: <n> # Host seat check: a busy frame unchanged this long is frozen, not busy
|
|
52
|
-
# landGate: {mode: shared|exclusive}
|
|
53
|
+
# landGate: {mode: shared|exclusive, push: true|false}
|
|
53
54
|
# delegation — while the owner is away, a named delegate (e.g. the supervisor chat) may
|
|
54
55
|
# answer owner asks until the given time; excluded classes stay owner-only. null = none.
|
|
55
56
|
# delegation: {asks: supervisor, until: 2026-09-23T09:00:00+07:00,
|
|
@@ -185,11 +186,9 @@ sonar: {organization: null}
|
|
|
185
186
|
# minFreeRamPct free-RAM percentage below which no new heavy op starts
|
|
186
187
|
# set them only in the gitignored config.yaml copy, e.g.
|
|
187
188
|
# resources: {minFreeDiskGb: 5, minFreeDiskPct: 1, minFreeRamPct: 10}
|
|
188
|
-
# release - where the
|
|
189
|
-
# suite: local the shipped default: `starci release cut` runs the
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# A release can reach GitHub and its tag before the Linux parity has run; a red CI is fixed forward with the next pre-release (starci release ci-status).
|
|
194
|
-
# "none" is refused: the Linux parity does not vanish, it moves to CI. Set it only in the gitignored config.yaml copy: release: {suite: ci}
|
|
189
|
+
# release - where the full suite of a release is judged (scripts/guards/release-suite-mode.mjs; docs/releasing.md "Where the suite runs"). One key:
|
|
190
|
+
# suite: local the shipped default: `starci release cut` also runs the L4 rows on this host (root rows, example apps, Sonar proofs, Linux parity container) before it tags; it never gates a push
|
|
191
|
+
# suite: ci the owner's recorded choice (2026-10-09, widened 2026-10-26 by push-main-after-scoped-check): EVERY row is judged by the GitHub workflows (ci, examples, sonar) on each push to main;
|
|
192
|
+
# the cut runs none and lists each row as delegated, never green. A red CI opens the next alpha.N.M and never blocks the next push (starci release ci-status).
|
|
193
|
+
# "none" is refused: the suite does not vanish, it moves to CI. Set it only in the gitignored config.yaml copy: release: {suite: ci}
|
|
195
194
|
release: {suite: local}
|
|
@@ -294,13 +294,13 @@ const validateSupervisorBlock=(supervisor,profile)=>{
|
|
|
294
294
|
throw new Error('Invalid config.yaml: supervisor.mode must be chat or kernel, or null.');
|
|
295
295
|
// The [Supervisor] kernel seat (scripts/machine/home.mjs supervisorSettings): kernel {agent?, model?, effort?}
|
|
296
296
|
// pins its agent (default: the kernel pin), workers {base?, max?} its adaptive [Worker] cap (0 to 10; the shipped default is 0 fix workers),
|
|
297
|
-
// landGate {mode?: shared|exclusive} the land gate (scripts/supervisor/land.mjs).
|
|
297
|
+
// landGate {mode?: shared|exclusive, push?: boolean} the land gate (scripts/supervisor/land.mjs); push (default true, owner ruling push-main-after-scoped-check) lets `starci release push` send main to the remote after a green scoped check, false is the owner's lock on the remote.
|
|
298
298
|
const seat=supervisor.kernel,workers=supervisor.workers,gate=supervisor.landGate;
|
|
299
299
|
if(seat!==undefined&&seat!==null)validateAgentSeat(seat,'supervisor.kernel',profile);
|
|
300
300
|
if(!(workers===undefined||workers===null||(plain(workers)&&Object.keys(workers).every(key=>['base','max'].includes(key)&&Number.isInteger(workers[key])&&workers[key]>=0&&workers[key]<=10))))
|
|
301
301
|
throw new Error('Invalid config.yaml: supervisor.workers must be {base?, max?} integers from 0 to 10, or null.');
|
|
302
|
-
if(!(gate===undefined||gate===null||(plain(gate)&&Object.keys(gate).every(key=>key==='mode'&&['shared','exclusive'].includes(gate.mode)))))
|
|
303
|
-
throw new Error('Invalid config.yaml: supervisor.landGate must be {mode?: shared|exclusive}, or null.');
|
|
302
|
+
if(!(gate===undefined||gate===null||(plain(gate)&&Object.keys(gate).every(key=>(key==='mode'&&['shared','exclusive'].includes(gate.mode))||(key==='push'&&typeof gate.push==='boolean')))))
|
|
303
|
+
throw new Error('Invalid config.yaml: supervisor.landGate must be {mode?: shared|exclusive, push?: boolean}, or null.');
|
|
304
304
|
// stallMinutes: scripts/supervisor/stall.mjs calls a running workflow STALLED after this many minutes with no progress.
|
|
305
305
|
// frozenMinutes: scripts/supervisor/supervisor-watchdog.mjs reads a busy seat frame with no turn progress for this long as frozen.
|
|
306
306
|
const frozen=supervisor.frozenMinutes;
|
|
@@ -3,14 +3,14 @@ import {invalid} from './invalid-config.mjs';
|
|
|
3
3
|
|
|
4
4
|
/** The keys of the release block. */
|
|
5
5
|
export const RELEASE_KEYS=Object.freeze(['suite']);
|
|
6
|
-
/** Where the full test suite of a release is judged: `local` (the release cut runs it, the shipped default) or `ci` (the GitHub
|
|
6
|
+
/** Where the full test suite of a release is judged: `local` (the release cut also runs the L4 rows before it tags, the shipped default) or `ci` (the GitHub workflows judge EVERY row on each push to main; the cut runs none). */
|
|
7
7
|
export const SUITE_MODES=Object.freeze(['local','ci']);
|
|
8
|
-
|
|
8
|
+
const RELEASE_DEFAULTS=Object.freeze({suite:'local'});
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* config.yaml `release` - {suite?: local | ci}: who judges the full suite of a release (docs/releasing.md "Where the suite runs"). `local` is the shipped
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* config.yaml `release` - {suite?: local | ci}: who judges the full suite of a release (docs/releasing.md "Where the suite runs"). `local` is the shipped default: `starci release cut` also runs the
|
|
12
|
+
* L4 rows on this host before it tags. `ci` is the owner's recorded choice (2026-10-09, widened 2026-10-26 by push-main-after-scoped-check): the full suite runs on GitHub only, on every push
|
|
13
|
+
* to main; the cut runs no row and the verdict is read with `starci release ci-status`. The push of main needs neither mode: it needs a green scoped check.
|
|
14
14
|
* `none` is refused: the suite does not vanish, it moves to CI, and the value says so.
|
|
15
15
|
*/
|
|
16
16
|
export function validateRelease(release){
|
|
@@ -253,7 +253,7 @@ guidance:
|
|
|
253
253
|
a family where selection, current and focus share the same pink.
|
|
254
254
|
- id: big-touch-targets
|
|
255
255
|
requirement: |
|
|
256
|
-
Keep the published sizes: Common's
|
|
256
|
+
Keep the published sizes: Common's 40px control height for controls, the Subnav toggle and the TopBar
|
|
257
257
|
trigger, and `size="control"` throughout the navbar. Do not shrink them with app classes,
|
|
258
258
|
and put answer choices in RadioGroup/CheckboxGroup/SegmentedControl rather than small
|
|
259
259
|
custom chips.
|
|
@@ -185,9 +185,19 @@ shapes:
|
|
|
185
185
|
- id: navbar-controls
|
|
186
186
|
implements: [FE-APP-SHELL-2]
|
|
187
187
|
compose: |
|
|
188
|
-
Every bar control uses `size="control"`, the 2.
|
|
188
|
+
Every bar control uses `size="control"`, the 2.5rem (40px) shared height. Icon uses the family
|
|
189
189
|
size-5 token (20px) throughout the region. SearchField or its trigger shows Ctrl K and opens
|
|
190
|
-
on Ctrl/Cmd+K; inside a surface its variant resolves through the nesting context.
|
|
190
|
+
on Ctrl/Cmd+K; inside a surface its variant resolves through the nesting context. A compact
|
|
191
|
+
language switch in a bar is SegmentedControl size="control" (the HeroUI lg group held to
|
|
192
|
+
2.75rem); md and lg keep HeroUI's own sizes.
|
|
193
|
+
- id: site-chrome-bar
|
|
194
|
+
implements: [FE-APP-SHELL-3]
|
|
195
|
+
compose: |
|
|
196
|
+
A public site composes its bar inside the flow shell, in one module (the scaffold SiteShell):
|
|
197
|
+
WorkspaceShell viewport="flow" header={TopBar brand navigation actions}. navigation is a
|
|
198
|
+
NavLandmark layout="bar" of TextAction links; actions is the language SegmentedControl
|
|
199
|
+
size="control", then the sign-in Button intent="neutral" and the call-to-action Button, all
|
|
200
|
+
size="control". The app right cluster (menu, theme Switch, bell, account) is for the app shell only.
|
|
191
201
|
- id: workspace-identity
|
|
192
202
|
implements: [FE-APP-SHELL-2]
|
|
193
203
|
compose: |
|
|
@@ -240,7 +250,7 @@ shapes:
|
|
|
240
250
|
scrolls independently. Groups use translated headings such as Main, Operations and Settings
|
|
241
251
|
from modules/i18n/messages/app-shell.yaml. The expanded item has a size-5 icon, label and
|
|
242
252
|
Grammar current-page treatment. A keyboard-accessible toggle collapses it into icon-only
|
|
243
|
-
destinations with size-5 icons in 2.
|
|
253
|
+
destinations with size-5 icons in 2.5rem targets, current-page highlighting, count badges
|
|
244
254
|
and hover/focus tooltips. Group order is stable, spacing replaces group headings and
|
|
245
255
|
settings-type groups stay at the end. The application persists collapse state per user.
|
|
246
256
|
- id: conversation-panel
|
|
@@ -323,6 +333,20 @@ imageryClarification:
|
|
|
323
333
|
library-edit scope. Historical image notes are design references, not decisions for the current
|
|
324
334
|
product.
|
|
325
335
|
guidance:
|
|
336
|
+
- id: owner-ui-rulings-alpha-31
|
|
337
|
+
ownership: FE-RULE-OWNERSHIP
|
|
338
|
+
implements: [COLLECTION-1, LAYOUT-1, SURFACE-1, ACTION-3, CTA-6]
|
|
339
|
+
requirement: |
|
|
340
|
+
The canonical cases own the rule bodies; this entry only points to them. COLLECTION-1 case-9
|
|
341
|
+
(owner:selection-fill-no-check): navigation-like single selection uses ListBox
|
|
342
|
+
selectionIndicator="none", with the selectedKeys and accessibility contract unchanged.
|
|
343
|
+
LAYOUT-1 case-10 (owner:joined-multi-pane): the selected family realizes one touching pane
|
|
344
|
+
surface with vertical Dividers. SURFACE-1 case-4 (owner:scroll-region-background-tone):
|
|
345
|
+
VerticalScrollRegion tone="background" under messages and log entries, with the header and
|
|
346
|
+
composer on the surface. ACTION-3 case-5 (owner:hover-actions-floating-toolbar): ChatBubble's
|
|
347
|
+
public floating toolbar owns the IconButtons and tooltips beside the text; apps do not target
|
|
348
|
+
starci-core internals. CTA-6 case-4 (owner:button-variant-follows-intent): ordinary repeated
|
|
349
|
+
item commands are neutral and render ghost.
|
|
326
350
|
- id: nested-controls-and-panel-lists
|
|
327
351
|
ownership: FE-RULE-OWNERSHIP
|
|
328
352
|
implements: [ANATOMY-2, PADDING-0]
|
|
@@ -22,36 +22,43 @@ pins:
|
|
|
22
22
|
side: fe
|
|
23
23
|
source: packages/heroicons/package.json
|
|
24
24
|
why: 'StarCi custom Heroicons-compatible cuts; eslint-grammar-starci (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
|
|
25
|
+
'@starci/event-stream':
|
|
26
|
+
version: 0.1.0
|
|
27
|
+
group: starci
|
|
28
|
+
install: registry
|
|
29
|
+
side: fe
|
|
30
|
+
source: packages/event-stream/package.json
|
|
31
|
+
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
25
32
|
'@starci/grammar':
|
|
26
|
-
version: 0.12.
|
|
33
|
+
version: 0.12.13
|
|
27
34
|
group: starci
|
|
28
35
|
install: registry
|
|
29
36
|
side: fe
|
|
30
37
|
source: packages/grammar/package.json
|
|
31
38
|
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.'
|
|
32
39
|
'@starci/eslint-canon-be':
|
|
33
|
-
version: 3.4.
|
|
40
|
+
version: 3.4.55
|
|
34
41
|
group: starci
|
|
35
42
|
install: registry
|
|
36
43
|
side: be
|
|
37
44
|
source: packages/eslint/be/package.json
|
|
38
45
|
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.'
|
|
39
46
|
'@starci/eslint-canon-fe':
|
|
40
|
-
version: 9.1.
|
|
47
|
+
version: 9.1.9
|
|
41
48
|
group: starci
|
|
42
49
|
install: registry
|
|
43
50
|
side: fe
|
|
44
51
|
source: packages/eslint/fe/package.json
|
|
45
52
|
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.'
|
|
46
53
|
'@starci/eslint-grammar-starci':
|
|
47
|
-
version: 0.12.
|
|
54
|
+
version: 0.12.13
|
|
48
55
|
group: starci
|
|
49
56
|
install: registry
|
|
50
57
|
side: fe
|
|
51
58
|
source: packages/eslint/grammar-starci/package.json
|
|
52
59
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
53
60
|
'@starci/eslint-grammar-offset-pop':
|
|
54
|
-
version: 0.12.
|
|
61
|
+
version: 0.12.13
|
|
55
62
|
group: starci
|
|
56
63
|
install: registry
|
|
57
64
|
side: fe
|
|
@@ -91,14 +98,14 @@ pins:
|
|
|
91
98
|
source: packages/test-world/package.json
|
|
92
99
|
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.'
|
|
93
100
|
'@starci/cli':
|
|
94
|
-
version: 1.7.
|
|
101
|
+
version: 1.7.46
|
|
95
102
|
group: starci
|
|
96
103
|
install: registry
|
|
97
104
|
side: both
|
|
98
105
|
source: packages/cli/package.json
|
|
99
106
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
100
107
|
'@starci/hfs':
|
|
101
|
-
version: 4.7.
|
|
108
|
+
version: 4.7.46
|
|
102
109
|
group: starci
|
|
103
110
|
install: registry
|
|
104
111
|
side: both
|
|
@@ -34,7 +34,7 @@ heap:
|
|
|
34
34
|
items:
|
|
35
35
|
- id: candidate-trigger
|
|
36
36
|
title: the release CI runs on the release candidate ref
|
|
37
|
-
requires: "ci.yml runs on push of branches ['release/candidate-*'] and tags ['v*'] (and workflow_dispatch); the release flows wait for the run of the release sha there"
|
|
37
|
+
requires: "ci.yml runs on push of branches ['main', 'release/candidate-*'] and tags ['v*'] (and workflow_dispatch); the release flows wait for the run of the release sha there"
|
|
38
38
|
covers: [app-ci, runtime]
|
|
39
39
|
by: [R221 CI_TRIGGERS_RELEASE_ONLY, R251 HFS_APP_WORKFLOW_TRIGGER]
|
|
40
40
|
- id: job-shape
|
|
@@ -82,7 +82,18 @@ concerns:
|
|
|
82
82
|
home: [fe.hooks.socketio]
|
|
83
83
|
pattern: FE-REALTIME
|
|
84
84
|
forbids: [subscriptions-transport-ws, '@supabase/realtime-js', pusher-js, ably]
|
|
85
|
-
why: one socket.io-client singleton per namespace and one side-effect component that dispatches to redux and SWR; both editions run the gateway in the be api process. A one-way channel served as server-sent events
|
|
85
|
+
why: one socket.io-client singleton per namespace and one side-effect component that dispatches to redux and SWR; both editions run the gateway in the be api process. A one-way channel served as server-sent events is the event-stream concern below (FE-REALTIME).
|
|
86
|
+
- id: event-stream
|
|
87
|
+
tier: opt
|
|
88
|
+
packages: ['@starci/event-stream']
|
|
89
|
+
home: [fe.transport.stream]
|
|
90
|
+
pattern: FE-REALTIME
|
|
91
|
+
forbids: ['@microsoft/fetch-event-source', eventsource, event-source-polyfill, eventsource-parser, reconnecting-eventsource, 'sse.js', 'fetch-sse', 'sse-ts']
|
|
92
|
+
why: >-
|
|
93
|
+
the one server-sent-events client (owner:event-stream-package): the app creates one instance in modules/api/sse/clients/event-stream.ts
|
|
94
|
+
with createEventStream (the bearer token source, one renewal on a 401, backoff, heartbeat timeout), each stream operation
|
|
95
|
+
(sse/<area>/stream-<channel>.ts) follows on it, and the useSubscribe<Channel>Swr hook reads that operation. No app keeps a
|
|
96
|
+
hand-written reader (read-event-stream.ts) and no second SSE library is installed.
|
|
86
97
|
- id: auth
|
|
87
98
|
tier: core
|
|
88
99
|
packages: []
|
|
@@ -20,7 +20,7 @@ schema: starci/hfs-rules@3
|
|
|
20
20
|
version: 3.1.0
|
|
21
21
|
gates:
|
|
22
22
|
pre-commit: the managed L0 hook runs app hygiene, managed-file drift, ESLint per side and stylelint on staged source, and prettier on staged files; typecheck and targeted specs belong to the op gate under R225
|
|
23
|
-
pre-push: the managed app hook
|
|
23
|
+
pre-push: the managed app hook invokes the bundled push policy under R225; it starts no source check, build or test
|
|
24
24
|
settle: the op gate `starci gate run` over the op's changed files (merge guard, starci app lint --changed, codegen, tsc, the slice's specs; only findings new against the workflow's previous checkpoint block), re-read at starci kernel settle, which refuses a red done; a green settle commits the op's checkpoint on wf-<workflowId>
|
|
25
25
|
land: the workflow's finish, the only time main is touched - gate.mjs over the whole workflow branch against its merge-base with main, the merge guard, review.verify of the exact head that lands, the branch rebased onto main, then main fast-forwarded and pushed
|
|
26
26
|
ci: GitHub Actions runs the same pinned starci app lint, format:check, typecheck, unit with the per-file coverage threshold, build:be and build:fe, then the Sonar scan and gate
|
|
@@ -3850,7 +3850,7 @@ rules:
|
|
|
3850
3850
|
- {path: packages/hfs/src/hfs-supabase-secrets.spec.mjs, edition: lite}
|
|
3851
3851
|
- id: R221
|
|
3852
3852
|
code: "CI_TRIGGERS_RELEASE_ONLY"
|
|
3853
|
-
law: "
|
|
3853
|
+
law: "Workflow triggers are push filtered to v* tags or release/candidate-* branches, and workflow_dispatch. Runtime workflows and app ci.yml also admit main. App ci.yml includes the candidate branch filter for starci app release push. Other branches, events, path filters and ignore filters are refused."
|
|
3854
3854
|
scope: runtime
|
|
3855
3855
|
editions: [lite, full]
|
|
3856
3856
|
docs: [docs/git-governance.md#r221]
|
|
@@ -3894,7 +3894,7 @@ rules:
|
|
|
3894
3894
|
- {kind: runtime, id: protected-zone, at: scripts/hfs/runtime-rules/rights-policy.mjs}
|
|
3895
3895
|
- id: R225
|
|
3896
3896
|
code: "RT_HOOK_SHAPE"
|
|
3897
|
-
law: "The app hook templates keep the gate model: pre-commit is L0 only (no typecheck
|
|
3897
|
+
law: "The app hook templates keep the gate model: pre-commit is L0 only (no typecheck or test run); pre-push invokes the bundled scripts/guards/release-push-gate.mjs policy for fast-forward main, a clean outgoing secret scan and annotated v* tags. It runs no build, test, typecheck or lint."
|
|
3898
3898
|
scope: runtime
|
|
3899
3899
|
kinds: [check]
|
|
3900
3900
|
gates: [land, runtime]
|
|
@@ -4269,7 +4269,7 @@ rules:
|
|
|
4269
4269
|
editions: [lite, full]
|
|
4270
4270
|
docs:
|
|
4271
4271
|
- {path: docs/application-stacks.md#r251-deploy-workflow, editions: [lite, full]}
|
|
4272
|
-
law: "In lite and full apps, deployment jobs declare a protected environment, explicit permissions and concurrency, materialize sealed secrets through SOPS_AGE_KEY or starci app secret materialize, and reference only the age key and transport SSH key/known_hosts secrets. They never embed a dotenv blob or write a dotenv file. Every remote workflow action uses a 40-hex commit SHA (local actions are allowed and container actions require a full sha256 digest); supply-chain reports each unpinned uses reference at its source line, including disabled jobs and steps; installed tools are checksum or digest verified, or supplied by a pinned action. App workflows start only on release tags [v*], the release candidate branches release/candidate-* (required in ci.yml: starci app release push waits for that run, owner ruling candidate-ci 2026-10-18) or workflow_dispatch
|
|
4272
|
+
law: "In lite and full apps, deployment jobs declare a protected environment, explicit permissions and concurrency, materialize sealed secrets through SOPS_AGE_KEY or starci app secret materialize, and reference only the age key and transport SSH key/known_hosts secrets. They never embed a dotenv blob or write a dotenv file. Every remote workflow action uses a 40-hex commit SHA (local actions are allowed and container actions require a full sha256 digest); supply-chain reports each unpinned uses reference at its source line, including disabled jobs and steps; installed tools are checksum or digest verified, or supplied by a pinned action. App workflows start only on release tags [v*], the release candidate branches release/candidate-* (required in ci.yml: starci app release push waits for that run, owner ruling candidate-ci 2026-10-18) or workflow_dispatch; ci.yml also runs on main, and other branch pushes are refused."
|
|
4273
4273
|
kinds: [check]
|
|
4274
4274
|
gates: [settle, land, ci]
|
|
4275
4275
|
failureCodes: ["HFS_DEPLOY_WORKFLOW", "HFS_WORKFLOW_PIN", "HFS_CI_TOOL_INSTALL", "HFS_APP_WORKFLOW_TRIGGER"]
|
|
@@ -5576,23 +5576,32 @@ rules:
|
|
|
5576
5576
|
templateNa: "The app composes its shell from product destinations and identity; full and lite example shells provide the reference."
|
|
5577
5577
|
- id: R318
|
|
5578
5578
|
code: "FE_NESTED_VARIANT"
|
|
5579
|
-
law: "Apps compose Grammar with layout-only CSS; paint utilities, arbitrary paint properties and inline style paint belong to Grammar. Grammar context owns all layer-aware field variants, nested Tabs defaults and the content edge of panel lists; apps omit static field variants, do not force primary Tabs inside a painted surface, do not paint raw surfaces, and add no horizontal list-wrapper padding within the nearest painted Grammar surface."
|
|
5579
|
+
law: "Apps compose Grammar with layout-only CSS; paint utilities, arbitrary paint properties and inline style paint belong to Grammar. Grammar context owns all layer-aware field variants, nested Tabs defaults and the content edge of panel lists; apps omit static field variants, do not force primary Tabs inside a painted surface, do not paint raw surfaces, and add no horizontal list-wrapper padding within the nearest painted Grammar surface. BOUNDARY-1 case-5 owns Divider full-bleed composition. Navigation-like single-select lists use fill without checks (COLLECTION-1 case-9); application styles never target Grammar internal classes (ACTION-3 case-5)."
|
|
5580
5580
|
kinds: [lint, design]
|
|
5581
5581
|
gates: [pre-commit, settle, land, ci]
|
|
5582
|
-
failureCodes: ["FE_NESTED_VARIANT", "FE_LIST_PANEL_PADDING", "FE_GRAMMAR_LAYOUT_ONLY"]
|
|
5582
|
+
failureCodes: ["FE_NESTED_VARIANT", "FE_LIST_PANEL_PADDING", "FE_GRAMMAR_LAYOUT_ONLY", "FE_DIVIDER_FULL_BLEED"]
|
|
5583
5583
|
enforcers:
|
|
5584
5584
|
- {kind: eslint-fe, id: nested-control-variant}
|
|
5585
5585
|
- {kind: eslint-fe, id: no-list-panel-padding}
|
|
5586
5586
|
- {kind: eslint-fe, id: no-app-surface-paint}
|
|
5587
|
+
- {kind: eslint-fe, id: divider-full-bleed}
|
|
5588
|
+
- {kind: eslint-fe, id: selection-fill-no-check}
|
|
5589
|
+
- {kind: eslint-fe, id: no-grammar-internal-selector}
|
|
5587
5590
|
editions: [lite, full]
|
|
5588
5591
|
docs: ["docs/code-pattern-enforcement.md#r318-app-grammar-composition"]
|
|
5589
5592
|
templateNa: "Product composition is proved by the FE canon's Grammar import and surface fixtures; a template cannot choose product slots or lists."
|
|
5590
5593
|
proves:
|
|
5594
|
+
- {path: packages/eslint/fe/divider-full-bleed.spec.mjs, editions: [lite, full], codes: [FE_DIVIDER_FULL_BLEED]}
|
|
5591
5595
|
- {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
|
|
5596
|
+
- {path: packages/eslint/fe/ui-rulings.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_GRAMMAR_LAYOUT_ONLY]}
|
|
5592
5597
|
exemplifiedBy:
|
|
5598
|
+
- {path: packages/eslint/fe/divider-full-bleed.spec.mjs, editions: [lite, full], codes: [FE_DIVIDER_FULL_BLEED]}
|
|
5593
5599
|
- {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
|
|
5600
|
+
- {path: packages/eslint/fe/ui-rulings.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_GRAMMAR_LAYOUT_ONLY]}
|
|
5594
5601
|
witness:
|
|
5602
|
+
- {path: packages/eslint/fe/divider-full-bleed.spec.mjs, editions: [lite, full], codes: [FE_DIVIDER_FULL_BLEED]}
|
|
5595
5603
|
- {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
|
|
5604
|
+
- {path: packages/eslint/fe/ui-rulings.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_GRAMMAR_LAYOUT_ONLY]}
|
|
5596
5605
|
cases:
|
|
5597
5606
|
- id: case-1
|
|
5598
5607
|
date: "2026-10-25"
|
|
@@ -5629,3 +5638,29 @@ rules:
|
|
|
5629
5638
|
source: owner
|
|
5630
5639
|
quote: "Critique this carefully so the button choice is a system and not a feeling, understand? Update .claude."
|
|
5631
5640
|
ruling: "The button variant follows the declared intent (owner ruling button-variant-follows-intent): the app passes intent and step, Grammar derives the variant from them and from the placement it reads from React context. The semantics are HeroUI's, so the lint is universal in the FE canon."
|
|
5641
|
+
|
|
5642
|
+
- id: R320
|
|
5643
|
+
code: "FE_CONTROL_HEIGHT"
|
|
5644
|
+
law: "One 40px control height is the default of every standalone control (CONTROL-HEIGHT-1): an app never picks a larger size than the default outside a landing hero call to action, and controls of one row share one size."
|
|
5645
|
+
kinds: [lint, design]
|
|
5646
|
+
gates: [pre-commit, settle, land, ci]
|
|
5647
|
+
failureCodes: ["FE_CONTROL_HEIGHT"]
|
|
5648
|
+
enforcers:
|
|
5649
|
+
- {kind: eslint-fe, id: control-height}
|
|
5650
|
+
editions: [lite, full]
|
|
5651
|
+
docs:
|
|
5652
|
+
- packages/eslint/fe/docs/control-height.md
|
|
5653
|
+
- docs/code-pattern-enforcement.md#r320-control-height
|
|
5654
|
+
proves:
|
|
5655
|
+
- {path: packages/eslint/fe/control-height.spec.mjs, editions: [lite, full], codes: [FE_CONTROL_HEIGHT]}
|
|
5656
|
+
exemplifiedBy:
|
|
5657
|
+
- {path: packages/eslint/fe/control-height.spec.mjs, editions: [lite, full], codes: [FE_CONTROL_HEIGHT]}
|
|
5658
|
+
witness:
|
|
5659
|
+
- {path: packages/eslint/fe/control-height.spec.mjs, editions: [lite, full], codes: [FE_CONTROL_HEIGHT]}
|
|
5660
|
+
templateNa: "The control height is Grammar's own value; a template states no size."
|
|
5661
|
+
cases:
|
|
5662
|
+
- id: case-1
|
|
5663
|
+
date: "2026-10-26"
|
|
5664
|
+
source: owner
|
|
5665
|
+
quote: "The size of the buttons and controls is uneven; the teacher thinks height = 40px (h-10) should be fixed for every button, switch, input, tabs and so on, what do you think? (translated from Vietnamese)"
|
|
5666
|
+
ruling: "One 40px control height is the default of every standalone control (owner ruling control-height-40): Grammar draws it, an app omits size, sm is for a nested item or dense toolbar row, lg only for a landing hero call to action, and controls of one row share one size."
|
|
@@ -305,7 +305,7 @@ ruleParams:
|
|
|
305
305
|
- {typeSuffix: Options, names: [options], nameSuffix: Options}
|
|
306
306
|
# R89 BE_SOURCE_FORM: the closed role-suffix vocabulary of a source file name (<kebab-name>.<suffix>.ts; BE-CONVENTION 1.15)
|
|
307
307
|
# and the suffixes that are never a role. index.ts, main.ts, app.module.ts and the migrations of be.persistence are the
|
|
308
|
-
# only names outside it.
|
|
308
|
+
# only names outside it. Typed realtime topic builders use the existing policy role (BE-REALTIME-3 of knowledge/patterns/be/realtime.yaml).
|
|
309
309
|
suffixes: [module, module-definition, decorators, options, config, connection, service, schema, command, query, handler, templates, compensation, saga-state, saga-step, saga, contracts, port, resolver, controller, gateway, webhook, subscription, consumer, cli, event, queue, projection, projection-entity, processor, step, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, adapter, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
|
|
310
310
|
# R47 BE_CONTRACT_UNGUARDED: the world's shape vocabulary. `helper` names the function a contract spec imports to erase the values of a JSON
|
|
311
311
|
# payload (`shapeOf`); a contract spec proves a fake's fixtures by asserting helper(real) equals helper(fixture).
|
|
@@ -1617,7 +1617,7 @@ slots:
|
|
|
1617
1617
|
requires: [index.ts]
|
|
1618
1618
|
# <name>.templates.ts holds the capability's static templates, copy and presets as TS constants (owner ruling 2026-10-13);
|
|
1619
1619
|
# user-editable ones are seed rows, large or user media live in object storage.
|
|
1620
|
-
allows: ["<capability>.module.ts", "<capability>.module-definition.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts", "*.schema.ts", "*.schema.spec.ts"]
|
|
1620
|
+
allows: ["<capability>.module.ts", "<capability>.module-definition.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts", "*.schema.ts", "*.schema.spec.ts", "*.policy.ts", "*.policy.spec.ts"]
|
|
1621
1621
|
forbids: [testing/, exceptions/, utils/, helpers/, shared/]
|
|
1622
1622
|
tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
|
|
1623
1623
|
coverage: required
|
|
@@ -2336,8 +2336,8 @@ slots:
|
|
|
2336
2336
|
# The browser's calls to the back end (owner ruling 2026-10-13, fe-stack graphql/rest/codegen, FE-API-GRAPHQL): the auth and
|
|
2337
2337
|
# no-auth Apollo clients with their link chain (graphql/clients), one file per operation (graphql/queries/query-<x>.ts,
|
|
2338
2338
|
# graphql/mutations/mutation-<x>.ts and graphql/subscriptions/subscription-<x>.ts, each with its gql document map and typed request/response in types/), the generated wire types
|
|
2339
|
-
# (graphql/__generated__), the axios calls (rest/<area>/*.ts) for uploads, presigned URLs and auth endpoints and the
|
|
2340
|
-
# server-sent-events streams (sse/<area>/stream-<channel>.ts, one operation per stream, run on the stream
|
|
2339
|
+
# (graphql/__generated__), the axios calls (rest/<area>/*.ts) for uploads, presigned URLs and auth endpoints and the
|
|
2340
|
+
# server-sent-events streams (sse/<area>/stream-<channel>.ts, one operation per stream, run on the app's @starci/event-stream instance in sse/clients). A repository has
|
|
2341
2341
|
# exactly ONE client folder and ONE GraphQLResponse union: the app's (fe.transport.client/outcome) when one app calls the back end,
|
|
2342
2342
|
# or the api package's (fe.package.api.client/outcome) when several do (FE_TRANSPORT_OWNER, machine).
|
|
2343
2343
|
requires: [index.ts]
|
|
@@ -2381,7 +2381,7 @@ slots:
|
|
|
2381
2381
|
tier: transport
|
|
2382
2382
|
allows: ["*.ts"]
|
|
2383
2383
|
tests: none
|
|
2384
|
-
why: "the
|
|
2384
|
+
why: "the event-stream client of the one app that calls the back end, the stream half of the transport client layer (FE-REALTIME): event-stream.ts creates the app's one @starci/event-stream instance (createEventStream with the bearer access token source, which opens the stream with fetch, parses the events and reopens it with Last-Event-ID after a capped, doubling wait); a stream operation (sse/<area>/stream-<channel>.ts) follows on it and a useSubscribe hook of hooks/swr reads that operation; no hand-written reader lives here (owner:event-stream-package) and only a stream operation imports this folder (FE_TRANSPORT_OWNER); a repository where several apps stream keeps one instance in each of them"
|
|
2385
2385
|
rules: [FE_TRANSPORT_OWNER]
|
|
2386
2386
|
exemplifiedBy:
|
|
2387
2387
|
- {path: packages/eslint/fe/r50-transport-owner-coverage.spec.mjs, editions: [lite, full]}
|