@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/lint/run.mjs +46 -10
  3. package/package.json +4 -1
  4. package/release/candidate-ci.mjs +7 -7
  5. package/release/push.mjs +3 -3
  6. package/runtime/config.example.yaml +9 -10
  7. package/runtime/engine/config.mjs +3 -3
  8. package/runtime/engine/release-config.mjs +5 -5
  9. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +1 -1
  10. package/runtime/knowledge/grammars/starci/playbook.yaml +27 -3
  11. package/runtime/knowledge/hfs/canon-pins.yaml +14 -7
  12. package/runtime/knowledge/hfs/ci-standard.yaml +1 -1
  13. package/runtime/knowledge/hfs/fe-stack.yaml +12 -1
  14. package/runtime/knowledge/hfs/rules.yaml +41 -6
  15. package/runtime/knowledge/hfs/slots.yaml +5 -5
  16. package/runtime/knowledge/patterns/be/realtime.yaml +72 -12
  17. package/runtime/modules/kernel/command-policy.yaml +5 -5
  18. package/runtime/modules/kernel/failure-codes.yaml +42 -20
  19. package/runtime/modules/kernel/removed-vocabulary.yaml +33 -7
  20. package/runtime/packages/cli/src/catalog.generated.mjs +90 -37
  21. package/runtime/scripts/api/git/commit-tree.mjs +5 -0
  22. package/runtime/scripts/api/git/update-ref.mjs +8 -0
  23. package/runtime/scripts/api/git/write-tree.mjs +5 -0
  24. package/runtime/scripts/guards/command-policy.mjs +4 -4
  25. package/runtime/scripts/guards/push-scan.mjs +128 -0
  26. package/runtime/scripts/guards/release-push-gate.mjs +31 -29
  27. package/runtime/scripts/hfs/architecture/index.mjs +2 -1
  28. package/runtime/scripts/hfs/architecture/type-context.mjs +38 -23
  29. package/runtime/scripts/hfs/example-secrets.mjs +61 -8
  30. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +5 -5
  31. package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +61 -0
  32. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +4 -6
  33. package/runtime/scripts/hfs/typescript-programs.mjs +30 -3
  34. package/runtime/scripts/machine/home.mjs +2 -2
  35. package/templates/app/ci-workflows/github/workflows/ci.yml +2 -2
  36. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -2
  37. package/templates/app/hooks/husky/pre-push +3 -56
  38. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +1 -3
  39. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +1 -3
  40. package/templates/be/patterns/realtime/realtime-config.mapper.ts.tpl +32 -0
  41. package/runtime/scripts/api/fs/write-json-file.mjs +0 -10
  42. package/runtime/scripts/api/git/merge-base-query.mjs +0 -5
  43. package/runtime/scripts/gates/commit-proof.mjs +0 -53
  44. package/runtime/scripts/guards/release-definition.mjs +0 -204
  45. package/runtime/scripts/guards/release-record.mjs +0 -58
  46. package/runtime/scripts/guards/release-suite-mode.mjs +0 -15
  47. 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, registerHooks } from 'node:module';
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 resolve with Node's import conditions even when a package publishes no require target.
175
- // The synchronous hook exists only for this lookup; later package resolution keeps its own conditions.
176
- const hook = registerHooks({ resolve: (specifier, context, nextResolve) => nextResolve(specifier, specifier === packageName
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.44",
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",
@@ -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 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
- // unknown RELEASE_CI_UNKNOWN: GitHub could not be read to the end; a release never proceeds blind
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, `RELEASE_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 } };
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, `RELEASE_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, `RELEASE_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, `RELEASE_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' } };
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). It requires what the hook requires: an annotated release tag on HEAD and the L4 record of the gate for that
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 (or the owner's sha-bound override record) moves the branch and the tag.
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 as the hook reads it: the annotated tag on HEAD whose L4 record names head and tag. Returns { tag } or { reason }. */
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} shared lets running lanes commit directly; exclusive = gate only. A land
42
- # fast-forwards LOCAL main only: the remote main moves with a release (starci release cut)
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 Linux parity of a release is judged (scripts/guards/release-suite-mode.mjs; docs/releasing.md "Where the suite runs"). One key:
189
- # suite: local the shipped default: `starci release cut` runs the root rows (npm run test:packages, npm run test:e2e, npm run check) and the Linux parity container before it pushes,
190
- # and the pre-push gate asks for the green root rows
191
- # suite: ci the owner's recorded choice (2026-10-09): no local Linux container; the GitHub ci workflow runs the Linux parity (and SonarCloud) AFTER the push.
192
- # The cut still runs the same root rows and the example apps; the record lists linux-parity as delegated, never green.
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 workflow runs it after the push; the cut runs only the affected specs of the release and the rows CI cannot run). */
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
- export const RELEASE_DEFAULTS=Object.freeze({suite:'local'});
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
- * default: `starci release cut` runs the root suite and the Linux parity container before it pushes. `ci` is the owner's recorded choice (2026-10-09) that
13
- * the full suite runs on GitHub only: the cut runs no root suite and no Linux container, and the CI workflow's verdict is read afterwards (`starci release ci-status`).
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 44px floor for controls, the Subnav toggle and the TopBar
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.75rem shared height. Icon uses the family
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.75rem targets, current-page highlighting, count badges
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.10
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.51
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.6
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.10
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.10
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.44
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.44
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 needs no package, only a stream reader in modules/api/sse/clients, a stream operation in modules/api/sse and a useSubscribe<Channel>Swr hook (FE-REALTIME).
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 validates an annotated release tag and the recorded L4 evidence for HEAD, permitting the release refs and backup namespace under R225; it starts no source check, build or test
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: "The only workflow triggers are a push of release tags, a push of release candidate branches `release/candidate-*`, a person dispatching it and, in the runtime's own workflows (`.github/workflows/` at the root), a push to `main`: every tracked workflow declares an `on` that holds only `push` filtered to `tags: ['v*']` and/or `branches` naming only `release/candidate-*` (and, in the runtime's own workflows, `main`; no paths or ignore filter beside them) and `workflow_dispatch`. Every `ci.yml`, and the runtime's `examples.yml`, must name `release/candidate-*`: `starci release cut` and `starci app release push` push the exact release sha there and move main and the tag only when that run is green (owner ruling candidate-ci, 2026-10-18). A push to any other branch, a pull_request, a schedule, a workflow_call or any other event refuses the workflow."
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, no test run); pre-push checks the release gate (the starci-release L4 record, refs/backup/, v[0-9] tags; the runtime's own hook is scripts/guards/release-push-gate.mjs, which judges main and v* tags against the release definition and lets every other ref through) and runs no npm test, jest, typecheck or lint."
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, never on another branch push."
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 fetch readers of
2340
- # server-sent-events streams (sse/<area>/stream-<channel>.ts, one operation per stream, run on the stream reader of sse/clients). A repository has
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 fetch reader of server-sent-events streams of the one app that calls the back end, the stream half of the transport client layer (FE-REALTIME): it opens the stream with the bearer access token, parses the events and reopens it with Last-Event-ID after a capped, doubling wait; a stream operation (sse/<area>/stream-<channel>.ts) runs on it and a useSubscribe hook of hooks/swr reads that operation; a fetch here asks for text/event-stream and only a stream operation imports the reader (FE_TRANSPORT_OWNER); a repository where several apps stream keeps one reader in each of them"
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]}