@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
@@ -7,7 +7,7 @@ purpose: |-
7
7
  (`transport/sse/<channel>.controller.ts`), a socket gateway (`<channel>.gateway.ts`) or a GraphQL subscription
8
8
  (`<x>.subscription.ts`), that READS and PUSHES only. It never writes. The push is fed by a reactor: an event consumer in
9
9
  `features/reactors/` receives the domain event and publishes to a topic of the `RealtimeHub` of `platform/realtime` (an app
10
- with no event bus lets the domain service that commits the change publish, after its commit); the door only subscribes the
10
+ with no event bus lets the domain service that commits the change publish with the hub's commit ordering, BE-REALTIME-7); the door only subscribes the
11
11
  client to a topic of that hub, scoped to the client's principal. SSE is the default door for one-way server-to-client push
12
12
  (job progress, a status that changes, streamed tokens), in lite and in full (owner decision 2026-10-24): it is plain HTTP
13
13
  behind the ingress, reconnects by itself and resumes from `Last-Event-ID`. The socket.io gateway stays for a channel that also
@@ -28,7 +28,8 @@ provenance:
28
28
  - owner decision 2026-10-24 (the lite edition may serve SSE, and SSE is the default door for one-way push; ruling:R212/case-5, amended by owner:lite-graphql-subscription-door and ruling:R212/case-7 for the GraphQL subscription door)
29
29
  limitations: |-
30
30
  Static checks prove the door's role, that the hub is the only thing injected and that a subscribed topic is built from the
31
- principal. They do not prove the topic naming is collision free across channels, that an initial snapshot is consistent with
31
+ principal. They do not prove the publisher's membership read, that a non-Postgres publish follows the domain commit,
32
+ that the topic naming is collision free across channels, that an initial snapshot is consistent with
32
33
  the stream, or the behaviour of the socket under reconnects; the three `realtime/*` scenarios prove delivery, isolation and
33
34
  cleanup on the real stack. Nothing proves the ingress settings of an SSE channel (no compression, no buffering, HTTP/2, an
34
35
  idle timeout above the heartbeat): they are reviewed against the stack files and a real stream. The memory hub is
@@ -127,8 +128,8 @@ rules:
127
128
  needs is pushed by a reactor (or by the domain service that commits the change, in an app with no event bus); the socket
