@starci/hfs 3.0.0 → 4.0.1

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 (199) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +25 -21
  3. package/bin/hfs.mjs +59 -37
  4. package/lint/run.mjs +70 -39
  5. package/package.json +2 -2
  6. package/runtime/engine/admission.mjs +3 -3
  7. package/runtime/engine/ledger-db.mjs +2 -2
  8. package/runtime/engine/machine-db.mjs +90 -9
  9. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
  10. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +28 -9
  12. package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
  13. package/runtime/knowledge/hfs/slots.yaml +193 -128
  14. package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
  15. package/runtime/modules/kernel/failure-codes.yaml +23 -32
  16. package/runtime/scripts/checks/architecture/backend.mjs +1 -1
  17. package/runtime/scripts/checks/architecture/config.mjs +31 -11
  18. package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
  19. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
  20. package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
  21. package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
  22. package/runtime/scripts/checks/architecture/hfs.mjs +104 -66
  23. package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
  24. package/runtime/scripts/checks/architecture/registration.mjs +1 -1
  25. package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
  26. package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
  27. package/runtime/scripts/checks/architecture/typescript.mjs +45 -20
  28. package/runtime/scripts/checks/typescript-programs.mjs +2 -2
  29. package/runtime/scripts/lib/hfs-check.mjs +156 -141
  30. package/runtime/scripts/lib/hfs-path-findings.mjs +13 -2
  31. package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
  32. package/runtime/scripts/lib/hfs-rules/deps.mjs +4 -2
  33. package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
  34. package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
  35. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
  36. package/runtime/scripts/lib/hfs-slots.mjs +244 -61
  37. package/runtime/scripts/lib/hfs-view.mjs +9 -7
  38. package/runtime/scripts/lib/language.mjs +11 -1
  39. package/runtime/scripts/lib/safe-remove.mjs +95 -10
  40. package/scaffold/app.mjs +205 -0
  41. package/scaffold/service.mjs +26 -16
  42. package/sync/cli.mjs +1 -1
  43. package/sync/hygiene.mjs +11 -8
  44. package/sync/index.mjs +109 -111
  45. package/sync/managed.mjs +9 -8
  46. package/sync/sonar-key.mjs +20 -22
  47. package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +4 -2
  48. package/templates/app/gitignore +6 -0
  49. package/templates/app/hooks/husky/pre-commit +25 -0
  50. package/templates/app/hooks/husky/pre-push +7 -0
  51. package/templates/app/package-scripts/package.json +22 -0
  52. package/templates/{be → app}/quality-config/sonar-project.properties +3 -2
  53. package/templates/app/skeleton/.editorconfig +15 -0
  54. package/templates/app/skeleton/.gitattributes +2 -0
  55. package/templates/app/skeleton/.nvmrc +1 -0
  56. package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
  57. package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
  58. package/templates/app/skeleton/README.md +36 -0
  59. package/templates/app/skeleton/scripts/codegen.mjs +4 -0
  60. package/templates/{fe → app}/tool-config/prettierignore +4 -1
  61. package/templates/be/skeleton/.sops.yaml +2 -0
  62. package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
  63. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
  64. package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
  65. package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
  66. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
  67. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
  68. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
  69. package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
  70. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
  71. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
  72. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
  73. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
  74. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
  75. package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
  76. package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
  77. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
  78. package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
  79. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
  80. package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
  81. package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
  82. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
  83. package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
  84. package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
  85. package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
  86. package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
  87. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
  88. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
  89. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
  90. package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
  91. package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
  92. package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
  93. package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
  94. package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
  95. package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
  96. package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
  97. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
  98. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
  99. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
  100. package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
  101. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
  102. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
  103. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
  104. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
  105. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
  106. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
  107. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
  108. package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
  109. package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
  110. package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
  111. package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
  112. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
  113. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
  114. package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
  115. package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
  116. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
  117. package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
  118. package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
  119. package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
  120. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
  121. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
  122. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
  123. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
  124. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
  125. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
  126. package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
  127. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
  128. package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
  129. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
  130. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
  131. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
  132. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
  133. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
  134. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
  135. package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
  136. package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
  137. package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
  138. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
  139. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
  140. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
  141. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
  142. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
  143. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
  144. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
  145. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
  146. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
  147. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
  148. package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
  149. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
  150. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
  151. package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
  152. package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
  153. package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
  154. package/sync/skeleton.mjs +0 -76
  155. package/templates/be/gitignore +0 -2
  156. package/templates/be/hooks/husky/pre-commit +0 -13
  157. package/templates/be/hooks/husky/pre-push +0 -6
  158. package/templates/be/package-scripts/package.json +0 -19
  159. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  160. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
  161. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
  162. package/templates/be/tool-config/prettierignore +0 -8
  163. package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -40
  164. package/templates/fe/gitignore +0 -3
  165. package/templates/fe/hooks/husky/pre-commit +0 -16
  166. package/templates/fe/hooks/husky/pre-push +0 -5
  167. package/templates/fe/package-scripts/package.json +0 -13
  168. package/templates/fe/parts/api-client.ts +0 -44
  169. package/templates/fe/parts/api-outcome.ts +0 -7
  170. package/templates/fe/quality-config/sonar-project.properties +0 -8
  171. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  172. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
  173. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
  174. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
  175. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
  176. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
  177. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
  178. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
  179. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
  180. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
  181. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
  182. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
  183. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
  184. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
  185. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
  186. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
  187. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
  188. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
  189. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
  190. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
  191. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
  192. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
  193. package/templates/fe/tool-config/prettierrc +0 -1
  194. /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
  195. /package/templates/{be → app}/starciwork.gitignore +0 -0
  196. /package/templates/{be → app}/tool-config/prettierrc +0 -0
  197. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
  198. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
  199. /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
