@starci/hfs 4.12.2 → 4.13.2

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 (160) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -9
  3. package/cache/app-families.d.mts +30 -0
  4. package/cache/app-families.mjs +94 -0
  5. package/cache/content-index.d.mts +13 -0
  6. package/cache/content-index.mjs +37 -0
  7. package/cache/db-family.d.mts +13 -0
  8. package/cache/db-family.mjs +37 -0
  9. package/cache/fake-session.d.mts +42 -0
  10. package/cache/fake-session.mjs +57 -0
  11. package/cache/fingerprint.d.mts +28 -0
  12. package/cache/fingerprint.mjs +120 -0
  13. package/cache/format-family.d.mts +13 -0
  14. package/cache/format-family.mjs +49 -0
  15. package/cache/git.d.mts +11 -0
  16. package/cache/git.mjs +13 -0
  17. package/cache/hash.d.mts +15 -0
  18. package/cache/hash.mjs +58 -0
  19. package/cache/hfs-family.d.mts +35 -0
  20. package/cache/hfs-family.mjs +164 -0
  21. package/cache/import-graph.d.mts +75 -0
  22. package/cache/import-graph.mjs +204 -0
  23. package/cache/jsonl.d.mts +31 -0
  24. package/cache/jsonl.mjs +115 -0
  25. package/cache/lint-family.d.mts +162 -0
  26. package/cache/lint-family.mjs +433 -0
  27. package/cache/order.d.mts +1 -0
  28. package/cache/order.mjs +2 -0
  29. package/cache/progress.d.mts +19 -0
  30. package/cache/progress.mjs +68 -0
  31. package/cache/quality-baselines.d.mts +109 -0
  32. package/cache/quality-baselines.mjs +235 -0
  33. package/cache/records.d.mts +33 -0
  34. package/cache/records.mjs +54 -0
  35. package/cache/report.d.mts +19 -0
  36. package/cache/report.mjs +32 -0
  37. package/cache/run-log.d.mts +17 -0
  38. package/cache/run-log.mjs +37 -0
  39. package/cache/run.d.mts +33 -0
  40. package/cache/run.mjs +65 -0
  41. package/cache/session.d.mts +29 -0
  42. package/cache/session.mjs +197 -0
  43. package/cache/side-key.d.mts +10 -0
  44. package/cache/side-key.mjs +47 -0
  45. package/cache/test-family.d.mts +130 -0
  46. package/cache/test-family.mjs +258 -0
  47. package/cache/trust.d.mts +53 -0
  48. package/cache/trust.mjs +157 -0
  49. package/cache/types-family.d.mts +74 -0
  50. package/cache/types-family.mjs +154 -0
  51. package/cache/types.d.mts +99 -0
  52. package/cache/types.mjs +8 -0
  53. package/cache/wiring-family.d.mts +14 -0
  54. package/cache/wiring-family.mjs +43 -0
  55. package/coverage/cli.d.mts +4 -2
  56. package/coverage/cli.mjs +44 -5
  57. package/db/migrate.d.mts +16 -0
  58. package/db/migrate.mjs +21 -4
  59. package/db/postgres.d.mts +23 -0
  60. package/db/postgres.mjs +32 -1
  61. package/lint/run.d.mts +47 -1
  62. package/lint/run.mjs +157 -5
  63. package/package.json +2 -1
  64. package/release/gate.d.mts +2 -0
  65. package/release/gate.mjs +4 -2
  66. package/release/records.d.mts +15 -0
  67. package/release/records.mjs +25 -0
  68. package/release/urgent.mjs +23 -4
  69. package/runtime/knowledge/grammars/core/playbook.yaml +12 -18
  70. package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
  71. package/runtime/knowledge/hfs/ci-standard.yaml +29 -6
  72. package/runtime/knowledge/hfs/rules.yaml +97 -7
  73. package/runtime/knowledge/hfs/slots.yaml +20 -8
  74. package/runtime/modules/kernel/failure-codes.yaml +46 -4
  75. package/runtime/modules/schemas/application-stacks.schema.yaml +48 -0
  76. package/runtime/scripts/hfs/architecture/hfs.d.mts +6 -6
  77. package/runtime/scripts/hfs/check.mjs +1 -1
  78. package/runtime/scripts/hfs/gate-threshold.mjs +2 -1
  79. package/runtime/scripts/hfs/rules/boot-proof.d.mts +5 -0
  80. package/runtime/scripts/hfs/rules/boot-proof.mjs +15 -1
  81. package/runtime/scripts/hfs/rules/ci-custody.mjs +3 -1
  82. package/runtime/scripts/hfs/rules/ci-standard.mjs +32 -3
  83. package/runtime/scripts/hfs/rules/database-migrations.d.mts +14 -0
  84. package/runtime/scripts/hfs/rules/database-migrations.mjs +55 -0
  85. package/runtime/scripts/hfs/rules/database.mjs +2 -1
  86. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +53 -6
  87. package/runtime/scripts/hfs/rules/release-provenance.mjs +32 -4
  88. package/runtime/scripts/hfs/rules/stacks.mjs +5 -1
  89. package/runtime/scripts/hfs/rules/supply-chain.mjs +4 -1
  90. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +98 -7
  91. package/runtime/scripts/hfs/rules/unused-symbol.d.mts +9 -2
  92. package/runtime/scripts/hfs/rules/unused-symbol.mjs +15 -2
  93. package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +21 -5
  94. package/runtime/scripts/hfs/stack-deploy-config.d.mts +18 -0
  95. package/runtime/scripts/hfs/stack-deploy-config.mjs +53 -1
  96. package/runtime/scripts/hfs/stacks-gate.mjs +2 -0
  97. package/runtime/scripts/hfs/tree-findings.mjs +3 -1
  98. package/scaffold/add-inbox-lite.mjs +2 -4
  99. package/scaffold/add-queue-lite.mjs +2 -3
  100. package/scaffold/add-table.mjs +2 -4
  101. package/scaffold/migration-stamp.d.mts +11 -0
  102. package/scaffold/migration-stamp.mjs +32 -0
  103. package/src/helpers/hfs-arch-fixture.d.mts +1 -1
  104. package/src/main.d.mts +4 -1
  105. package/src/main.mjs +189 -74
  106. package/src/stack-records-double.d.mts +33 -0
  107. package/stack/ci-key.d.mts +11 -0
  108. package/stack/ci-key.mjs +44 -0
  109. package/stack/cli.d.mts +7 -1
  110. package/stack/cli.mjs +22 -13
  111. package/stack/config.d.mts +15 -0
  112. package/stack/converge.d.mts +2 -0
  113. package/stack/converge.mjs +1 -1
  114. package/stack/db-phases.d.mts +185 -0
  115. package/stack/db-phases.mjs +346 -0
  116. package/stack/deploy.d.mts +22 -5
  117. package/stack/deploy.mjs +242 -72
  118. package/stack/journal.d.mts +146 -0
  119. package/stack/journal.mjs +191 -0
  120. package/stack/manager-backup.d.mts +54 -0
  121. package/stack/manager-backup.mjs +136 -0
  122. package/stack/notify.d.mts +9 -0
  123. package/stack/notify.mjs +32 -0
  124. package/stack/release-store.d.mts +49 -1
  125. package/stack/release-store.mjs +102 -16
  126. package/stack/remote.d.mts +2 -0
  127. package/stack/remote.mjs +32 -11
  128. package/stack/rollback-guard.d.mts +10 -0
  129. package/stack/rollback-guard.mjs +41 -0
  130. package/stack/server-key.d.mts +2 -0
  131. package/stack/server-key.mjs +1 -1
  132. package/stack/ssh-gate.d.mts +35 -0
  133. package/stack/ssh-gate.mjs +145 -0
  134. package/stack/status.d.mts +51 -0
  135. package/stack/status.mjs +115 -0
  136. package/stack/urgent-floor.d.mts +10 -61
  137. package/stack/urgent-floor.mjs +28 -123
  138. package/stack/verify.mjs +26 -0
  139. package/sync/ci-commands.mjs +10 -5
  140. package/sync/format-worker.d.mts +1 -0
  141. package/sync/format-worker.mjs +17 -0
  142. package/sync/format.d.mts +21 -3
  143. package/sync/format.mjs +99 -7
  144. package/sync/index.d.mts +3 -6
  145. package/sync/index.mjs +12 -12
  146. package/sync/stack-images.d.mts +25 -10
  147. package/sync/stack-images.mjs +215 -54
  148. package/templates/app/ci-workflows/github/workflows/ci.yml +114 -7
  149. package/templates/app/ci-workflows/github/workflows/images.yml +21 -65
  150. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +130 -37
  151. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +114 -7
  152. package/templates/app/ci-workflows-lite/github/workflows/images.yml +21 -65
  153. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +130 -37
  154. package/templates/app/gitignore +1 -0
  155. package/templates/app/gitignore-lite +1 -0
  156. package/templates/app/package-scripts/db.json +1 -1
  157. package/templates/app/skeleton/.gitattributes +2 -0
  158. package/templates/app/skeleton-lite/.gitattributes +2 -0
  159. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +2 -1
  160. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +0 -63