128
129
  library and the pub/sub engine (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`, `graphql-subscriptions`, `graphql-ws`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`,
129
130
  and so is the heartbeat timer (`setInterval` is owned by `platform/realtime`, `infraOwners`). Because the door reads only the
130
- hub, a snapshot cannot be sent on connect from the door: the client reads the current state through a query operation first
131
- (or again on every open), and the stream carries the changes.
131
+ hub, a snapshot cannot be sent on connect from the door: the client reads the current state through a query operation that
132
+ checks resource membership first (or again on every open), and the stream carries the changes (BE-REALTIME-3).
132
133
  rationale: |-
133
134
  A door that can only touch the hub cannot change business state, so the biggest risk of a long-lived unauthenticated-looking
134
135
  connection, a write through it, is removed by type.
@@ -143,13 +144,26 @@ rules:
143
144
  - id: BE-REALTIME-3
144
145
  title: The topic is built from the principal
145
146
  kind: mandatory
146
- hfsRules: [R186, R41]
147
+ hfsRules: [R186, R41, R89, R204]
147
148
  requirement: |-
148
149
  Every `@SubscribeMessage`, `@Subscription` and `@Sse` handler takes `@CurrentPrincipal()` of `domain/identity` and builds the topic it
149
150
  passes to `hub.subscribe` from the principal's id (plus a client-supplied id only inside that prefix) with the topic
150
- builder of the owning domain contract, so one client cannot
151
- listen to another's channel. The reactor that pushes uses the same topic builder from the owning domain contract, never a
152
- hand-written string. The default-deny guards apply to the door like to any other; an SSE stream is never `@Public`. The
151
+ builder of the owning domain contract, so one client cannot listen to another's channel. A typed realtime topic builder is
152
+ a policy: it lives in `<name>.policy.ts` of slot `be.domain`, is exported through the capability's `index.ts`, and returns
153
+ `RealtimeTopic<Frame>` with the principal-prefixed `name` and the frame's narrowing `accepts` predicate. The named frame
154
+ contract lives in `*.contracts.ts`. The builder and predicate are pure functions of their inputs, with no injection, database
155
+ read or membership lookup; `policy` keeps the logic-role coverage of R204. The publisher and every door import this one
156
+ builder from the owning domain contract, never a hand-written topic string.
157
+
158
+ Resource membership is enforced on the publish side. The domain service that owns the change, or the domain service called
159
+ by a reactor's command, reads the current entitled members and fans out to one principal-prefixed topic per member. A client-supplied room
160
+ or conversation id stays inside that principal's prefix: a non-member can open only its own idle topic, which no publisher
161
+ targets. A removed member's already-open stream stays open but receives no newly published frames from the next publish onward, because
162
+ the refreshed member list no longer targets it. Frames already delivered or retained in its own topic are not revoked;
163
+ replay remains subject to the hub's bounds. Snapshot and history queries independently check current membership before
164
+ returning rows; the door injects only the hub (BE-REALTIME-2), never a membership-reading service or guard.
165
+
166
+ The default-deny guards apply to the door like to any other; an SSE stream is never `@Public`. The
153
167
  browser `EventSource` cannot send an `Authorization` header, so the front end reads the stream with `fetch` and the bearer
154
168
  access token (an operation of modules/api/sse read by a useSubscribe<Channel>Swr hook); a token in the URL is refused because it ends up in logs. The `Last-Event-ID` header is client
155
169
  input like any other: it is passed as `{ after }` to `hub.subscribe` and only picks where the replay resumes inside the
@@ -160,12 +174,26 @@ rules:
160
174
  - id: case-1
161
175
  when: Subscribing to one order's status
162
176
  write: 'hub.subscribe(orderStatusTopic(principal.id, orderId)); the reactor publishes to orderStatusTopic(buyerId, orderId) of the order owner, so a different principal receives nothing.'
177
+ - id: case-2
178
+ when: A room or conversation has several members and membership can change
179
+ write: |-
180
+ The owning domain exports roomMessageTopic(memberId, roomId) from its room-message.policy.ts, returning
181
+ { name: `room-message:${memberId}:${roomId}`, accepts: isRoomMessageFrame } with the declared return type RealtimeTopic<RoomMessageFrame>.
182
+ The SSE door calls hub.stream(roomMessageTopic(principal.id, roomId), { after: lastEventId }) and injects only the hub.
183
+ The publisher reads the current members and publishes a frame to roomMessageTopic(member.id, roomId) for each of them,
184
+ following the hub's commit ordering (BE-REALTIME-7). A non-member's topic receives nothing; removing a member leaves its
185
+ stream open and excludes it from subsequent fan-out. The snapshot query checks membership in its own read.
163
186
  verification:
164
187
  automated:
165
188
  - BE_REALTIME_TOPIC_SCOPE
166
189
  - BE_DEFAULT_DENY
190
+ - BE_SOURCE_FORM
191
+ - BE_PUBLIC_CONTRACT_FORM
192
+ - HFS_COVERAGE_SCOPE_DRIFT
167
193
  manual:
168
- - Review that the topic builder shared with the reactor lives in the domain contract and is used by both sides.
194
+ - Review that the pure topic policy is exported by the owning domain and used by both the publisher and door.
195
+ - Prove the publisher targets exactly the current member list, including the next publish after removing a member; the non-member's stream receives no frame.
196
+ - Prove snapshot and history queries reject a caller whose membership is absent or removed.
169
197
  relatedExamples: [realtime]
170
198
  - id: BE-REALTIME-4
171
199
  title: Three behaviours are proven on the test world
@@ -301,7 +329,7 @@ rules:
301
329
  hfsRules: [R90, R266, R155]
302
330
  requirement: |-
303
331
  Both editions run N api replicas. `platform/realtime` keeps ONE port, `RealtimeHub` (`publish(topic, frame, within?): Promise<void>`
304
- called by a reactor, or by the domain service that owns the state change and passes its own transaction as `within`,
332
+ called by a reactor, or by the domain service that owns the state change with the commit ordering below,
305
333
  `subscribe(topic, { after })` for GraphQL and socket doors, `stream(topic, { after })` for the SSE door), so a door never
306
334
  changes between a single replica and N replicas: only the hub binding does. Three adapters implement the port:
307
335
  `MemoryRealtimeHub` (non-scale: one process, tests and a single-replica dev run, replay from an in-process ring buffer),
@@ -318,8 +346,16 @@ rules:
318
346
  replay reads the stream with XRANGE and resumes after the newest entry carrying the id; when the subscriber connection comes
319
347
  back the hub reloads each followed topic after the minimum last stream id of its open subscriptions, with the same per-subscription
320
348
  duplicate suppression and paused retry policy as Postgres; ioredis, every key and channel carries the
321
- app prefix `REALTIME_REDIS_PREFIX`). The app selects the hub with `REALTIME_HUB=memory|postgres|redis`, parsed once by
322
- `parseRealtimeConfig` and validated at boot; unset, the parser defaults to `redis` when `REALTIME_REDIS_URL` is declared (lite
349
+ app prefix `REALTIME_REDIS_PREFIX`). Only the Postgres hub can publish inside the domain transaction: pass that
350
+ transaction as `within` when the domain write and realtime table use the same database. Redis and memory push immediately,
351
+ even if a caller passes `within`; they do not defer delivery until commit. With either hub the domain service awaits its
352
+ successful transaction before calling `hub.publish(topic, frame)` without `within`. A rollback therefore publishes nothing;
353
+ a publish failure after commit cannot roll back the domain write, and the client resyncs through its membership-checked
354
+ snapshot query. A service whose hub binding can vary uses this after-commit sequence unless it is bound to Postgres.
355
+ The app selects the hub with `REALTIME_HUB=memory|postgres|redis`, parsed once by
356
+ `parseRealtimeConfig` in `platform/realtime/realtime-config.mapper.ts` (the unit-tested mapping role of BE-NAMING-1),
357
+ with its unit spec in `realtime-config.mapper.spec.ts`, and validated at boot; unset, the parser defaults to `redis`
358
+ when `REALTIME_REDIS_URL` is declared (lite
323
359
  and full), `postgres` when lite declares no Redis, and never `memory`, so a scaled deploy cannot fall back to the in-process