@@ -1,4 +1,4 @@
1
- // engine/machine-db.mjs — the ONE writer of machine.sqlite (DBTREE.sql Part B, schema 'starci/machine@1', user_version 1).
1
+ // engine/machine-db.mjs — the ONE writer of machine.sqlite (DBTREE.sql Part B, schema 'starci/machine@1', user_version MACHINE_VERSION).
2
2
  //
3
3
  // machine.sqlite is the host's single operational store: the ledger registry, the Supervisor (sup_*), the engine
4
4
  // reconciler (process_runs, engine_leader, leader_history, schedules, engine_actions, controller_modes, sla_episodes),
@@ -24,8 +24,9 @@
24
24
  // and the next flushOutbox (the land gate) applies it. Only idempotent writers are deferrable (DEFERRABLE).
25
25
  // reader : readOnly, query_only=ON, busy_timeout=15000
26
26
  // startup : sqlite_version, node_version, journal_mode and user_version are recorded in machine_meta; an old-schema file
27
- // (anything that is not 'starci/machine@1') is refused, never migrated — a fresh machine.sqlite is
28
- // created by openMachine on first use.
27
+ // (anything that is not 'starci/machine@1') is refused — a fresh machine.sqlite is created by openMachine on
28
+ // first use. A starci/machine@1 file at an older user_version is brought forward by migrateMachine
29
+ // (engine/migrations/machine/000N-*.sql after 0001-init) on the first writer open.
29
30
  // Nothing outside engine/ opens machine.sqlite with `new DatabaseSync`: callers use openMachine / openMachineReader /
30
31
  // withMachine / readMachine and the typed functions on the handle.
31
32
  import fs from 'node:fs';
@@ -42,7 +43,9 @@ import { redactBytes, redactData, redactText } from '../scripts/lib/redact.mjs';
42
43
  const require = createRequire(import.meta.url);
43
44
  const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
44
45
  export const MACHINE_SCHEMA = 'starci/machine@1';
45
- export const MACHINE_VERSION = 1;
46
+ export const MACHINE_VERSION = 2;
47
+ /** Forward migrations after 0001-init, in order; each bumps user_version to its `version` (migrateMachine). */
48
+ export const MACHINE_MIGRATIONS = Object.freeze([{ version: 2, name: '0002-worktrees-no-workflow-kind' }]);
46
49
  export const MACHINE_BUSY_TIMEOUT_MS = 15000;
47
50
  /** Test seam: STARCI_MACHINE_BUSY_TIMEOUT_MS (a positive integer) replaces the writer's busy_timeout; unset in production. */
48
51
  export const busyTimeoutOf = (env = process.env) => { const n = Number(env?.STARCI_MACHINE_BUSY_TIMEOUT_MS); return Number.isInteger(n) && n > 0 ? n : MACHINE_BUSY_TIMEOUT_MS; };