@@ -15,4 +15,19 @@ export declare function recordRow(dir: string, head: string, row: {
15
15
  }): void;
16
16
  /** The recorded green row of `head` whose name and command match and whose log still has the recorded sha256, or null. */
17
17
  export declare function greenRow(dir: string, head: string, name: string, command: string): Json;
18
+ /** The trailer keys an urgent release writes into its tag annotation (WP3 of contexts/project/cicd.md): the tag's CI run reads them to back-fill the record. */
19
+ export declare const URGENT_TRAILER = "Starci-Urgent";
20
+ export declare const URGENT_REASON_TRAILER = "Starci-Urgent-Reason";
21
+ /** What the trailers of an urgent tag say: who released it and why. */
22
+ export interface UrgentTrailers {
23
+ role: string;
24
+ reason: string;
25
+ }
26
+ /** The trailer block of an urgent tag annotation: one `Starci-Urgent: <role>` and one `Starci-Urgent-Reason: <text>` line (the reason is one line, so it cannot forge a trailer). */
27
+ export declare function urgentTrailerBlock(role: string, reason: string): string;
28
+ /**
29
+ * The urgent trailers of a tag annotation, read from its last paragraph (the place `git interpret-trailers` reads), or null when it has none.
30
+ * Both keys must be there; the last occurrence of a key wins, like git's own parse.
31
+ */
32
+ export declare function parseUrgentTrailers(message: string): UrgentTrailers | null;
18
33
  export {};
@@ -53,3 +53,28 @@ export function greenRow(dir, head, name, command) {
53
53
  const row = rows.find((known) => known?.name === name && known.command === command && known.pass === true);
54
54
  return row && row.log && fileSha256(row.log) === row.logSha256 ? row : null;
55
55
  }
56
+ /** The trailer keys an urgent release writes into its tag annotation (WP3 of contexts/project/cicd.md): the tag's CI run reads them to back-fill the record. */
57
+ export const URGENT_TRAILER = 'Starci-Urgent';
58
+ export const URGENT_REASON_TRAILER = 'Starci-Urgent-Reason';
59
+ /** The role as a trailer value: the caller's role name (letters, digits, dot, dash, underscore); anything else becomes a dash. */
60
+ const trailerRole = (role) => role.replaceAll(/[^\w.-]/gu, '-');
61
+ /** The trailer block of an urgent tag annotation: one `Starci-Urgent: <role>` and one `Starci-Urgent-Reason: <text>` line (the reason is one line, so it cannot forge a trailer). */
62
+ export function urgentTrailerBlock(role, reason) {
63
+ return `${URGENT_TRAILER}: ${trailerRole(role)}\n${URGENT_REASON_TRAILER}: ${reason.replaceAll(/\s+/gu, ' ').trim()}`;
64
+ }
65
+ /**
66
+ * The urgent trailers of a tag annotation, read from its last paragraph (the place `git interpret-trailers` reads), or null when it has none.
67
+ * Both keys must be there; the last occurrence of a key wins, like git's own parse.
68
+ */
69
+ export function parseUrgentTrailers(message) {
70
+ const last = message.replaceAll('\r\n', '\n').trim().split(/\n\s*\n/u).at(-1) ?? '';
71
+ const found = new Map();
72
+ for (const line of last.split('\n')) {
73
+ const match = /^([A-Za-z][A-Za-z0-9-]*):[ \t]*(.*)$/u.exec(line.trimEnd());
74
+ if (match)
75
+ found.set(match[1], match[2].trim());
76
+ }
77
+ const role = found.get(URGENT_TRAILER);
78
+ const reason = found.get(URGENT_REASON_TRAILER);
79
+ return role && reason ? { role, reason } : null;
80
+ }
@@ -11,7 +11,7 @@ import { firstLine } from '../stack/context.mjs';
11
11
  import { failure, refusal } from '../stack/errors.mjs';