324
360
  hub unseen. All three
325
361
  adapters are legal in both editions; select `redis` when Redis is declared (lite with Redis, and full), `postgres` when it is
@@ -381,6 +417,18 @@ rules:
381
417
  RealtimeModule.register({ isGlobal: true, ...withConnection(options.realtime, ORDER_ENTITY_MANAGER, options.database.url) })
382
418
  // REALTIME_HUB=redis, the secret REALTIME_REDIS_URL, REALTIME_REDIS_DB (and REALTIME_REDIS_PREFIX when the server is shared);
383
419
  // Redis 7 or later for the sharded socket.io adapter.
420
+ - id: case-9
421
+ when: A domain service changes state with the Redis or memory hub, or a selectable hub binding
422
+ write: |-
423
+ Collect the frame and entitled recipients while committing the change; publish only after the awaited transaction
424
+ resolves. No publish runs inside its callback, so a rollback sends no frame:
425
+ const push = await this.entityManager.transaction(async (tx) => {
426
+ // Read current membership and write the domain change through tx; return its topic/frame pairs.
427
+ return pendingPushes
428
+ })
429
+ for (const { topic, frame } of push) await this.hub.publish(topic, frame)
430
+ Passing tx to the Redis or memory hub does not make its immediate push transactional. Postgres alone may use
431
+ hub.publish(topic, frame, tx) inside that transaction (case-2).
384
432
  - id: case-4
385
433
  when: GraphQL subscription, non-scale
386
434
  write: |-
@@ -449,8 +497,14 @@ rules:
449
497
  manual:
450
498
  - Confirm the ingress holds no sticky-session declaration for the api, and that polling is off in the socket.io client configuration.
451
499
  - Confirm a Postgres hub connects through a session-mode URL and that the realtime tables are migrations of the app.
500
+ - Prove a domain rollback publishes nothing; with Redis or memory, verify publish follows the awaited commit and a publish failure leaves the committed change intact.
452
501
  relatedExamples: [realtime]
453
502
  files:
503
+ - path: src/modules/domain/<capability>/<name>.policy.ts
504
+ slot: be.domain
505
+ role: The pure typed topic builder and frame predicate of BE-REALTIME-3, exported by the domain contract for publisher and door.
506
+ spec: src/modules/domain/<capability>/<name>.policy.spec.ts
507
+ optional: true
454
508
  - path: src/features/realtime/<channel>/index.ts
455
509
  slot: be.feature.realtime
456
510
  template: realtime/channel.index.ts.tpl
@@ -544,6 +598,12 @@ files:
544
598
  slot: be.platform
545
599
  role: RealtimeHub, the one port of the realtime kind (publish, subscribe, stream) and the typed topic; its three adapters are the files below.
546
600
  optional: true