@@ -256,7 +259,7 @@ export const compareRevs = (a, b) => { const t = (r) => (/^\d{13}:/.test(String(
256
259
 
257
260
  /** An old or foreign store: refuse it; a fresh store is created by openMachine on first use. */
258
261
  function refuseOld(file, why) {
259
- throw Object.assign(Error(`machine-schema-old: ${path.resolve(file)} ${why}; this runtime opens only '${MACHINE_SCHEMA}' (user_version ${MACHINE_VERSION}) — move the refused file aside and openMachine creates a fresh machine.sqlite on first use`),
262
+ throw Object.assign(Error(`machine-schema-old: ${path.resolve(file)} ${why}; this runtime opens only '${MACHINE_SCHEMA}' (user_version 1..${MACHINE_VERSION}) — move the refused file aside and openMachine creates a fresh machine.sqlite on first use`),
260
263
  { code: 'STARCI_MACHINE_SCHEMA_OLD' });
261
264
  }
262
265
 
@@ -267,7 +270,51 @@ function checkSchema(db, file) {
267
270
  const schema = db.prepare("SELECT value FROM machine_meta WHERE key='schema'").get()?.value;
268
271
  if (schema !== MACHINE_SCHEMA) refuseOld(file, `is schema '${schema ?? 'none'}'`);
269
272
  if (version > MACHINE_VERSION) throw Object.assign(Error(`machine-schema-newer: ${file} user_version ${version} > ${MACHINE_VERSION}; upgrade the runtime`), { code: 'STARCI_MACHINE_SCHEMA_NEWER' });
270
- if (version !== MACHINE_VERSION) refuseOld(file, `is user_version ${version}`);
273
+ // 1..MACHINE_VERSION: an older version is brought forward on the first writer open (migrateMachine); a reader reads it as is.
274
+ if (!(version >= 1)) refuseOld(file, `is user_version ${version}`);
275
+ }
276
+
277
+ /**
278
+ * Bring an older machine.sqlite forward: for each MACHINE_MIGRATIONS entry above user_version and not yet in schema_migrations -
279
+ * integrity_check and a VACUUM INTO backup (`<file>.pre-<name>.bak`, skipped for a store created by this open) first, then in one
280
+ * transaction the migration SQL, schema_version+1, user_version, its schema_migrations row, foreign_key_check and quick_check
281
+ * before COMMIT. Idempotent and safe across processes (re-checked under the lock). Returns the names applied.
282
+ */
283
+ export function migrateMachine(db, { file, now = Date.now, backup: wantBackup = true } = {}) {
284
+ const version = () => Number(pragma(db, 'user_version'));
285
+ const pending = () => MACHINE_MIGRATIONS.filter((m) => version() < m.version && !db.prepare('SELECT 1 FROM schema_migrations WHERE version=?').get(m.version));
286
+ if (!pending().length) return [];
287
+ const check = db.prepare('PRAGMA integrity_check').all();
288
+ need(check.length === 1 && check[0].integrity_check === 'ok', `machine-migrate-refused: ${file} fails integrity_check`, 'STARCI_MACHINE_MIGRATE_REFUSED');
289
+ const applied = [];
290
+ for (const m of pending()) {
291
+ const sql = fs.readFileSync(path.join(ENGINE_DIR, 'migrations', 'machine', `${m.name}.sql`), 'utf8');
292
+ const backup = wantBackup && file ? `${path.resolve(file)}.pre-${m.name}.bak` : null;
293
+ if (backup && !fs.existsSync(backup)) db.exec(`VACUUM INTO '${backup.replace(/'/g, "''")}'`);
294
+ db.exec('BEGIN IMMEDIATE');
295
+ let relaxed = false;
296
+ try {
297
+ if (version() >= m.version) { db.exec('ROLLBACK'); continue; }
298
+ if (typeof db.enableDefensive === 'function') { db.enableDefensive(false); relaxed = true; } // writable_schema is refused while defensive
299
+ const started = now();
300
+ db.exec(sql);
301
+ db.exec(`PRAGMA schema_version=${Number(pragma(db, 'schema_version')) + 1}`);
302
+ db.exec('PRAGMA writable_schema=OFF');
303
+ db.exec(`PRAGMA user_version=${m.version}`);
304
+ db.prepare("INSERT INTO schema_migrations(version,name,runtime_rev,sql_sha256,backup_path,started_at,finished_at,status) VALUES(?,?,?,?,?,?,?,'done')")
305
+ .run(m.version, m.name, runtimeRev(), sha256(sql), backup, started, now());
306
+ need(db.prepare('PRAGMA foreign_key_check').all().length === 0, `machine-migrate-failed: ${m.name} left a foreign key violation`, 'STARCI_MACHINE_MIGRATE_FAILED');
307
+ const quick = db.prepare('PRAGMA quick_check').all();
308
+ need(quick.length === 1 && quick[0].quick_check === 'ok', `machine-migrate-failed: ${m.name} fails quick_check`, 'STARCI_MACHINE_MIGRATE_FAILED');
309
+ db.exec('COMMIT');
310
+ applied.push(m.name);
311
+ } catch (error) {
312
+ try { db.exec('ROLLBACK'); } catch { /* none */ }
313
+ try { db.exec('PRAGMA writable_schema=OFF'); } catch { /* none */ }
314
+ throw error;
315
+ } finally { if (relaxed) db.enableDefensive(true); }
316
+ }
317
+ return applied;
271
318
  }
272
319
 
273
320
  function createSchema(db, { file, env, now }) {
@@ -288,7 +335,7 @@ function createSchema(db, { file, env, now }) {
288
335
  db.prepare("INSERT INTO mode_changes(controller,from_mode,to_mode,by,reason,at) VALUES(?,NULL,'shadow','machine-db:init','0001-init',?)").run(controller, at);
289
336
  db.prepare("INSERT INTO controller_modes(controller,mode,set_at,set_by) VALUES(?,'shadow',?,'machine-db:init')").run(controller, at);
290
337
  }
291
- db.exec(`PRAGMA user_version=${MACHINE_VERSION}`);
338
+ db.exec('PRAGMA user_version=1'); // 0001; the forward files follow in migrateMachine
292
339
  }
293
340
  db.exec('COMMIT');
294
341
  } catch (error) { try { db.exec('ROLLBACK'); } catch { /* none */ } throw error; }
@@ -327,6 +374,7 @@ function openConnection(file, { readOnly = false, env = process.env, now = Date.
327
374
  createSchema(db, { file, env, now });
328
375
  }
329
376
  checkSchema(db, file);
377
+ migrateMachine(db, { file, now, backup: !fresh });
330
378
  recordFacts(db);
331
379
  return db;
332
380
  } catch (error) {
@@ -1254,6 +1302,39 @@ const pushes = (m, { repoRoot = null, limit = 50 } = {}) => m.db.prepare(`SELECT
1254
1302
  const upsertWorktree = (m, wt) => upsertRow(m.db, 'worktrees', { created_at: m.now(), ...snake(wt), path: path.resolve(wt.path) }, ['path']);
1255
1303
  const removedWorktree = (m, wtPath, { error = null, archivedRef = null } = {}) => m.db.prepare('UPDATE worktrees SET removed_at=CASE WHEN ? IS NULL THEN ? ELSE removed_at END, remove_error=?, archived_ref=COALESCE(?,archived_ref) WHERE path=?')
1256
1304
  .run(error, m.now(), error, archivedRef, path.resolve(wtPath)).changes > 0;
1305
+ /**
1306
+ * The worktree registry (scripts/lib/worktrees.mjs is its one caller): reserve a row for a worktree about to be created,
1307
+ * atomically against the per-repo cap. BEGIN IMMEDIATE: two dispatches never both take the last slot. `cap` null: no cap.
1308
+ * {ok, live} | {ok:false, reason:'worktree-cap', live, cap}
1309
+ */
1310
+ function reserveWorktree(m, wt, { cap = null } = {}) {
1311
+ return m.transaction((db) => {
1312
+ const repoRoot = repoKey(wt.repoRoot);
1313
+ const target = path.resolve(wt.path);
1314
+ const live = db.prepare('SELECT path FROM worktrees WHERE repo_root=? AND removed_at IS NULL').all(repoRoot).filter((r) => r.path !== target).length;
1315
+ if (cap != null && live >= cap) return { ok: false, reason: 'worktree-cap', live, cap };
1316
+ upsertRow(db, 'worktrees', { ...snake(wt), path: target, repo_root: repoRoot, created_at: m.now(), removed_at: null, remove_error: null }, ['path']);
1317
+ return { ok: true, live: live + 1 };
1318
+ });
1319
+ }
1320
+ /** Live (not removed) registry rows, optionally of one repo. */
1321
+ const liveWorktrees = (m, { repoRoot = null } = {}) => m.db.prepare(`SELECT * FROM worktrees WHERE removed_at IS NULL ${repoRoot ? 'AND repo_root=?' : ''} ORDER BY created_at`)
1322
+ .all(...(repoRoot ? [repoKey(repoRoot)] : []));
1323
+ const worktreeRow = (m, wtPath) => m.db.prepare('SELECT * FROM worktrees WHERE path=?').get(path.resolve(wtPath)) ?? null;
1324
+ /** Every repo the registry ever held a worktree of. */
1325
+ const worktreeRepos = (m) => m.db.prepare('SELECT DISTINCT repo_root FROM worktrees ORDER BY repo_root').all().map((r) => r.repo_root);
1326
+ /** A reservation whose `git worktree add` failed: the row goes, nothing was created. */
1327
+ /** A GC collector's resume point (machine_meta gc_cursor:<name>): where its last bounded pass stopped, or null. */
1328
+ const gcCursor = (m, name) => m.db.prepare('SELECT value FROM machine_meta WHERE key=?').get(`gc_cursor:${name}`)?.value ?? null;
1329
+ const setGcCursor = (m, name, value) => (value == null
1330
+ ? m.db.prepare('DELETE FROM machine_meta WHERE key=?').run(`gc_cursor:${name}`)
1331
+ : m.db.prepare('INSERT INTO machine_meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(`gc_cursor:${name}`, String(value))).changes > 0;
1332
+ /** The worktree GC's stop mark (a removal changed the main checkout): {at, damage, path} or null. set(null) clears it. */
1333
+ const worktreeGcStop = (m) => parse(m.db.prepare("SELECT value FROM machine_meta WHERE key='worktree_gc_stopped'").get()?.value ?? null);
1334
+ const setWorktreeGcStop = (m, stop) => (stop
1335
+ ? m.db.prepare("INSERT INTO machine_meta(key,value) VALUES('worktree_gc_stopped',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(stop))
1336
+ : m.db.prepare("DELETE FROM machine_meta WHERE key='worktree_gc_stopped'").run()).changes > 0;
1337
+ const dropWorktree = (m, wtPath) => m.db.prepare('DELETE FROM worktrees WHERE path=? AND removed_at IS NULL').run(path.resolve(wtPath)).changes > 0;
1257
1338
  const upsertEnvServer = (m, server) => upsertRow(m.db, 'env_servers', snake(server), ['server_id']);
1258
1339
  const envServer = (m, serverId) => m.db.prepare('SELECT * FROM env_servers WHERE server_id=?').get(serverId) ?? null;
1259
1340
  const envServers = (m, { live = false } = {}) => m.db.prepare(`SELECT * FROM env_servers ${live ? "WHERE state IN ('starting','ready')" : ''} ORDER BY server_id`).all();
@@ -1443,13 +1524,13 @@ const API = {
1443
1524
  upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, release: releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets,
1444
1525
  startGcRun, finishGcRun, recordGcItem, addGcItem: recordGcItem, updateGcItem, gcItems, gcMark, addGcMarks: gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots,
1445
1526
  upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox,
1446
- upsertWorktree, removedWorktree, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk,
1527
+ upsertWorktree, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk,
1447
1528
  log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
1448
1529
  };
1449
1530
 
1450
1531
  /** Every typed function at module level too: fn(handle, ...args) — blob-gc and callers holding a handle. */
1451
1532
  export {
1452
- putMachineBlob, jsonOrBlob, meta, checkpoint, registerLedger, resolveLedger, listLedgers, touchLedger, setLedgerState, upsertRepository, forEachLedger, attachFleet, supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, supLeases, startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport, openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, openOwed, ackOwed, closeOwed, listOwed, upsertLearning, listLearning, recordOwnerRuling, upsertBridge, recordSupMessage, supMessages, markSupMessagesRead, setSupSignal, supSignal, clearSupSignal, recordMachineLlmUsage, startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, processRuns, leaderOf, acquireLeader, renewLeader, releaseLeader, leaderHistory, cursorOf, setCursor, cursors, enqueue, dueQueue, queueRows, dequeue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules, actionIntent, actionRunning, actionFinish, markStaleActionsUnknown, actionOf, actions, actionStep, controllerModes, setControllerMode, modeChanges, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, openSla, recordViolation, clearViolation, setService, recordProbe, services, serviceEvents, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, startSeatTurn, endSeatTurn, seatTranscriptSnapshot, upsertTerminal, closeTerminal, openTerminals, acquireHostLock, renewHostLock, releaseHostLock, hostLock, hostLocks, claimResource, releaseClaim, sweptClaim, liveClaims, upsertAgentSession, inventorySnapshot, throttleState, setThrottle, throttleEvents, recordThrottleDecision, releaseThrottleDecision, recordHostSample, hostSamples, setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas, upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets, startGcRun, finishGcRun, recordGcItem, updateGcItem, gcItems, gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots, upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox, upsertWorktree, removedWorktree, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk, log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
1533
+ putMachineBlob, jsonOrBlob, meta, checkpoint, registerLedger, resolveLedger, listLedgers, touchLedger, setLedgerState, upsertRepository, forEachLedger, attachFleet, supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, supLeases, startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport, openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, openOwed, ackOwed, closeOwed, listOwed, upsertLearning, listLearning, recordOwnerRuling, upsertBridge, recordSupMessage, supMessages, markSupMessagesRead, setSupSignal, supSignal, clearSupSignal, recordMachineLlmUsage, startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, processRuns, leaderOf, acquireLeader, renewLeader, releaseLeader, leaderHistory, cursorOf, setCursor, cursors, enqueue, dueQueue, queueRows, dequeue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules, actionIntent, actionRunning, actionFinish, markStaleActionsUnknown, actionOf, actions, actionStep, controllerModes, setControllerMode, modeChanges, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, openSla, recordViolation, clearViolation, setService, recordProbe, services, serviceEvents, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, startSeatTurn, endSeatTurn, seatTranscriptSnapshot, upsertTerminal, closeTerminal, openTerminals, acquireHostLock, renewHostLock, releaseHostLock, hostLock, hostLocks, claimResource, releaseClaim, sweptClaim, liveClaims, upsertAgentSession, inventorySnapshot, throttleState, setThrottle, throttleEvents, recordThrottleDecision, releaseThrottleDecision, recordHostSample, hostSamples, setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas, upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets, startGcRun, finishGcRun, recordGcItem, updateGcItem, gcItems, gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots, upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox, upsertWorktree, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk, log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
1453
1534
  };
1454
1535
  export const addGcItem = recordGcItem;
1455
1536
  export const addGcMarks = gcMark;
@@ -0,0 +1,13 @@
1
+ -- 0002-worktrees-no-workflow-kind (starci/machine@1, user_version 1 -> 2). Applied by engine/machine-db.mjs migrateMachine on
2
+ -- the first writer open of an older machine.sqlite (after an integrity_check and a VACUUM INTO backup, with foreign_key_check
3
+ -- + quick_check before COMMIT); a fresh machine.sqlite runs 0001 then the forward files.
4
+ --
5
+ -- The per-workflow worktree is removed: a workflow owns no checkout of its own, an op works in its job worktree and a land
6
+ -- in its scratch. worktrees.kind no longer accepts 'workflow'. A row of that kind describes the removed mechanism and is
7
+ -- deleted (a directory it named, if any is left, is an unregistered worktree the worktree GC and the footprint scan report);
8
+ -- then the table's CHECK is rewritten in place (writable_schema: the views over worktrees stay as they are).
9
+ DELETE FROM worktrees WHERE kind='workflow';
10
+ PRAGMA writable_schema=ON;
11
+ UPDATE sqlite_master
12
+ SET sql=replace(sql, 'CHECK(kind IN (''op'',''workflow'',''land-scratch'',', 'CHECK(kind IN (''op'',''land-scratch'',')
13
+ WHERE type='table' AND name='worktrees';
@@ -0,0 +1,93 @@
1
+ -- 0005-ended-workflow-views (starci/runtime@1, user_version 4 -> 5). Applied by engine/ledger-db.mjs migrateLedger on the first
2
+ -- writer open of an older ledger (after a VACUUM INTO backup and an integrity_check, with foreign_key_check + quick_check
3
+ -- before COMMIT); a fresh ledger runs 0001 then the forward files.
4
+ --
5
+ -- The runtime views stop surfacing the live leftovers of an ENDED workflow (phase archived|finished): archive/finish close
6
+ -- the workflow's live Decision Items inside their own transaction before the phase flips, and events_refuse_archived refuses
7
+ -- every later write, so a leftover live DI (2026-09-30 nivo-backend di-876124d7) can never be resolved and only ever reads
8
+ -- 'bad' in the UI — a permanent attention item and blocker on work that no longer runs. Every leg carries the ENDED
9
+ -- predicate of scripts/reconciler/decisions.mjs listDecisions: LEFT JOIN workflows w; the row survives when its workflow is
10
+ -- missing or not ended (w.phase IS NULL OR w.phase NOT IN ('archived','finished')).
11
+ -- v_decision_rows keeps an ended workflow's resolved rows (decision history) and drops only its live-status leftovers;
12
+ -- v_blocking applies the join per UNION leg — an ended workflow owes no unit, decision, condition, incident,
13
+ -- worker-question, settle-tail, product-land or settle-overdue row;
14
+ -- v_open_work the same per leg (decision + job).
15
+ DROP VIEW IF EXISTS v_decision_rows;
16
+ DROP VIEW IF EXISTS v_blocking;
17
+ DROP VIEW IF EXISTS v_open_work;
18
+ CREATE VIEW v_decision_rows AS
19
+ SELECT d.*,
20
+ (d.due_at IS NOT NULL AND d.status IN ('open','claimed','escalated') AND d.due_at < CAST(unixepoch('subsec')*1000 AS INTEGER)) AS overdue,
21
+ CASE WHEN d.status IN ('open','claimed','escalated')
22
+ AND ((d.due_at IS NOT NULL AND d.due_at < CAST(unixepoch('subsec')*1000 AS INTEGER)) OR d.escalations>=2) THEN 'bad'
23
+ WHEN d.status='open' AND d.due_at IS NOT NULL
24
+ AND (d.due_at - CAST(unixepoch('subsec')*1000 AS INTEGER)) < 0.2*(d.due_at - d.opened_at) THEN 'warn'
25
+ ELSE COALESCE(m.ui,'unknown') END AS ui
26
+ FROM decision_items d LEFT JOIN ui_state_map m ON m.entity='decision' AND m.native=d.status
27
+ LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
28
+ WHERE d.status NOT IN ('open','claimed','escalated') OR w.phase IS NULL OR w.phase NOT IN ('archived','finished');
29
+
30
+ CREATE VIEW v_blocking AS
31
+ SELECT e.workflow_id, 'unit' AS entity_type, e.to_unit AS entity_id, 'unit' AS blocker_type, e.from_unit AS blocker_id,
32
+ 'UpstreamNotDone' AS reason_code, p.state AS detail, p.updated_at AS since, 'kernel' AS who
33
+ FROM unit_edges e JOIN work_units t ON t.workflow_id=e.workflow_id AND t.unit_id=e.to_unit
34
+ JOIN work_units p ON p.workflow_id=e.workflow_id AND p.unit_id=e.from_unit
35
+ LEFT JOIN workflows w ON w.workflow_id=e.workflow_id
36
+ WHERE t.state IN ('planned','queued') AND p.state NOT IN ('done','dropped')
37
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
38
+ UNION ALL
39
+ SELECT d.workflow_id, COALESCE(d.entity_type,'workflow'), COALESCE(d.entity_id,d.workflow_id), 'decision', d.di_id,
40
+ 'DecisionOpen:'||d.kind, d.summary, d.opened_at, d.decider
41
+ FROM decision_items d LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
42
+ WHERE d.status IN ('open','claimed','escalated')
43
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
44
+ UNION ALL
45
+ SELECT c.workflow_id, c.entity_type, c.entity_id, 'condition', c.type, c.reason, c.message, c.last_transition_at, COALESCE(c.owner,'controller')
46
+ FROM conditions c LEFT JOIN workflows w ON w.workflow_id=c.workflow_id
47
+ WHERE c.status<>'True'
48
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
49
+ UNION ALL
50
+ SELECT i.workflow_id, 'job', COALESCE(i.job_id,i.workflow_id), 'incident', i.incident_id, 'IncidentOpen:'||i.kind,
51
+ COALESCE(i.detail,i.last_progress), i.created_at, i.owner
52
+ FROM incidents i LEFT JOIN workflows w ON w.workflow_id=i.workflow_id
53
+ WHERE i.status='open'
54
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
55
+ UNION ALL
56
+ SELECT x.workflow_id, 'attempt', CAST(x.attempt_id AS TEXT), 'question', CAST(x.inbox_id AS TEXT), 'WorkerQuestionPending',
57
+ json_extract(x.payload_json,'$.question'), x.created_at, 'kernel'
58
+ FROM inbox x LEFT JOIN workflows w ON w.workflow_id=x.workflow_id
59
+ WHERE x.kind='worker-question' AND x.status IN ('pending','claimed')
60
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
61
+ UNION ALL
62
+ SELECT s.workflow_id, 'attempt', CAST(s.attempt_id AS TEXT), 'settle-tail', CAST(s.attempt_id AS TEXT), 'SettleTailFailed', s.last_error,
63
+ COALESCE(s.started_at,s.queued_at), 'controller'
64
+ FROM settle_tails s LEFT JOIN workflows w ON w.workflow_id=s.workflow_id
65
+ WHERE s.state='failed'
66
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
67
+ UNION ALL
68
+ SELECT l.workflow_id, 'workflow', l.workflow_id, 'product-land', CAST(l.land_id AS TEXT), 'ProductLand:'||l.result, l.reason,
69
+ l.started_at, 'kernel'
70
+ FROM product_lands l LEFT JOIN workflows w ON w.workflow_id=l.workflow_id
71
+ WHERE l.result IN ('failed','conflict','red')
72
+ AND NOT EXISTS(SELECT 1 FROM product_lands k WHERE k.workflow_id=l.workflow_id AND k.land_id>l.land_id AND k.result='landed')
73
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
74
+ UNION ALL
75
+ -- H1: a filed report nobody settled within its SLA (the same rows as v_settle_overdue) blocks its job.
76
+ SELECT a.workflow_id, 'attempt', CAST(a.attempt_id AS TEXT), 'settle', CAST(a.attempt_id AS TEXT),
77
+ 'SettleOverdue:'||COALESCE(a.report_outcome,'?'), a.job_id, a.reported_at,
78
+ CASE WHEN a.report_outcome='done' THEN 'settler' ELSE 'kernel' END
79
+ FROM op_attempts a LEFT JOIN workflows w ON w.workflow_id=a.workflow_id
80
+ WHERE a.reported_at IS NOT NULL AND a.settled_at IS NULL AND a.end_state IS NULL
81
+ AND (CAST(unixepoch('subsec')*1000 AS INTEGER) - a.reported_at) > CASE WHEN a.report_outcome='done' THEN 180000 ELSE 900000 END
82
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'));
83
+
84
+ CREATE VIEW v_open_work AS
85
+ SELECT 'decision' AS kind, d.workflow_id, d.di_id AS ref, d.summary AS what, d.opened_at AS since, d.due_at
86
+ FROM decision_items d LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
87
+ WHERE d.status IN ('open','claimed','escalated')
88
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
89
+ UNION ALL
90
+ SELECT 'job-'||j.status, j.workflow_id, j.job_id, j.op_id, j.updated_at, j.deadline
91
+ FROM jobs j LEFT JOIN workflows w ON w.workflow_id=j.workflow_id
92
+ WHERE j.status IN ('leased','running','answering','effect_unknown')
93
+ AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'));
@@ -5,7 +5,14 @@ purpose: >-
5
5
  then the repositories' own upgrade lane (each sets the exact version and reinstalls; nothing else installs a @starci package). Read on 2026-09-29 from the packages under
6
6
  .claude/packages and from the installed node_modules of nivo-backend, nivo-fe, starci-next, starci-next-fe,
7
7
  mia-mia-backend and miamia-fe; where they disagreed the newest version installed in more than one repository
8
- won, and the runtime's own version won for @starci packages.
8
+ won, and the runtime's own version won for @starci packages. An app (hfs 4) has ONE package.json for both sides, so a
9
+ dependency both sides declared at different versions takes one: its pin where it has one, otherwise the higher of the
10
+ two, pinned here with a why (2026-10-01, from nivo-backend/nivo-fe, starci-next/starci-next-fe and
11
+ mia-mia-backend/miamia-fe: every such conflict but graphql was already pinned). The runtime's example apps merged the
12
+ same way on 2026-10-01 (examples/todo-app from todo-app-backend/todo-app-frontend, examples/ecommerce-app from
13
+ ecommerce-app-be/ecommerce-app-fe): no dependency outside this file was declared at two versions across their sides, so
14
+ every choice is a pin (each side's older @starci pins and graphql moved to the version here). `side` names the side whose code uses
15
+ the dependency; the app root package.json carries the pins of both.
9
16
  pins:
10
17
  # --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
11
18
  # installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
@@ -17,19 +24,19 @@ pins:
17
24
  source: packages/grammar/package.json
18
25
  why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
19
26
  '@starci/eslint-canon-be':
20
- version: 2.1.0
27
+ version: 3.0.1
21
28
  group: starci
22
29
  install: registry
23
30
  side: be
24
31
  source: packages/eslint/be/package.json
25
- why: '2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws (R36 sql-text-only, R83 no-repository-class, R85/R87/R89 service-deps, R43 config-parsed-in-main, R47/R48 test world, R95 operations, R98-R101 builders).'
32
+ why: '3.0.1: its bundled canon-pins copy pins hfs 4.0.1. 3.0.0: `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; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
26
33
  '@starci/eslint-canon-fe':
27
- version: 7.1.0
34
+ version: 8.0.1
28
35
  group: starci
29
36
  install: registry
30
37
  side: fe
31
38
  source: packages/eslint/fe/package.json
32
- why: '7.0.0: the front end has no tests (test rules removed, FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
39
+ why: '8.0.1: its bundled canon-pins copy pins hfs 4.0.1. 8.0.0: `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; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
33
40
  '@starci/stylelint-canon':
34
41
  version: 2.0.1
35
42
  group: starci
@@ -50,19 +57,26 @@ pins:
50
57
  side: both
51
58
  source: packages/prettier-config/package.json
52
59
  '@starci/jest-preset':
53
- version: 2.1.0
60
+ version: 2.2.0
54
61
  group: starci
55
62
  install: registry
56
63
  side: be
57
64
  source: packages/jest-preset/package.json
58
- why: '2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
65
+ why: '2.2.0: the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
66
+ '@starci/test-world':
67
+ version: 1.0.0
68
+ group: starci
69
+ install: registry
70
+ side: be
71
+ source: packages/test-world/package.json
72
+ why: 'the shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end.'
59
73
  '@starci/hfs':
60
- version: 3.0.0
74
+ version: 4.0.1
61
75
  group: starci
62
76
  install: registry
63
77
  side: both
64
78
  source: packages/hfs/package.json
65
- why: '2.0.0 (C0 release): the BE-CONVENTION machine, emit-contracts (GraphQL SDL and OpenAPI from the typed operation table), the four-project test layout, FE has no tests, repo.scripts slot, secrets guard in work-hygiene, rule ids R<2-3 digits>.'
79
+ why: '4.0.1: `hfs scaffold app` resolves the real lockfile with npm (`npm install --package-lock-only`) instead of writing a root-only stub that `npm ci` refuses. 4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
66
80
  # --- tooling
67
81
  typescript:
68
82
  version: 5.9.3
@@ -152,6 +166,11 @@ pins:
152
166
  group: framework
153
167
  side: fe
154
168
  why: every HFS front end uses next-intl with the [locale] segment (hfs sync --init writes it); nivo-fe resolves 4.13.6.
169
+ graphql:
170
+ version: 16.14.2
171
+ group: framework
172
+ side: both
173
+ why: 'one app package.json for both sides (hfs 4): nivo-backend declares ^16.14.1 and nivo-fe ^16.14.2, the higher wins; all four product installs resolve 16.14.2. The example apps: todo-app-backend declared ^16.14.2 (devDependencies) and ecommerce-app-be ^16.14.2 (dependencies and devDependencies); each app root now has one dependencies entry at the pin.'
155
174
  supertest:
156
175
  version: 7.3.0
157
176
  group: tooling
@@ -0,0 +1,18 @@
1
+ schema: starci/hfs-peer-integrations@1
2
+ purpose: >-
3
+ The runtime peers a driver integration needs and npm does not install for it: "if the app depends on every package of
4
+ `when` (at the named major, when one is named), it must declare `requires` in its dependencies". A missing peer is
5
+ invisible to the type checker and the unit specs and only fails when the api boots (todo-app and ecommerce-app,
6
+ 2026-10-01: neither old backend declared @as-integrations/express5, so the Apollo GraphQL api could not boot under
7
+ Express 5 and nothing noticed before e2e). R111 HFS_PEER_INTEGRATION_MISSING judges the app root package.json, the one
8
+ manifest of both sides, with this catalog; an entry is added the day a boot failure of the same kind is found.
9
+ pairs:
10
+ - id: nestjs-apollo-express5
11
+ when:
12
+ - {package: "@nestjs/apollo"}
13
+ - {package: "@nestjs/platform-express", major: 11}
14
+ requires: "@as-integrations/express5"
15
+ why: >-
16
+ @nestjs/platform-express 11 runs Express 5, and Apollo Server 5 serves an Express 5 app only through
17
+ @as-integrations/express5, which @nestjs/apollo loads at boot and does not depend on; without it the GraphQL api
18
+ does not start.