12
12
  import { assertUnmoved, git, inspect, releaseDir, runGate, summaryOf } from './gate.mjs';
13
13
  import { assertNames, branchRefs, probe, tagRefs } from './push.mjs';
14
- import { URGENT_RULING, URGENT_SCHEMA, greenRow, recordRow, writeUrgent } from './records.mjs';
14
+ import { URGENT_RULING, URGENT_SCHEMA, greenRow, parseUrgentTrailers, recordRow, urgentTrailerBlock, writeUrgent } from './records.mjs';
15
15
  /**
16
16
  * The fast local checks of an urgent release, by gate name, in the order they run: what can break production, the types and the build
17
17
  * (owner ruling urgent-typecheck-build, 2026-10-30). App check, lint and the tests wait for the back-fill (`release gate --backfill`).
@@ -61,6 +61,25 @@ function planOf(checks) {
61
61
  return ['urgent checks (each must pass; a green row recorded for HEAD with the same command and log is reused):',
62
62
  ...checks.map((check) => ` ${check.name.padEnd(10)} ${check.gate ? check.gate.command.join(' ') : '(nothing to run)'}${check.note ? ` [${check.note}]` : ''}`)];
63
63
  }
64
+ /**
65
+ * Writes the urgent trailers (`Starci-Urgent`, `Starci-Urgent-Reason`) into the annotation of the release tag, so the tag's own CI run knows the release was
66
+ * urgent and back-fills its record instead of deploying it again. A new tag carries them from the start; a tag the gate already put on HEAD and nobody has
67
+ * published yet is re-annotated (its message kept, the trailers appended); a tag already on the remote is never moved, so it stays without them and the line says so.
68
+ */
69
+ function annotateTag(ctx, { tag, head, role, reason, onHead, unpublished }) {
70
+ const trailers = urgentTrailerBlock(role, reason);
71
+ if (!onHead) {
72
+ git(ctx, ['tag', '-a', tag, '-m', `URGENT release ${tag} on ${head.slice(0, 12)} by ${role}: ${reason} (fast local checks only; back-fill: starci app release gate --backfill ${tag})`, '-m', trailers, head]);
73
+ return { written: true, line: `tag trailers ${tag}: Starci-Urgent, Starci-Urgent-Reason` };
74
+ }
75
+ const message = git(ctx, ['for-each-ref', '--format=%(contents)', `refs/tags/${tag}`]).stdout;
76
+ if (parseUrgentTrailers(message))
77
+ return { written: true, line: `tag trailers ${tag}: already there` };
78
+ if (!unpublished)
79
+ return { written: false, line: `tag trailers NONE: ${tag} is already published without them and a published tag is never moved; the deploy record still names the urgent role and reason` };
80
+ git(ctx, ['tag', '-a', '-f', tag, '-m', message.trim() === '' ? `release ${tag}` : message.trim(), '-m', trailers, head]);
81
+ return { written: true, line: `tag trailers ${tag}: re-annotated (not yet published): Starci-Urgent, Starci-Urgent-Reason` };
82
+ }
64
83
  /**
65
84
  * `starci app release push --urgent`. options: { execute, remote, reason, role, platform }. Dry run prints the checks, the tag and the push it would
66
85
  * make; --execute runs them, tags, pushes atomically and writes the urgent record. A red check is RELEASE_URGENT_CHECK_RED: nothing tagged, nothing pushed.
@@ -94,8 +113,8 @@ export async function releaseUrgentPush(ctx, config, options = {}) {
94
113
  if (failed)
95
114
  throw failure('RELEASE_URGENT_CHECK_RED', `${failed.name} is red (${failed.detail}; log ${failed.log}): nothing tagged, nothing pushed`);
96
115
  assertUnmoved(ctx, head);
97
- if (chosen.state !== 'on-head')
98
- git(ctx, ['tag', '-a', chosen.tag, '-m', `URGENT release ${chosen.tag} on ${head.slice(0, 12)} by ${role}: ${reason} (fast local checks only; back-fill: starci app release gate --backfill ${chosen.tag})`, head]);
116
+ const trailers = annotateTag(ctx, { tag: chosen.tag, head, role, reason, onHead: chosen.state === 'on-head', unpublished: refs.includes(`refs/tags/${chosen.tag}`) });
117
+ out.push(trailers.line);
99
118
  if (refs.length > 0) {
100
119
  const hook = probe(ctx, remote, refs, red);
101
120
  out.push(...hook.lines);
@@ -107,7 +126,7 @@ export async function releaseUrgentPush(ctx, config, options = {}) {
107
126
  }
108
127
  const record = {
109
128
  schema: URGENT_SCHEMA, head, tag: chosen.tag, createdAt: new Date(ctx.now()).toISOString(), producedBy: 'starci app release push --urgent',
110
- role, reason, ruling: URGENT_RULING, remote, branch, refs, pushed: true, checks: rows, backfill: null,
129
+ role, reason, trailers: trailers.written, ruling: URGENT_RULING, remote, branch, refs, pushed: true, checks: rows, backfill: null,
111
130
  };
112
131
  writeUrgent(common, record);
113
132
  return { ok: true, lines: [...out, `pushed to ${remote}: ${refs.join(' ') || '(already there)'}`, `URGENT record written: ${path.join(common, `${head}.urgent.json`)}`, `back-fill later: starci app release gate --backfill ${chosen.tag} --execute (in a clean checkout of ${chosen.tag})`], data: { ...data, pushed: true, record: path.join(common, `${head}.urgent.json`) } };
@@ -357,8 +357,8 @@ guidance:
357
357
  NumberField, DateField, TimeField, Select, ComboBox, DatePicker, DateRangePicker, Checkbox,
358
358
  CheckboxGroup, RadioGroup, OtpInput and PressableField resolve variant="secondary" inside it and
359
359
  variant="primary" outside. Compose the surface and omit the field variant.
360
- Switch and Slider have no layer variant; a Radio follows its RadioGroup. ChatWorkspace's
361
- compact drawer also establishes the context. SurfaceCard and bounded SurfaceAccordionCard derive
360
+ Switch and Slider have no layer variant; a Radio follows its RadioGroup. PaneGrid's
361
+ collapsed-pane drawer also establishes the context. SurfaceCard and bounded SurfaceAccordionCard derive
362
362
  data-grammar-surface-depth from the parent context. The top layer is raised and a nested layer is
363
363
  subordinate; never raise both. The family DNA owns their surface paint.
364
364
  FE_NESTED_VARIANT rejects a static field variant
@@ -597,23 +597,17 @@ presentation:
597
597
  Retained ownership reference, not proof of the current package/API. Verify actual installed source/props/tokens before use; correct outdated rows within selected maintenance scope using [source and maintenance](index.yaml#source-and-maintenance).
598
598
  A part that carries one rule per side or per property names that CSS property after the part (`item padding-inline`); one part and property has one rule.
599
599
  rows:
600
- - component: ChatWorkspace
601
- element: drawer body, hasRail, isCompactRail
600
+ - component: PaneGrid
601
+ element: drawer body, collapsing pane
602
602
  rule: PADDING-0
603
- - component: ChatWorkspace
604
- element: overlay rail, hasRail, isCompactRail
605
- rule: PADDING-4
606
- - component: ChatWorkspace
607
- element: overlay rail, hasRail, not isCompactRail
608
- rule: PADDING-4
609
- - component: ChatWorkspace
610
- element: rail trigger boundary padding-block, hasRail, isCompactRail
603
+ - component: PaneGrid
604
+ element: trigger bar padding-block, collapsing pane
611
605
  rule: PADDING-2
612
- - component: ChatWorkspace
613
- element: rail trigger boundary padding-inline, hasRail, isCompactRail
606
+ - component: PaneGrid
607
+ element: trigger bar padding-inline, collapsing pane
614
608
  rule: PADDING-4
615
- - component: ChatWorkspace
616
- element: rail trigger, hasRail, isCompactRail
609
+ - component: PaneGrid
610
+ element: drawer trigger, collapsing pane
617
611
  rule: PADDING-3
618
612
  - component: EmptyNotice
619
613
  element: root
@@ -959,8 +953,8 @@ presentation:
959
953
  source/props/tokens before use; correct outdated rows within selected maintenance scope using [source
960
954
  and maintenance](index.yaml#source-and-maintenance).
961
955
  rows:
962
- - component: ChatWorkspace
963
- element: rail trigger, hasRail, isCompactRail
956
+ - component: PaneGrid
957
+ element: drawer trigger, collapsing pane
964
958
  rule: FONT-2
965
959
  - component: Heading
966
960
  element: root, scale!="display", level=1
@@ -31,42 +31,42 @@ pins:
31
31
  source: packages/event-stream/package.json
32
32
  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.'
33
33
  '@starci/grammar':
34
- version: 0.16.2
34
+ version: 0.16.5
35
35
  group: starci
36
36
  install: registry
37
37
  side: fe
38
38
  source: packages/grammar/package.json
39
39
  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.'
40
40
  '@starci/eslint-canon-be':
41
- version: 3.9.2
41
+ version: 3.10.0
42
42
  group: starci
43
43
  install: registry
44
44
  side: be
45
45
  source: packages/eslint-canon-be/package.json
46
46
  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.'
47
47
  '@starci/eslint-canon-fe':
48
- version: 9.6.2
48
+ version: 9.6.5
49
49
  group: starci
50
50
  install: registry
51
51
  side: fe
52
52
  source: packages/eslint-canon-fe/package.json
53
53
  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.'
54
54
  '@starci/eslint-grammar-core':
55
- version: 0.16.2
55
+ version: 0.16.5
56
56
  group: starci
57
57
  install: registry
58
58
  side: fe
59
59
  source: packages/eslint-grammar-core/package.json
60
60
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
61
61
  '@starci/eslint-grammar-offset-pop':
62
- version: 0.16.2
62
+ version: 0.16.5
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
66
66
  source: packages/eslint-grammar-offset-pop/package.json
67
67
  why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
68
68
  '@starci/stylelint-canon':
69
- version: 2.2.1
69
+ version: 2.3.0
70
70
  group: starci
71
71
  install: registry
72
72
  side: fe
@@ -85,7 +85,7 @@ pins:
85
85
  side: both
86
86
  source: packages/prettier-config/package.json
87
87
  '@starci/jest-preset':
88
- version: 2.5.3
88
+ version: 2.6.0
89
89
  group: starci
90
90
  install: registry
91
91
  side: be
@@ -106,7 +106,7 @@ pins:
106
106
  source: packages/ai-router/package.json
107
107
  why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
108
108
  '@starci/hfs':
109
- version: 4.12.2
109
+ version: 4.13.2
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -11,10 +11,18 @@ title: The one StarCi CI standard
11
11
  # over the runtime's own workflows) checks every item marked `check: ci-standard`; the other items are owned by the rule or render named in `by`.
12
12
  # Apps get the standard only through `starci app sync`: the managed CI files are sync targets, and `starci app sync` (the dry run) / the app check refuse drift.
13
13
 
14
- # The jobs of the managed app CI (job ids, in this order), per edition; `sonar` exists when the Sonar service is enabled.
14
+ # The jobs of the managed app CI (job ids, in this order), per edition; `sonar` exists when the Sonar service is enabled. `plan` decides what a run does,
15
+ # `images` builds the images once per commit beside the checks (a call of the reusable images.yml, so GitHub names its jobs `images / ...`), `verdict` is
16
+ # the one job branch protection requires and the deploy waits for.
17
+ # required the jobs a green run has concluded success. The CLI's `stack deploy --ci-run` reads this list from here and asks GitHub for each of them
18
+ # in the run it is given, so the list names the jobs GitHub reports under their own id; the verdict needs `images`, so it proves them.
19
+ # pipeline the jobs the workflow must hold besides those, judged through the verdict
15
20
  jobs:
16
- full: {required: [fast, lint, test, coverage, build], optional: [sonar]}
17
- lite: {required: [fast, lint, test, coverage, build], optional: [db-types, sonar]}
21
+ full: {required: [plan, fast, lint, test, coverage, build, verdict], pipeline: [images], optional: [sonar]}
22
+ lite: {required: [plan, fast, lint, test, coverage, build, verdict], pipeline: [images], optional: [db-types, sonar]}
23
+
24
+ # One job per environment with deploy.auto follows the verdict: its id starts with this prefix and it calls the reusable stack-deploy.yml.
25
+ deployJobPrefix: deploy-
18
26
 
19
27
  # The per-job settings the managed CI renders when .starcistacks/application-stacks.yaml ci.jobs leaves them out (ci.jobs.<job>.timeoutMinutes,
20
28
  # ci.jobs.test.shards). Sized from a real run (NIVO v0.2.3 on the 2-vCPU GitHub runner, 2026-10-18): the be lint with the Sonar import took 11.5 minutes and the
@@ -34,14 +42,29 @@ heap:
34
42
  items:
35
43
  - id: candidate-trigger
36
44
  title: the release CI runs on the release candidate ref
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"
45
+ requires: "ci.yml runs on push of branches ['main', 'release/candidate-*'] and tags ['v*'], on pull_request to main and on workflow_dispatch; the release flows wait for the run of the release sha there; pull_request_target, workflow_run and schedules are never triggers"
38
46
  covers: [app-ci, runtime]
39
47
  by: [R221 CI_TRIGGERS_RELEASE_ONLY, R251 HFS_APP_WORKFLOW_TRIGGER]
40
48
  - id: job-shape
41
- title: the job shape fast / lint / test / coverage / build / sonar
42
- requires: "the managed CI holds exactly the jobs of `jobs` for its edition; the unit run is a step of `test`; the merge, the coverage proof and the Codecov upload are steps of `coverage`, which alone holds the OIDC id-token"
49
+ title: the job shape plan / fast / lint / test / coverage / build / images / verdict / sonar and the deploy jobs
50
+ requires: "the managed CI holds exactly the jobs of `jobs` for its edition and one `deploy-<env>` job per environment with deploy.auto; the unit run is a step of `test`; the merge, the coverage proof and the Codecov upload are steps of `coverage`, which alone holds the OIDC id-token; the verdict needs every other job and runs always; a deploy job needs the verdict and calls the reusable stack-deploy.yml, and no other job deploys"
43
51
  covers: [app-ci]
44
52
  check: ci-standard
53
+ - id: pull-request-ci
54
+ title: a pull request runs the checks with no secret and no environment
55
+ requires: "a workflow that runs on pull_request has read-only workflow permissions, binds no job to an environment and reads no secret outside the jobs the plan skips for a pull request (sonar); it never checks out or runs the code of a pull request under pull_request_target"
56
+ covers: [app-ci]
57
+ by: [R221 CI_TRIGGERS_RELEASE_ONLY, R251 HFS_APP_WORKFLOW_TRIGGER]
58
+ - id: images-once
59
+ title: the images are built once per commit, in parallel with the checks, and pushed by digest
60
+ requires: "images.yml is a reusable workflow (workflow_call) that ci.yml calls beside the checks; every pushed image carries the immutable tag sha-<sha> (the release tag v<version> is a retag of that digest), an SBOM and provenance; the deploy takes digests only; no mutable environment tag is deployed; build arguments are public values"
61
+ covers: [app-ci]
62
+ by: [R271 HFS_RELEASE_TAG_MISMATCH, R269 HFS_SBOM_MISSING]
63
+ - id: deploy-brake
64
+ title: a green release tag deploys by itself, and STARCI_DEPLOY_PAUSED stops it
65
+ requires: "the deploy of an environment with deploy.auto starts only on a release tag run after the verdict, bound to that GitHub Environment (deployment refs restricted to v* tags); its variable STARCI_DEPLOY_PAUSED=true skips the automatic deploy without a code change; a manual dispatch of stack-deploy.yml ignores the brake"
66
+ covers: [app-ci]
67
+ by: [R251 HFS_DEPLOY_WORKFLOW]
45
68
  - id: full-history-base-ref
46
69
  title: full history and the lint base STARCI_BASE_REF
47
70
  requires: "every actions/checkout of the managed CI fetches the full history (fetch-depth: 0), and the lint job writes STARCI_BASE_REF to GITHUB_ENV before it lints"
@@ -3776,7 +3776,7 @@ rules:
3776
3776
  - {path: packages/hfs/src/hfs-lite-secrets.spec.mjs, edition: lite}
3777
3777
  - id: R221
3778
3778
  code: "CI_TRIGGERS_RELEASE_ONLY"
3779
- 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."
3779
+ law: "Workflow triggers are push filtered to v* tags or release/candidate-* branches, workflow_call (a reusable workflow has no trigger of its own) and workflow_dispatch. Runtime workflows and app ci.yml also admit main, and app ci.yml admits pull_request to main (branches ['main'] only: a pull request run has no secret and no environment). App ci.yml includes the candidate branch filter for starci app release push. pull_request_target, workflow_run, schedules, other branches, events, path filters and ignore filters are refused."
3780
3780
  scope: runtime
3781
3781
  editions: [lite, full]
3782
3782
  docs: [docs/git-governance.md#r221]
@@ -4195,7 +4195,7 @@ rules:
4195
4195
  editions: [lite, full]
4196
4196
  docs:
4197
4197
  - {path: docs/application-stacks.md#r251-deploy-workflow, editions: [lite, full]}
4198
- 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; every job that reads secrets.SOPS_AGE_KEY, a deployment job or not, is bound to the GitHub Environment whose members it opens, so it reads that environment's server key and never a repository secret of that name. Deployment jobs 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."
4198
+ 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; every job that reads secrets.SOPS_AGE_KEY, a deployment job or not, is bound to the GitHub Environment whose members it opens, so it reads that environment's server key and never a repository secret of that name. Deployment jobs 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), workflow_call or workflow_dispatch; ci.yml also runs on main and on pull_request to main (read-only workflow permissions, no environment bound in the file), and other branch pushes are refused. A release deploys through the reusable stack-deploy.yml (ruling app-cicd-auto-deploy): the job of ci.yml that calls it (deploy-<env>, after the verdict job) holds permissions, concurrency and with.environment, the called deploy job binds the GitHub Environment (deployment refs restricted to v* tags) and reads SOPS_AGE_KEY there, and the transport is the sealed member vps-ci opened with that key, so the CI deploy key, the host and the pinned host keys are never GitHub secrets, workflow text or log lines; STARCI_DEPLOY_PAUSED on the environment stops the automatic deploy. The secrets allowlist and the dotenv ban are judged on the deployment jobs, not on the check jobs beside them."
4199
4199
  kinds: [check]
4200
4200
  gates: [settle, land, ci]
4201
4201
  failureCodes: ["HFS_DEPLOY_WORKFLOW", "HFS_WORKFLOW_PIN", "HFS_CI_TOOL_INSTALL", "HFS_APP_WORKFLOW_TRIGGER"]
@@ -4205,7 +4205,7 @@ rules:
4205
4205
  proves:
4206
4206
  - {path: packages/hfs/src/hfs-deploy-workflow.spec.mjs, editions: [lite, full]}
4207
4207
  exemplifiedBy:
4208
- - {path: packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml, edition: lite}
4208
+ - {path: packages/hfs/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml, edition: lite}
4209
4209
  - {path: packages/hfs/templates/app/ci-workflows/github/workflows/ci.yml, edition: full}
4210
4210
  - {path: packages/hfs/src/hfs-deploy-workflow.spec.mjs, editions: [lite, full]}
4211
4211
  witness:
@@ -4224,7 +4224,7 @@ rules:
4224
4224
  proves:
4225
4225
  - {path: packages/hfs/src/hfs-script-hygiene.spec.mjs, editions: [lite, full]}
4226
4226
  exemplifiedBy:
4227
- - {path: packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml, edition: lite}
4227
+ - {path: packages/hfs/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml, edition: lite}
4228
4228
  - {path: packages/hfs/templates/app/ci-workflows/github/workflows/ci.yml, edition: full}
4229
4229
  - {path: packages/hfs/src/hfs-script-hygiene.spec.mjs, editions: [lite, full]}
4230
4230
  witness:
@@ -4331,7 +4331,7 @@ rules:
4331
4331
  editionWhy: "owner:lite-backing-services makes the db service lite's database; database configuration, extension, migration and seed requirements apply to that backing."
4332
4332
  docs: [docs/microservices.md#r259]
4333
4333
  templateNa: "Extension use, contract migrations, vault operations and seed literals are product-authored db content, rather than a scaffolded SQL surface."
4334
- law: "DB_CONFIG_EXT: extensions belong to the database allowlist, vault writes and migration role passwords are refused, and seed data holds no real email or phone. Destructive DDL belongs only to *_contract_* migrations whose targets are no longer referenced by back-end SQL or entities. Seeds contain no real email or phone literals."
4334
+ law: "DB_CONFIG_EXT: extensions belong to the database allowlist, vault writes and migration role passwords are refused, and seed data holds no real email or phone. Destructive DDL belongs only to *_contract_* migrations whose targets are no longer referenced by back-end SQL or entities, and a contract migration that is new since the previous release tag (the newest v* tag HEAD contains) never destroys an object that the back-end sources of that tag still reference: the deploy applies it right after the accept, while that release is the rollback target, so the code change goes in one release and its contract migration in the next. Seeds contain no real email or phone literals."
4335
4335
  kinds: [check]
4336
4336
  gates: [settle, land, ci]
4337
4337
  failureCodes: ["DB_EXTENSION_ALLOWED", "DB_MIGRATION_DESTRUCTIVE", "HFS_SEED_PII", "DB_VAULT_WRITE"]
@@ -4600,7 +4600,7 @@ rules:
4600
4600
  - {path: packages/hfs/src/hfs-doc-fresh.spec.mjs, edition: full}
4601
4601
  - id: R271
4602
4602
  code: "HFS_RELEASE_TAG_MISMATCH"
4603
- law: "A lite or full product release uses a v-prefixed semantic version tag equal to package.json version; images.yml publishes images under that release tag, and every database deployment refuses a non-tag ref before migration."
4603
+ law: "A lite or full product release uses a v-prefixed semantic version tag equal to package.json version; images.yml pushes each image under the immutable tag sha-<github.sha> and adds the release name v<version> only as a retag of that digest on a release tag run, under a guard that rejects any other ref; and every database deployment refuses a non-tag ref before migration (a lite app migrates inside its stack deploy, which stack-deploy.yml starts only from a release tag, so its db-deploy.yml is retired and starci app sync removes it)."
4604
4604
  kinds: [check]
4605
4605
  gates: ["settle","land","ci"]
4606
4606
  failureCodes: ["HFS_RELEASE_TAG_MISMATCH", "HFS_DB_DEPLOY_UNGUARDED"]
@@ -4614,7 +4614,7 @@ rules:
4614
4614
  exemplifiedBy:
4615
4615
  - {"path":"packages/hfs/templates/app/ci-workflows-lite/github/workflows/images.yml","edition":"lite"}
4616
4616
  - {"path":"packages/hfs/templates/app/ci-workflows/github/workflows/images.yml","edition":"full"}
4617
- - {"path":"packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml","edition":"lite"}
4617
+ - {"path":"packages/hfs/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml","edition":"lite"}
4618
4618
  - {path: packages/hfs/src/hfs-release-provenance.spec.mjs, edition: lite}
4619
4619
  - {path: packages/hfs/src/hfs-release-provenance.spec.mjs, edition: full}
4620
4620
  witness:
@@ -5979,3 +5979,93 @@ rules:
5979
5979
  source: owner
5980
5980
  quote: "A list inside another surface is outlined (BOUNDARY-5, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); SurfaceListCard takes its depth from the surface context (grammar 0.16.2)."
5981
5981
  ruling: "SurfaceListCard resolves its depth from the surface context like SurfaceCard and SurfaceAccordionCard; an app that forces depth=\"top\" on a surface nested in another surface raises a surface inside a surface, so the lint refuses it."
5982
+ - id: R341
5983
+ code: "FE_FREESTYLE_REASON_MISSING"
5984
+ law: "A Grammar Freestyle region, the one sanctioned place where an app writes its own plain elements and CSS because no Grammar component fits, names its reason: a non-empty string literal saying why. The rules that judge hand-written elements, classes, styles and generated art leave the raw elements of a Freestyle subtree alone; a Grammar component inside it is judged as before. An element with a spread attribute is not judged."
5985
+ kinds: [lint]
5986
+ gates: [pre-commit, settle, land, ci]
5987
+ failureCodes: ["FE_FREESTYLE_REASON_MISSING"]
5988
+ enforcers:
5989
+ - {kind: eslint-grammar-core, id: freestyle-has-reason}
5990
+ editions: [lite, full]
5991
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5992
+ templateNa: "A template writes no Freestyle region; the app adds one only when no Grammar component fits."
5993
+ proves:
5994
+ - packages/eslint-grammar-core/src/freestyle.spec.ts
5995
+ exemplifiedBy:
5996
+ - {path: packages/eslint-grammar-core/src/freestyle.spec.ts, editions: [lite, full], codes: [FE_FREESTYLE_REASON_MISSING]}
5997
+ witness:
5998
+ - {path: packages/eslint-grammar-core/src/freestyle.spec.ts, editions: [lite, full], codes: [FE_FREESTYLE_REASON_MISSING]}
5999
+ cases:
6000
+ - id: case-1
6001
+ date: "2026-10-31"
6002
+ source: owner
6003
+ quote: "cái trang này free style đi trò div và làm bth thôi, thêm 1 component Freestyle hoặc tên gì đó tương đương đi trò, ý là cho custom nhưng vẫm bám grammar trừ bất đắc dĩ ntn"
6004
+ ruling: "The Grammar has one sanctioned escape hatch, Freestyle: a region where the app writes its own plain divs and CSS because no Grammar component fits; it names its reason (the lint requires it), and the rest of the page still follows the Grammar."
6005
+ - id: R342
6006
+ code: "BE_ENTITY_EXTENDS_BASE"
6007
+ law: "An @Entity class does not repeat the columns every table shares: a database-set created_at or updated_at (CreateDateColumn, UpdateDateColumn, or a Column on those names, or on createdAt / updatedAt, with insert false, update false or a default) and, beside both, the generated uuid id are inherited from AbstractEntity or UuidAbstractEntity of @modules/platform/database (BE-PERSISTENCE-3, ruling entity-base). The finding names the base to extend: UuidAbstractEntity with the uuid key, AbstractEntity without it. A table whose timestamps the application sets (a plain Column), or that lacks one of them, extends nothing and maps its own; the base file platform/database/abstract.entity.ts is exempt."
6008
+ kinds: [lint]
6009
+ gates: [pre-commit, settle, land, ci]
6010
+ failureCodes: ["BE_ENTITY_EXTENDS_BASE"]
6011
+ enforcers:
6012
+ - {kind: eslint-be, id: entity-extends-base}
6013
+ editions: [lite, full]
6014
+ docs: [contexts/code/be/persistence.md]
6015
+ proves:
6016
+ - packages/eslint-canon-be/src/entity-base.spec.ts
6017
+ exemplifiedBy:
6018
+ - {path: packages/hfs/templates/be/table/entity.ts.tpl, editions: [lite, full]}
6019
+ - {path: packages/eslint-canon-be/src/entity-base.spec.ts, editions: [lite, full], codes: [BE_ENTITY_EXTENDS_BASE]}
6020
+ witness:
6021
+ - {path: packages/eslint-canon-be/src/entity-base.spec.ts, editions: [lite, full], codes: [BE_ENTITY_EXTENDS_BASE]}
6022
+ cases:
6023
+ - id: case-1
6024
+ date: "2026-10-31"
6025
+ source: owner
6026
+ quote: "Entities do not repeat the columns every table shares; the owner approved a lint so they never repeat them again (ruling entity-base, 2026-10-31)."
6027
+ ruling: "A table whose migration has created_at, updated_at (set by the database) and the generated uuid id maps them by extending AbstractEntity or UuidAbstractEntity from @modules/platform/database; an entity that declares them itself is a finding. A table without them extends nothing."
6028
+ - id: R343
6029
+ code: "HFS_CHECK_CACHE_FORGED"
6030
+ editions: ["lite", "full"]
6031
+ law: "An app's committed check cache (.starcistacks/cache/) is trusted in CI only as it stands at the trusted base; a record whose key matches the tree and whose findings differ from the inherited or computed result is HFS_CHECK_CACHE_FORGED (a hard blocker at any count), a touched key without a current record is HFS_CHECK_CACHE_STALE and a missing cache is HFS_CHECK_CACHE_MISSING (both report-only warnings)."
6032
+ kinds: [codemod]
6033
+ gates: [ci]
6034
+ failureCodes: ["HFS_CHECK_CACHE_FORGED", "HFS_CHECK_CACHE_STALE", "HFS_CHECK_CACHE_MISSING"]
6035
+ enforcers:
6036
+ - {kind: hfs, id: check-cache, at: packages/hfs/cache/trust.mjs}
6037
+ proves:
6038
+ - {path: packages/hfs/src/hfs-check-cache.spec.mjs, editions: [lite, full]}
6039
+ exemplifiedBy:
6040
+ - {path: packages/hfs/src/hfs-check-cache.spec.mjs, editions: [lite, full]}
6041
+ witness:
6042
+ - {path: packages/hfs/src/hfs-check-cache.spec.mjs, editions: [lite, full], codes: [HFS_CHECK_CACHE_FORGED, HFS_CHECK_CACHE_STALE, HFS_CHECK_CACHE_MISSING]}
6043
+ cases:
6044
+ - id: case-1
6045
+ date: "2026-10-31"
6046
+ source: owner
6047
+ quote: "app check cache lại vào file dc không? sau chỉ check diff; chốt starcistacks lưu hết nhé và check diff và triển cho ci và local lun."
6048
+ ruling: "The app commits its check results in .starcistacks/cache/; CI trusts only the cache at the trusted base, a forged record blocks at any count, stale and missing only warn (ruling app-check-cache)."
6049
+ - id: R344
6050
+ code: "BE_AI_VIRTUAL_KEY"
6051
+ law: "An app's ai-router gateway has one client key, STARCI_AI_KEY (sk-starci-<app>-<48 hex>, sealed in the ai member, used by the integrations/ai LangChain client and the agent runtime alike), and no admin token: integrations/ai reads exactly that secret name with env.secret, as a literal, and the custody owner integrations/ai-gateway passes the gateway no adminToken."
6052
+ kinds: [lint]
6053
+ gates: [pre-commit, settle, land, ci]
6054
+ failureCodes: ["BE_AI_VIRTUAL_KEY"]
6055
+ enforcers:
6056
+ - {kind: eslint-be, id: ai-virtual-key-name}
6057
+ editions: [lite, full]
6058
+ docs: [contexts/code/be/ai.md]
6059
+ templateNa: "The skeleton scaffolds no model call; an app adds the ai and ai-gateway integrations and the ai member when it calls a model."
6060
+ proves:
6061
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_VIRTUAL_KEY]}
6062
+ exemplifiedBy:
6063
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_VIRTUAL_KEY]}
6064
+ witness:
6065
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_VIRTUAL_KEY]}
6066
+ cases:
6067
+ - id: case-1
6068
+ date: "2026-10-31"
6069
+ source: owner
6070
+ quote: "Xóa ai api key và openclaw ai key chỉnh thành starci ai key là key ảo"
6071
+ ruling: "Per app and environment the ai member holds the upstream provider keys and one random virtual key STARCI_AI_KEY that the gateway mounts and every client uses; the gateway has no admin API (ruling be-ai-one-virtual-key)."
@@ -7,7 +7,7 @@
7
7
  # catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
8
8
  # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
9
9
  schema: starci/hfs-slots@3
10
- version: 3.14.0
10
+ version: 3.15.0
11
11
 
12
12
  versioning:
13
13
  # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
@@ -58,6 +58,7 @@ versioning:
58
58
  # 3.13.0 (rulings lite-pgmq-bus-bullmq-jobs and lite-db-image-in-stacks, 2026-10-29) is a change for lite only: ruleParams.db.extensions
59
59
  # admits pgmq, the event bus, ruleParams.db.queueOutbox is retired with the pg_cron half of R258, the lite event-bus functions replace
60
60
  # event_bus_write in ruleParams.be.dbFunctions, and app.db.image moves to .starcistacks/images/db/Dockerfile (db/ keeps schema and data).
61
+ # 3.15.0 (ruling app-check-cache, 2026-10-31) adds the optional app.starcistacks.cache, the committed check cache of the app (.starcistacks/cache/, both editions).
61
62
  # 3.14.0 (ruling app-secrets-one-standard, 2026-10-29) adds the optional app.ci-stack-key-restore, the managed workflow that writes the
62
63
  # deploy server's age identity back from its GitHub Environment backup.
63
64
 
@@ -645,7 +646,7 @@ slots:
645
646
  since: 3.1.0
646
647
  profiles: [app]
647
648
  editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite renders its image workflow; full renders images for its separately deployed services."
648
- # Builds every image of the app (one matrix entry per declared app, path-filtered), never pushes: rendered by starci app sync.
649
+ # The reusable workflow ci.yml calls: builds every image of the app once per commit beside the checks, pushes the deployed ones to the registry by sha-<sha> with SBOM and provenance. Rendered by starci app sync.
649
650
  path: ".github/workflows/images.yml"
650
651
  presence: required
651
652
  tracked: tracked
@@ -657,23 +658,23 @@ slots:
657
658
  - id: app.ci-db
658
659
  profiles: [app]
659
660
  exemplifiedBy:
660
- - packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
661
661
  - {path: packages/hfs/src/hfs-release-provenance.spec.mjs, edition: full}
662
- editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its database deploy workflow; full owns an optional provider-specific workflow."
663
- # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
662
+ editionWhy: "ruling:app-cicd-auto-deploy: a lite app migrates in the migrate phase of its stack deploy, so its database deploy workflow is retired (starci app sync --execute removes it); full owns an optional provider-specific workflow."
663
+ # The database deploy workflow: workflow_dispatch only (like app.ci-e2e). A lite app has none: the stack deploy backs up and migrates inside the database container.
664
664
  path: ".github/workflows/db-deploy.yml"
665
665
  presence: optional
666
+ litePresence: forbidden # ruling:app-cicd-auto-deploy: retired in lite; migrations are the deploy's migrate phase
666
667
  tracked: tracked
667
668
  tier: none
668
669
  tests: none
669
- liteManagedBy: ci-workflows-lite
670
+ goesTo: "the stack deploy: starci app stack deploy takes a verified backup, applies the expand migrations before the services update and the contract migrations after the accept (a full app keeps its own optional workflow)"
670
671
  rules: [HFS_MANAGED_FILE_DRIFT]
671
672
  since: 2.1.0
672
673
  - id: app.ci-stack-deploy
673
674
  since: 3.3.0
674
675
  profiles: [app]
675
676
  editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): both editions deploy their images to a Docker Swarm stack through the same starci stack verbs; the workflow lists the images the app declares."
676
- # The Swarm stack deploy workflow: workflow_dispatch only (like app.ci-db); a thin file that calls `starci app stack`, rendered by starci app sync.
677
+ # The Swarm stack deploy workflow: reusable (ci.yml calls it after the verdict for every environment with deploy.auto) and workflow_dispatch for the owner's manual operations; a thin file that calls `starci app stack`, rendered by starci app sync.
677
678
  path: ".github/workflows/stack-deploy.yml"
678
679
  presence: optional
679
680
  tracked: tracked
@@ -1023,6 +1024,17 @@ slots:
1023
1024
  tests: none
1024
1025
  since: 3.11.0
1025
1026
  rules: [HFS_MANAGED_FILE_DRIFT]
1027
+ - id: app.starcistacks.cache
1028
+ profiles: [app]
1029
+ # The committed result cache of `starci app check` and `starci app lint` (ruling app-check-cache, contexts/project/check-cache.md): JSON Lines records keyed by
1030
+ # content, one folder per family. App-owned and written only by the CLI; sync never renders, overwrites or judges it. Both editions.
1031
+ path: ".starcistacks/cache/"
1032
+ presence: optional
1033
+ tracked: tracked
1034
+ tier: none
1035
+ tests: none
1036
+ since: 3.15.0
1037
+ allows: ["fingerprint.jsonl", "check/**", "lint/**", "test-coverage/**", "sonar-baseline/**", "codecov-baseline/**"]
1026
1038
  - id: app.db.bootstrap
1027
1039
  profiles: [app]
1028
1040
  editions: [lite]
@@ -1987,7 +1999,7 @@ slots:
1987
1999
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts; plus src/tests/integration/<provider>/ (R112)
1988
2000
  coverage: required
1989
2001
  budget: {indexExports: 80} # a provider's port surface includes clients, config, options, tokens and errors
1990
- rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING, BE_AI_KEY_CUSTODY, BE_AI_GATEWAY_CLIENT]
2002
+ rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING, BE_AI_KEY_CUSTODY, BE_AI_GATEWAY_CLIENT, BE_AI_VIRTUAL_KEY]
1991
2003
  - id: be.integrations.model
1992
2004
  since: 3.1.0
1993
2005
  profiles: [be]