601
+ - path: src/modules/platform/realtime/realtime-config.mapper.ts
602
+ slot: be.platform
603
+ template: realtime/realtime-config.mapper.ts.tpl
604
+ role: parseRealtimeConfig maps EnvSource to the hub options at boot; hub-selection logic is a covered mapper role.
605
+ spec: src/modules/platform/realtime/realtime-config.mapper.spec.ts
606
+ optional: true
547
607
  - path: src/modules/platform/realtime/memory-realtime-hub.service.ts
548
608
  slot: be.platform
549
609
  role: "MemoryRealtimeHub, the non-scale adapter: one process, tests and a single-replica dev run, replay from an in-process ring buffer."
@@ -80,7 +80,7 @@ op:
80
80
  release:
81
81
  verbs: [cut, publish]
82
82
  scripts: [release:cut, release:publish]
83
- use: starci release cut is owner-approved and moves main plus its tag; starci release publish publishes packages from that release; starci release notes prints or writes a tag CHANGELOG section and is not owner-gated
83
+ use: starci release cut is owner-approved and tags a commit already pushed; starci release publish publishes a finished package at once; starci release push sends main and is not owner-gated; starci release notes prints or writes a tag CHANGELOG section and is not owner-gated
84
84
  # Programs that cannot change state, with any arguments.
85
85
  read: [ls, dir, cat, type, head, tail, wc, uniq, cut, tr, diff, cmp, comm, rg, grep, egrep, fgrep, echo, printf, pwd, cd, pushd, popd, which, where, whoami, hostname,
86
86
  stat, file, realpath, readlink, dirname, basename, date, true, false, test, '[', expr, sleep, seq, basename, md5sum, sha1sum, sha256sum, shasum, cksum, od, xxd, hexdump,
@@ -123,7 +123,7 @@ git:
123
123
  add: {code: RIGHTS_GIT_COMMIT, use: starci git commit}
124
124
  rm: {code: RIGHTS_GIT_COMMIT, use: starci git commit}
125
125
  mv: {code: RIGHTS_GIT_COMMIT, use: starci git commit}
126
- push: {code: RIGHTS_GIT_PUSH, use: 'starci git backup (refs/backup/* only); main and its tag move only through starci release cut'}
126
+ push: {code: RIGHTS_GIT_PUSH, use: 'starci git backup (refs/backup/* only); main moves only through starci release push and a release tag through starci release cut'}
127
127
  tag: {code: RIGHTS_GIT_TAG, use: starci release cut (the only tag maker; housekeeping marks are refs/backup/* or refs/salvage/*, never tags)}
128
128
  merge: {code: RIGHTS_GIT_SYNC, use: starci git sync (merge main into the lane) or starci git land (coordinator)}
129
129
  rebase: {code: RIGHTS_GIT_SYNC, use: starci git sync}
@@ -154,7 +154,7 @@ npm:
154
154
  programs: [npm, pnpm, yarn, bun]
155
155
  read: [view, v, info, show, ls, list, la, ll, outdated, explain, why, root, prefix, bin, help, search, whoami, ping, '-v', '--version', 'config get', 'config list', pack-dry]
156
156
  deny:
157
- publish: {code: RIGHTS_NPM_PUBLISH, use: starci release cut authorizes the release; starci release publish publishes its packages once}
157
+ publish: {code: RIGHTS_NPM_PUBLISH, use: starci release publish is the package-publication path and runs as soon as a package branch is done}
158
158
  unpublish: {code: RIGHTS_NPM_PUBLISH, use: starci release publish is the only package-publication path; unpublishing is owner-only}
159
159
  deprecate: {code: RIGHTS_NPM_PUBLISH, use: starci release publish is the only package-publication path; deprecation is owner-only}
160
160
  dist-tag: {code: RIGHTS_NPM_PUBLISH, use: starci release publish is the only package-publication path; dist-tag mutation is owner-only}
@@ -165,8 +165,8 @@ npm:
165
165
  add: {code: RIGHTS_RAW_TOOL, use: starci npm install adds exact-pinned dependencies and is limited to lead or owner}
166
166
  update: {code: RIGHTS_RAW_TOOL, use: starci npm install is the owner-controlled dependency-change path using canon pins}
167
167
  uninstall: {code: RIGHTS_RAW_TOOL, use: starci npm install is the owner-controlled dependency-change path using canon pins}
168
- test: {code: RIGHTS_RAW_TOOL, use: starci runtime verify in the runtime (the check, package clean-test and scaffold e2e; the full proof is the release cut) or starci gate unit --root <app> in an app}
169
- t: {code: RIGHTS_RAW_TOOL, use: starci runtime verify in the runtime (the check, package clean-test and scaffold e2e; the full proof is the release cut) or starci gate unit --root <app> in an app}
168
+ test: {code: RIGHTS_RAW_TOOL, use: starci runtime verify in the runtime (the check, package clean-test and scaffold e2e; the full proof is CI) or starci gate unit --root <app> in an app}
169
+ t: {code: RIGHTS_RAW_TOOL, use: starci runtime verify in the runtime (the check, package clean-test and scaffold e2e; the full proof is CI) or starci gate unit --root <app> in an app}
170
170
  run: {code: RIGHTS_RAW_TOOL, use: 'starci runtime verify proves a runtime change; starci work layout-render serves the app for a layout capture; use starci release sync-runtime for package runtime copies and grammar preparation, or starci check run, starci lint run, starci typecheck run, starci app stack, or starci gate run for other scripts'}
171
171
  run-script: {code: RIGHTS_RAW_TOOL, use: 'starci runtime verify proves a runtime change; use starci check run, starci lint run, starci typecheck run, starci app stack, or starci gate run for other scripts'}
172
172
  start: {code: RIGHTS_RAW_TOOL, use: 'starci app stack'}
@@ -1770,13 +1770,13 @@ CI_STANDARD:
1770
1770
  kind: check-finding
1771
1771
 
1772
1772
  CI_TRIGGERS_RELEASE_ONLY:
1773
- title: "Workflow trigger other than a release tag, a person or the runtime's main"
1774
- title_vi: "Workflow có trigger không phải tag phát hành, người chạy tay hay main của runtime"
1775
- meaning_vi: "Một workflow CI (gốc, ví dụ hoặc template của packages/hfs) khai báo trigger ngoài các loại được phép: push của tag v*, workflow_dispatch và, chỉ ở workflow gốc của runtime, push của nhánh main (không kèm paths hay bộ lọc ignore). Push nhánh khác, push main trong ví dụ hoặc template, pull_request, schedule hay workflow_call làm CI chạy sai thời điểm, và mọi app scaffold từ template sẽ thừa hưởng lỗi đó."
1773
+ title: "Workflow trigger outside R221"
1774
+ title_vi: "Workflow có trigger ngoài R221"
1775
+ meaning_vi: "Workflow khai báo trigger ngoài R221: main được phép trong workflow runtime và ci.yml của app; các nhánh, sự kiện và bộ lọc khác phải theo R221."
1776
1776
  causes_vi:
1777
- - "Giữ trigger push hoặc pull_request của mô hình CI cũ"
1778
- - "Thêm bộ lọc branches khác [main], hoặc paths, vào push; hoặc thêm push main vào ví dụ hoặc template của app"
1779
- nextStep_vi: "Người giám sát đặt on của workflow gốc thành push với tags: ['v*'] và/hoặc branches: [main] cùng workflow_dispatch; ví dụ và template trong packages/hfs/templates/app/ci-workflows chỉ giữ tags: ['v*'] và workflow_dispatch (render lại ví dụ bằng starci app sync --write); không thêm trigger khác (docs/git-governance.md)."
1777
+ - "Khai báo nhánh, sự kiện hoặc bộ lọc không thuộc R221"
1778
+ - "Khai báo nhánh, sự kiện hoặc bộ lọc không thuộc R221"
1779
+ nextStep_vi: "Sửa on theo R221 rồi chạy starci app sync --write để tạo lại workflow ứng dụng."
1780
1780
  owner: supervisor
1781
1781
  kind: check-finding
1782
1782
 
@@ -2284,6 +2284,17 @@ FE_CONSOLE_CALL:
2284
2284
  owner: op-retry
2285
2285
  kind: check-finding
2286
2286
 
2287
+ FE_CONTROL_HEIGHT:
2288
+ title: "A control picks a larger size or mixes sizes in one row"
2289
+ title_vi: "Điều khiển chọn cỡ lớn hơn hoặc trộn nhiều cỡ trong một hàng"
2290
+ meaning_vi: "Điều khiển Grammar tại <file> truyền size lớn hơn chiều cao chuẩn 40px, hoặc đứng cạnh điều khiển khác cỡ trong cùng một hàng."
2291
+ causes_vi:
2292
+ - "Truyền size lg hoặc xl cho nút, công tắc, ô nhập ngoài khu hero của trang landing."
2293
+ - "Trộn size sm với cỡ mặc định trong cùng một thanh công cụ hoặc hàng."
2294
+ nextStep_vi: "Bỏ size để Grammar vẽ đúng một chiều cao điều khiển 40px (quy định control-height-40)."
2295
+ owner: op-retry
2296
+ kind: check-finding
2297
+
2287
2298
  FE_COOKIE_ATTRIBUTES:
2288
2299
  title: "A written cookie leaves its attributes to the defaults"
2289
2300
  title_vi: "Cookie được ghi mà không nêu rõ thuộc tính"
@@ -2348,6 +2359,16 @@ FE_DESTRUCTIVE_CONFIRM_IN_DIALOG:
2348
2359
  owner: op-retry
2349
2360
  kind: check-finding
2350
2361
 
2362
+ FE_DIVIDER_FULL_BLEED:
2363
+ title: "Horizontal Grammar divider is inset by wrapper padding"
2364
+ title_vi: "Divider bị padding ngang của wrapper làm thụt vào"
2365
+ meaning_vi: "Divider ngang của Grammar phải dài full và sát viền surface."
2366
+ causes_vi:
2367
+ - "Wrapper chứa Divider có padding ngang"
2368
+ nextStep_vi: "Chuyển padding vào các band nội dung cạnh Divider."
2369
+ owner: "op-retry"
2370
+ kind: "check-finding"
2371
+
2351
2372
  FE_EFFECT_CLEANUP:
2352
2373
  title: "What an effect starts, its cleanup releases"
2353
2374
  title_vi: "Effect khởi động thứ không được dọn"
@@ -3209,12 +3230,12 @@ HFS_APP_LEXICON:
3209
3230
  kind: check-finding
3210
3231
 
3211
3232
  HFS_APP_WORKFLOW_TRIGGER:
3212
- title: "App workflows start on release tags or manual dispatch"
3233
+ title: "App workflow trigger outside R251"
3213
3234
  title_vi: "Workflow ứng dụng có sự kiện khởi chạy không hợp lệ"
3214
- meaning_vi: "Workflow tại <path> chạy theo push nhánh hoặc sự kiện khác; ứng dụng chỉ chạy CI theo release tag v* hoặc workflow_dispatch."
3235
+ meaning_vi: "Workflow ứng dụng khai báo trigger ngoài R251; ci.yml được chạy trên main và nhánh release candidate, các workflow khác giữ phạm vi trigger của R251."
3215
3236
  causes_vi:
3216
- - "Vi phạm R251: áp dụng ngoại lệ push main của runtime cho workflow ứng dụng."
3217
- nextStep_vi: "Đặt on.push.tags thành [v*] và dùng workflow_dispatch khi cần chạy thủ công; bỏ nhánh và sự kiện khác."
3237
+ - "Khai báo trigger không thuộc R251"
3238
+ nextStep_vi: "Sửa trigger theo R251 và chạy starci app sync --write."
3218
3239
  owner: op-retry
3219
3240
  kind: check-finding
3220
3241
 
@@ -4522,7 +4543,7 @@ RIGHTS_GIT_PUSH:
4522
4543
  causes_vi:
4523
4544
  - "Agent định đẩy main, một nhánh thường, mọi thẻ hoặc một refspec quá rộng"
4524
4545
  - "Coordinator hoặc Supervisor đẩy bản sao lưu nhưng đích không nằm trong refs/backup/* hoặc không có refspec tường minh"
4525
- nextStep_vi: "Lệnh bị từ chối; để thay đổi đi qua starci release cut, hoặc nếu là coordinator hay Supervisor thì dùng starci git backup với đích refs/backup/* tường minh."
4546
+ nextStep_vi: "Lệnh bị từ chối; để thay đổi đi qua starci release push, hoặc nếu là coordinator hay Supervisor thì dùng starci git backup với đích refs/backup/* tường minh."
4526
4547
  owner: op-retry
4527
4548
  kind: verb-refusal
4528
4549
 
@@ -4595,12 +4616,13 @@ RIGHTS_PROTECTED_ZONE:
4595
4616
  RIGHTS_PUSH_NOT_RELEASE:
4596
4617
  title: "Pre-push gate refused the push"
4597
4618
  title_vi: "Cổng pre-push từ chối lần đẩy"
4598
- meaning_vi: "Hook pre-push đã chặn một lần đẩy main hoặc thẻ v* của runtime vì commit được đẩy chưa phải commit phát hành: phiên bản chưa vượt phiên bản ở main trên remote, thiếu thẻ annotated v<version>, thiếu tiêu đề CHANGELOG có ngày, hoặc thiếu bản ghi L4 của đúng commit đó với suite gốc, suite các package và check đều xanh."
4619
+ meaning_vi: "Hook pre-push đã chặn một lần đẩy main hoặc thẻ v* của runtime hoặc app: main không phải fast-forward hoặc vùng đẩy có ứng viên bí mật; thẻ v* không phải thẻ annotated."
4599
4620
  causes_vi:
4600
4621
  - "Thao tác xóa main hoặc xóa thẻ phát hành"
4601
4622
  - "Đẩy main không phải fast-forward của main trên remote (force push, rewind hoặc nhánh rẽ)"
4602
- - "Commit được đẩy không phải commit phát hành: version trùng version ở main remote, thiếu thẻ annotated v<version>, CHANGELOG chưa có tiêu đề có ngày, hoặc bản ghi starci-release của đúng commit đó thiếu hay có dòng đỏ"
4603
- nextStep_vi: "Không đẩy main giữa hai bản phát hành; chạy starci release cut --tag v<version> (thêm --plan để xem trước) đã được owner duyệt, lệnh này ghi bản ghi L4 rồi đẩy main và thẻ v* cùng nhau."
4623
+ - "Vùng đẩy chứa ứng viên bí mật (tên tệp, dòng và mẫu, không bao giờ giá trị)"
4624
+ - "Đẩy thẻ v* không phải thẻ annotated"
4625
+ nextStep_vi: "Gỡ hoặc mã hóa ứng viên bí mật, fetch main rồi đẩy lại bằng starci release push; thẻ thì tạo bằng starci release cut --tag v<version> (thêm --plan để xem trước)."
4604
4626
  owner: supervisor
4605
4627
  kind: verb-refusal
4606
4628
 
@@ -4618,11 +4640,11 @@ RIGHTS_RAW_TOOL:
4618
4640
  RIGHTS_RELEASE_CUT:
4619
4641
  title: "Release cut refused"
4620
4642
  title_vi: "Từ chối cắt bản phát hành"
4621
- meaning_vi: "Release cut là quyết định phát hành của owner và chạy toàn bộ cổng L4 một lần; worker, lead, Supervisor và coordinator không được tự khởi động nó."
4643
+ meaning_vi: "Release cut gắn thẻ bản phát hành cho một commit đã đẩy và publish gói là quyết định phát hành của owner; worker, lead, Supervisor và coordinator không được tự khởi động chúng."
4622
4644
  causes_vi:
4623
4645
  - "Agent gọi starci release cut hoặc đường script tương đương"
4624
4646
  - "Agent định publish bản phát hành mà chưa có phê duyệt của owner"
4625
- nextStep_vi: "Lệnh bị từ chối; báo lane đã sẵn sàng để owner phê duyệt và khởi động starci release cut."
4647
+ nextStep_vi: "Lệnh bị từ chối; đẩy main bằng starci release push, và báo để owner phê duyệt starci release cut hoặc publish."
4626
4648
  owner: op-retry
4627
4649
  kind: verb-refusal
4628
4650
 
@@ -4643,7 +4665,7 @@ RIGHTS_SUITE_RUN:
4643
4665
  meaning_vi: "Toàn bộ suite chỉ chạy một lần trong mỗi release hoặc theo yêu cầu trong unit.verify hay e2e.verify; một worker chỉ chạy tệp spec vừa thay đổi hoặc để op gate chọn phạm vi."
4644
4666
  causes_vi:
4645
4667
  - "Agent gọi npm test, một script test toàn bộ hoặc node --test trên cả cây tests"
4646
- - "Agent chạy suite ngoài release cut và ngoài op verify được yêu cầu"
4668
+ - "Agent chạy suite ngoài CI và ngoài op verify được yêu cầu"
4647
4669
  nextStep_vi: "Lệnh bị từ chối; chạy riêng tệp spec đã thay đổi, để op gate chọn spec liên quan, hoặc dùng unit.verify/e2e.verify khi owner hay mục tiêu yêu cầu."
4648
4670
  owner: op-retry
4649
4671
  kind: verb-refusal
@@ -5008,11 +5030,11 @@ RT_HELD_UNSURFACED:
5008
5030
  RT_HOOK_SHAPE:
5009
5031
  title: "App hook template breaks the gate model"
5010
5032
  title_vi: "Template hook của app phá vỡ mô hình cổng"
5011
- meaning_vi: "Template hook husky của app (packages/hfs/templates/app/hooks/husky) không đúng mô hình: pre-commit chỉ được chạy mức L0 (work-hygiene, eslint, stylelint, prettier trên tệp staged), không typecheck và không chạy test; pre-push chỉ kiểm cổng phát hành (bản ghi L4 trong starci-release, không gian refs/backup/, mẫu thẻ v[0-9]*), không chạy npm test, jest, typecheck hay lint."
5033
+ meaning_vi: "Template hook của app không đúng R225: pre-commit chạy L0; pre-push gọi chính sách push dùng chung trong gói, không chạy build, test, typecheck hay lint."
5012
5034
  causes_vi:
5013
5035
  - "Thêm lại npm run typecheck hoặc test:affected vào pre-commit"
5014
- - "Đưa typecheck, lint hay test vào pre-push, hoặc xóa các dấu mốc cổng phát hành khỏi pre-push"
5015
- nextStep_vi: "Người giám sát trả template về đúng mô hình (pre-commit L0, pre-push chỉ kiểm cổng phát hành) rồi render lại hook các ví dụ bằng starci app sync --write, qua một lane đã land."
5036
+ - "Đưa build, typecheck, lint hay test vào pre-push hoặc bỏ lời gọi chính sách push dùng chung"
5037
+ nextStep_vi: "Khôi phục template theo R225 rồi chạy starci app sync --write để tạo lại hook."
5016
5038
  owner: supervisor
5017
5039
  kind: check-finding
5018
5040
 
@@ -131,12 +131,6 @@ removed:
131
131
  use: '`starci debug digest` (read-only) and `starci debug run <probe>`'
132
132
  since: 1.0.0-alpha.6
133
133
  match: '(?<![\w-])debug pass\b'
134
- - id: config-supervisor-landgate-push
135
- kind: config-key
136
- name: supervisor.landGate.push
137
- use: 'a land fast-forwards local main only and never pushes; the remote main of the runtime moves once per release, through starci release cut'
138
- since: 1.0.0-alpha.7
139
- match: 'landGate\.push\b'
140
134
  - id: flag-land-no-push
141
135
  kind: flag
142
136
  name: --no-push
@@ -355,7 +349,7 @@ removed:
355
349
  - id: verb-test-run
356
350
  kind: verb
357
351
  name: starci test run
358
- use: 'the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change, `starci release cut` runs the release rows'
352
+ use: 'the L1 to L4 spec ladder is gone with the runtime specs; `starci runtime verify` proves a change, CI runs the release rows'
359
353
  since: 1.0.0-alpha.19
360
354
  match: '\bstarci test run\b'
361
355
  - id: verb-gate-runtime-coverage
@@ -430,3 +424,35 @@ removed:
430
424
  use: 'DropdownMenuItem.tone uses neutral or danger; destructive commands use danger'
431
425
  since: 1.0.0-alpha.31
432
426
  match: 'DropdownMenuItem(?:Tone|\.tone)[^\r\n]*["'']negative["'']'
427
+
428
+ - id: code-release-receipt-missing
429
+ kind: code
430
+ name: RELEASE_RECEIPT_MISSING
431
+ use: 'the push of main asks for a fast-forward and a clean secret scan; a release tag asks for no receipt'
432
+ since: 1.0.0-alpha.31.1
433
+ - id: code-release-receipt-incomplete
434
+ kind: code
435
+ name: RELEASE_RECEIPT_INCOMPLETE
436
+ use: 'the push of main asks for a fast-forward and a clean secret scan; CI judges the full suite'
437
+ since: 1.0.0-alpha.31.1
438
+ - id: code-release-suite-mode
439
+ kind: code
440
+ name: RELEASE_SUITE_MODE
441
+ use: 'release.suite chooses whether the cut also runs the L4 rows; no record or gate reads the mode'
442
+ since: 1.0.0-alpha.31.1
443
+ - id: code-release-version-unchanged
444
+ kind: code
445
+ name: RELEASE_VERSION_UNCHANGED
446
+ use: 'main is pushed without a version move; RELEASE_VERSION_NOT_NEXT judges the version of a tag against the v* tags'
447
+ since: 1.0.0-alpha.31.1
448
+ - id: code-release-remote-head-unknown
449
+ kind: code
450
+ name: RELEASE_REMOTE_HEAD_UNKNOWN
451
+ use: 'a release tag is judged against the v* tags, not against the remote main head'
452
+ since: 1.0.0-alpha.31.1
453
+ - id: config-release-candidate-blocking
454
+ kind: config-key
455
+ name: candidate.blocking
456
+ use: 'the runtime cut has no candidate step; the ci, examples and sonar workflows judge each push to main (suite.ci.workflows)'
457
+ since: 1.0.0-alpha.31.1
458
+ match: 'candidate\.blocking\b'