@starci/hfs 4.0.10 → 4.1.0

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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -1,315 +0,0 @@
1
- // blob.mjs — the content-addressed blob store (operational evidence and agent output) beside the two SQLite stores:
2
- // putBlob/getBlob/statBlob/putJson write and read it; blob bytes are immutable and never removed here.
3
- //
4
- // Reading agent output back out of the blob store (alpha.3, ARCHITECTURE-DB §5.3). A Work record and the
5
- // draw subsystem cite agent output by {artifact?, sha256}; this is the one place that turns such a citation back into
6
- // bytes, a readable file with an extension, or a directory.
7
- // resolveBlob(ref, {db}) ref = {artifact?, sha256?} | 'blob:<sha>' | '<sha>' → {sha256, file} | null (an artifact id
8
- // alone resolves through the ledger's job_artifacts)
9
- // readBlobRef(ref) the verified bytes
10
- // blobAsFile(ref, {ext}) a read-only copy with an extension beside the store (<artifact root>-views; tools that key on the
11
- // extension: an image decoder, a browser), cached by sha
12
- // putBundle(dir) every file under `dir` put as a blob + a starci/blob-bundle@1 manifest {files: {rel: sha}}
13
- // put as a blob; returns the manifest sha (a draw loop: loop.json and every round)
14
- // bundleDir(sha) the bundle materialized as a directory under <artifact root>-views (cached by sha), or null
15
- // Each materialized view is verified against its cited digest before reuse.
16
- import fs from 'node:fs';
17
- import os from 'node:os';
18
- import path from 'node:path';
19
- import crypto from 'node:crypto';
20
-
21
- export const ARTIFACT_ROOT_ENV = 'STARCI_ARTIFACT_ROOT';
22
- export const artifactRoot = (env = process.env) => path.resolve(env[ARTIFACT_ROOT_ENV] || path.join(os.homedir(), '.starci', 'artifacts'));
23
- const SHA = /^[a-f0-9]{64}$/;
24
- const assertSha = sha => {
25
- if (typeof sha !== 'string' || !SHA.test(sha)) throw new TypeError('blob sha must be a lowercase sha256 hex digest');
26
- return sha;
27
- };
28
- // An explicit root scopes READs only; writers continue to use the default external root.
29
- const location = (sha, root = null, suffix = '') => {
30
- if (root !== null && (typeof root !== 'string' || !root || !path.isAbsolute(root)))
31
- throw new TypeError('blob read root must be an absolute path');
32
- const file = path.join(root === null ? artifactRoot() : path.resolve(root), assertSha(sha).slice(0, 2), `${sha}${suffix}`);
33
- if (root !== null) {
34
- regularParents(file, { strict: true });
35
- const stat = fs.lstatSync(file, { throwIfNoEntry: false });
36
- if (stat && (!stat.isFile() || stat.isSymbolicLink()
37
- || fs.realpathSync.native(file) !== path.join(fs.realpathSync.native(path.dirname(file)), path.basename(file))
38
- || !fs.readdirSync(path.dirname(file)).includes(path.basename(file))))
39
- throw new Error(`blob read path is not a contained regular file: ${file}`);
40
- }
41
- return file;
42
- };
43
- const metadataPath = (sha, root = null) => location(sha, root, '.json');
44
- const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
45
-
46
- // Reject roots inside a checkout, including worktrees whose .git is a file.
47
- function ensureExternalRoot(root) {
48
- let cursor = root;
49
- while (true) {
50
- if (fs.existsSync(path.join(cursor, '.git'))) throw new Error(`artifact root is inside a git checkout: ${root}`);
51
- const parent = path.dirname(cursor);
52
- if (parent === cursor) return;
53
- cursor = parent;
54
- }
55
- }
56
-
57
- function sourceBytes(bufferOrPath) {
58
- if (Buffer.isBuffer(bufferOrPath)) return bufferOrPath;
59
- if (bufferOrPath instanceof Uint8Array) return Buffer.from(bufferOrPath);
60
- if (typeof bufferOrPath === 'string') return fs.readFileSync(bufferOrPath);
61
- throw new TypeError('putBlob expects a Buffer, Uint8Array, or file path');
62
- }
63
-
64
- function publishOnce(destination, bytes) {
65
- const dir = path.dirname(destination);
66
- fs.mkdirSync(dir, { recursive: true });
67
- const temp = path.join(dir, `.${path.basename(destination)}.${process.pid}.${crypto.randomUUID()}.tmp`);
68
- let owned=false;
69
- try {
70
- const fd=fs.openSync(temp,'wx');
71
- owned=true;
72
- try{fs.writeFileSync(fd,bytes);fs.fsyncSync(fd);}finally{fs.closeSync(fd);}
73
- try { fs.linkSync(temp, destination); }
74
- catch (error) { if (error.code !== 'EEXIST') throw error; }
75
- } finally { if(owned)try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
76
- }
77
-
78
- /** Store original bytes under their sha256. A concurrent or repeated put never replaces published bytes. */
79
- export function putBlob(bufferOrPath, { mediaType = 'application/octet-stream' } = {}) {
80
- if (typeof mediaType !== 'string' || !mediaType.trim()) throw new TypeError('mediaType must be a nonempty string');
81
- const root = artifactRoot();
82
- ensureExternalRoot(root);
83
- const bytes = sourceBytes(bufferOrPath);
84
- const sha = digest(bytes);
85
- const destination = location(sha);
86
- const metadata = { size: bytes.length, mediaType, createdAt: new Date().toISOString() };
87
- // Metadata is published first so a visible blob always has a sidecar. A crash can leave an orphan sidecar;
88
- // the next put completes it. The first published media type and creation time win.
89
- publishOnce(metadataPath(sha), Buffer.from(JSON.stringify(metadata)));
90
- publishOnce(destination, bytes);
91
- const stored = fs.statSync(destination);
92
- if (stored.size !== bytes.length) throw new Error(`blob size mismatch: ${sha}`);
93
- // Reuse must verify bytes, including a corrupt same-size destination.
94
- getBlob(sha);
95
- const info = statBlob(sha);
96
- // Flush reused files too. Directory/link persistence remains platform-specific;
97
- // this local profile does not promise power-loss recovery of acknowledged refs.
98
- for(const file of [destination,metadataPath(sha)]){const fd=fs.openSync(file,'r+');try{fs.fsyncSync(fd);}finally{fs.closeSync(fd);}}
99
- return { sha, size: info.size, mediaType: info.mediaType };
100
- }
101
-
102
- export function blobPath(sha, { root = null } = {}) {
103
- const file = location(sha, root);
104
- return fs.existsSync(file) && fs.statSync(file).isFile() ? file : null;
105
- }
106
-
107
- export function getBlob(sha, { root = null, verifyBundle = false } = {}) {
108
- const file = blobPath(sha, { root });
109
- if (!file) throw Object.assign(new Error(`blob not found: ${sha}`), { code: 'ENOENT' });
110
- const bytes = fs.readFileSync(file);
111
- if (digest(bytes) !== sha) throw new Error(`blob hash mismatch: ${sha}`);
112
- if (root !== null) statBlob(sha, { root });
113
- if (verifyBundle) {
114
- let manifest;
115
- try { manifest = JSON.parse(bytes.toString('utf8')); } catch { /* ordinary blobs need not be JSON */ }
116
- if (typeof manifest?.schema === 'string' && manifest.schema.startsWith('starci/blob-bundle@')
117
- && !bundleDir(sha, { root }))
118
- throw Object.assign(new Error(`blob bundle cannot be verified: ${sha}`), { code: 'EINVALBUNDLE' });
119
- }
120
- return bytes;
121
- }
122
-
123
- export function statBlob(sha, { root = null } = {}) {
124
- const file = blobPath(sha, { root });
125
- if (!file) return null;
126
- const { size } = fs.statSync(file);
127
- const metadata = JSON.parse(fs.readFileSync(metadataPath(sha, root), 'utf8'));
128
- if (metadata.size !== size) throw new Error(`blob sidecar size mismatch: ${sha}`);
129
- if(typeof metadata.mediaType!=='string'||!metadata.mediaType.trim()||!Number.isFinite(Date.parse(metadata.createdAt)))throw Error(`blob sidecar metadata invalid: ${sha}`);
130
- return { sha, size, mediaType: metadata.mediaType, createdAt: metadata.createdAt };
131
- }
132
-
133
- function canonical(value) {
134
- if (value && typeof value === 'object') {
135
- if (typeof value.toJSON === 'function') return canonical(value.toJSON());
136
- if (Array.isArray(value)) return value.map(item => item === undefined ? null : canonical(item));
137
- return Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])]));
138
- }
139
- return value;
140
- }
141
-
142
- /* ------------------------------------------------------------ citations, views and bundles */
143
-
144
- const BUNDLE_SCHEMA = 'starci/blob-bundle@1';
145
- // Read-only views (files with an extension, materialized bundles) live beside the store, e.g. ~/.starci/artifacts-views.
146
- const viewRoot = (root = null) => `${root === null ? artifactRoot() : path.resolve(root)}-views`;
147
- const slash = (p) => String(p).replace(/\\/g, '/');
148
-
149
- /** The sha256 a citation names: {sha256}, 'blob:<sha>' or a bare sha; an {artifact} alone needs `db`. */
150
- function shaOfRef(ref, { db = null } = {}) {
151
- if (typeof ref === 'string') { const s = ref.trim().replace(/^blob:/, ''); return SHA.test(s) ? s : null; }
152
- if (!ref || typeof ref !== 'object') return null;
153
- if (typeof ref.sha256 === 'string' && SHA.test(ref.sha256)) return ref.sha256;
154
- if (ref.artifact != null && db) return db.prepare('SELECT sha256 FROM job_artifacts WHERE artifact_id=?').get(Number(ref.artifact))?.sha256 ?? null;
155
- return null;
156
- }
157
-
158
- /** {sha256, file} of a citation whose bytes are in the local store, else null. */
159
- export function resolveBlob(ref, { db = null, root = null } = {}) {
160
- const sha = shaOfRef(ref, { db });
161
- const file = sha ? blobPath(sha, { root }) : null;
162
- return file ? { sha256: sha, file } : null;
163
- }
164
-
165
- /** A read-only copy of the blob with `ext` (e.g. '.png'), cached beside the store; null when missing. */
166
- export function blobAsFile(ref, { ext = '', db = null, root = null } = {}) {
167
- const hit = resolveBlob(ref, { db, root });
168
- if (!hit) return null;
169
- if (typeof ext !== 'string' || (ext !== '' && !/^\.[a-z0-9]{1,16}$/i.test(ext))) throw new TypeError('blob view extension must be a simple suffix');
170
- const bytes = getBlob(hit.sha256, { root });
171
- const file = path.join(viewRoot(root), 'files', `${hit.sha256}${ext}`);
172
- regularParents(file, { strict: root !== null });
173
- publishOnce(file, bytes);
174
- if (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink() || digest(fs.readFileSync(file)) !== hit.sha256)
175
- throw new Error(`blob view hash mismatch: ${hit.sha256}`);
176
- if (root !== null && (fs.realpathSync.native(file) !== path.join(fs.realpathSync.native(path.dirname(file)), path.basename(file))
177
- || !fs.readdirSync(path.dirname(file)).includes(path.basename(file))))
178
- throw new Error(`blob view path is linked or has a different spelling: ${file}`);
179
- return file;
180
- }
181
-
182
- function regularParents(file, { strict = false } = {}) {
183
- let cursor = path.dirname(file);
184
- while (true) {
185
- const stat = strict ? fs.lstatSync(cursor, { throwIfNoEntry: false }) : (fs.existsSync(cursor) ? fs.lstatSync(cursor) : null);
186
- if (stat) {
187
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`blob view parent is not a regular directory: ${cursor}`);
188
- const parent = path.dirname(cursor);
189
- if (strict && parent !== cursor
190
- && (fs.realpathSync.native(cursor) !== path.join(fs.realpathSync.native(parent), path.basename(cursor))
191
- || !fs.readdirSync(parent).includes(path.basename(cursor))))
192
- throw new Error(`blob read parent is linked or has a different spelling: ${cursor}`);
193
- }
194
- const parent = path.dirname(cursor);
195
- if (parent === cursor) return;
196
- cursor = parent;
197
- }
198
- }
199
-
200
- function bundleEntries(manifest) {
201
- if (!manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) throw new TypeError('blob bundle files must be a mapping');
202
- const entries = Object.entries(manifest.files);
203
- const names = new Set();
204
- for (const [rel, sha] of entries) {
205
- const parts = rel.split('/');
206
- if (!rel || rel.includes('\\') || path.posix.isAbsolute(rel) || path.win32.isAbsolute(rel)
207
- || parts.some(part => !part || part === '.' || part === '..' || /[<>:"|?*\x00-\x1f]/.test(part) || /[. ]$/.test(part)
208
- || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)) || parts[0] === '.complete')
209
- throw new TypeError(`unsafe blob bundle path: ${rel}`);
210
- assertSha(sha);
211
- const key = rel.toLowerCase();
212
- if (names.has(key)) throw new TypeError(`blob bundle path collision: ${rel}`);
213
- names.add(key);
214
- }
215
- for (const name of names) {
216
- const parts = name.split('/');
217
- for (let index = 1; index < parts.length; index += 1) if (names.has(parts.slice(0, index).join('/')))
218
- throw new TypeError(`blob bundle file is also a directory: ${name}`);
219
- }
220
- return entries;
221
- }
222
-
223
- function verifiedBundle(dir, entries, { strict = false } = {}) {
224
- regularParents(path.join(dir, '.complete'), { strict });
225
- if (!fs.existsSync(dir)) return false;
226
- const expected = new Map(entries);
227
- const seen = new Set();
228
- const walk = current => {
229
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
230
- const file = path.join(current, entry.name);
231
- const rel = slash(path.relative(dir, file));
232
- if (entry.isSymbolicLink()) throw new Error(`blob bundle view contains a link: ${rel}`);
233
- if (strict && fs.realpathSync.native(file) !== path.join(fs.realpathSync.native(current), entry.name))
234
- throw new Error(`blob bundle view contains a redirected path: ${rel}`);
235
- if (entry.isDirectory()) { walk(file); continue; }
236
- if (!entry.isFile()) throw new Error(`blob bundle view contains a special file: ${rel}`);
237
- if (rel === '.complete') continue;
238
- if (!expected.has(rel) || digest(fs.readFileSync(file)) !== expected.get(rel)) throw new Error(`blob bundle view hash mismatch: ${rel}`);
239
- seen.add(rel);
240
- }
241
- };
242
- walk(dir);
243
- return seen.size === entries.length && fs.existsSync(path.join(dir, '.complete'));
244
- }
245
-
246
- const filesUnder = (dir, out = []) => {
247
- for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
248
- const full = path.join(dir, e.name);
249
- if (e.isDirectory()) filesUnder(full, out); else if (e.isFile()) out.push(full);
250
- }
251
- return out;
252
- };
253
-
254
- /** Put every file under `dir` and its manifest in the blob store; returns the manifest sha. */
255
- export function putBundle(dir, { mediaTypeOf = () => 'application/octet-stream' } = {}) {
256
- const files = {};
257
- for (const abs of filesUnder(dir)) files[slash(path.relative(dir, abs))] = putBlob(abs, { mediaType: mediaTypeOf(abs) }).sha;
258
- return putBlob(Buffer.from(JSON.stringify({ schema: BUNDLE_SCHEMA, files }, null, 2)), { mediaType: 'application/json' }).sha;
259
- }
260
-
261
- /** The manifest of a bundle: {schema, files: {rel: sha}} or null. */
262
- function bundleManifest(ref, { db = null, root = null } = {}) {
263
- const hit = resolveBlob(ref, { db, root });
264
- if (!hit) return null;
265
- const bytes = getBlob(hit.sha256, { root });
266
- let doc;
267
- try { doc = JSON.parse(bytes.toString('utf8')); } catch { return null; }
268
- return doc?.schema === BUNDLE_SCHEMA ? doc : null;
269
- }
270
-
271
- /** The bundle as a directory beside the store (cached by sha), or null when it or one file is missing. */
272
- export function bundleDir(ref, { db = null, root = null } = {}) {
273
- const sha = shaOfRef(ref, { db });
274
- const manifest = sha ? bundleManifest(sha, { root }) : null;
275
- if (!manifest) return null;
276
- const entries = bundleEntries(manifest);
277
- const members = [];
278
- for (const [rel, fileSha] of entries) {
279
- try { members.push([rel, getBlob(fileSha, { root })]); }
280
- catch (error) { if (error?.code === 'ENOENT') return null; throw error; }
281
- }
282
- const dir = path.join(viewRoot(root), 'bundles', sha);
283
- if (verifiedBundle(dir, entries, { strict: root !== null })) return dir;
284
- if (fs.existsSync(dir)) throw new Error(`blob bundle view is incomplete: ${sha}`);
285
- regularParents(dir, { strict: root !== null });
286
- fs.mkdirSync(path.dirname(dir), { recursive: true });
287
- const staging = fs.mkdtempSync(path.join(path.dirname(dir), `.${sha}-`));
288
- try {
289
- for (const [rel, bytes] of members) {
290
- const to = path.resolve(staging, ...rel.split('/'));
291
- const relative = path.relative(staging, to);
292
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`unsafe blob bundle target: ${rel}`);
293
- fs.mkdirSync(path.dirname(to), { recursive: true });
294
- fs.writeFileSync(to, bytes, { flag: 'wx' });
295
- }
296
- fs.writeFileSync(path.join(staging, '.complete'), sha, { flag: 'wx' });
297
- try { fs.renameSync(staging, dir); }
298
- catch (error) { if (!fs.existsSync(dir) || !verifiedBundle(dir, entries, { strict: root !== null })) throw error; }
299
- } finally {
300
- if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
301
- }
302
- if (!verifiedBundle(dir, entries, { strict: root !== null })) throw new Error(`blob bundle publication is incomplete: ${sha}`);
303
- return dir;
304
- }
305
-
306
- /**
307
- * The readable file of one record asset: a product asset kept in the tree ({path}, a ui direction the owner accepted,
308
- * decision Q2) resolves against the record's directory; agent output ({artifact?, sha256, name}) resolves in the blob
309
- * store as a copy carrying the extension of its artifact name. Null when neither is readable.
310
- */
311
- export function assetFileOf(recordDir, asset, { db = null, root = null } = {}) {
312
- if (!asset || typeof asset !== 'object') return null;
313
- if (typeof asset.path === 'string' && asset.path) { const file = path.resolve(recordDir, asset.path); return fs.existsSync(file) ? file : null; }
314
- return blobAsFile(asset, { ext: path.extname(String(asset.name ?? '')).toLowerCase(), db, root });
315
- }
@@ -1,83 +0,0 @@
1
- // Ledger path identity and ownership resolution; connection policy and caches remain in ledger.mjs.
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import {sha256} from '../digest.mjs';
6
- import {isUnderTempDir,localProjectsRoot,readMachine} from './machine.mjs';
7
- import {isSpecRun} from '../../scripts/lib/env.mjs';
8
- import {pathKey} from '../../scripts/lib/path-key.mjs';
9
- /** Overrides the projects root (the directory holding <ledger_id>/runtime.sqlite) for this process tree; narrower than machine-db.mjs LOCAL_ROOT_ENV, which this still honors through starciLocalRoot when unset. */
10
- export const PROJECTS_ROOT_ENV='STARCI_PROJECTS_ROOT';
11
- const normDir=file=>pathKey(file);
12
- /** %LOCALAPPDATA%/StarCi/projects (starciLocalRoot, itself overridable by STARCI_LOCAL_ROOT; a node --test process tree gets one under the OS temp directory). */
13
- export const projectsRootFor=(env=process.env)=>{
14
- if(env[PROJECTS_ROOT_ENV])return path.resolve(env[PROJECTS_ROOT_ENV]);
15
- const root=localProjectsRoot(env);
16
- if(isSpecRun(env)&&!isUnderTempDir(root,{env}))return path.join(os.tmpdir(),'starci-test-projects');
17
- return root;
18
- };
19
- /** Repository identity: resolved/realpath, forward slashes, case-folded only on Windows. */
20
- export const repoRootKey=repoRoot=>{let root=path.resolve(repoRoot);try{root=fs.realpathSync.native(root);}catch{}return normDir(root);};
21
- /**
22
- * The ledger id of a repository root until the machine registry (a3-2 machine.ledgers) resolves it: a name-based
23
- * UUID of the normalized root, so the same root always names the same ledger and no side registry is needed.
24
- */
25
- const ledgerIdFromKey=key=>{
26
- const h=sha256(`starci-ledger:${key}`);
27
- return `${h.slice(0,8)}-${h.slice(8,12)}-5${h.slice(13,16)}-${(8|(parseInt(h[16],16)&3)).toString(16)}${h.slice(17,20)}-${h.slice(20,32)}`;
28
- };
29
- export const ledgerIdForRepo=repoRoot=>ledgerIdFromKey(repoRootKey(repoRoot));
30
- /** The registered runtime.sqlite of `repoRoot` in machine.ledgers (a3-2 resolveLedger), or null. */
31
- function registeredLedgerFile(root,env,openReader){
32
- let row;try{row=readMachine(m=>m.resolveLedger({repoRoot:root}),null,{env});}catch{return null;}
33
- const file=row&&row.state!=='retired'&&row.file?path.resolve(row.file):null;
34
- if(file&&fs.existsSync(file))assertLedgerRoot(file,root,openReader);
35
- return file;
36
- }
37
- function assertLedgerRoot(file,root,openReader){
38
- const db=openReader(file);let own;try{own=db.prepare("SELECT value FROM meta WHERE key='repo_root'").get()?.value;}finally{db.close();}
39
- if(!own||repoRootKey(own)!==repoRootKey(root))throw Object.assign(Error(`ledger-root-mismatch: ${file} belongs to another or unverified repository; preserve the database and resolve the owner binding before migration`),{code:'STARCI_LEDGER_ROOT_MISMATCH'});
40
- }
41
- function unregisteredLedgerFile(root,env,openReader){
42
- const base=projectsRootFor(env),current=path.join(base,ledgerIdForRepo(root),'runtime.sqlite');
43
- if(fs.existsSync(current)){assertLedgerRoot(current,root,openReader);return current;}
44
- // Preserve a pre-fix POSIX binding only when its own exact repo_root proves
45
- // this repository. Never silently move, rename or merge an old folded store.
46
- const legacy=path.join(base,ledgerIdFromKey(repoRootKey(root).toLowerCase()),'runtime.sqlite');
47
- if(legacy!==current&&fs.existsSync(legacy)){assertLedgerRoot(legacy,root,openReader);return legacy;}
48
- return current;
49
- }
50
- /** Resolve the registry or exact owned store; the ledger owner supplies its verified read-only opener. */
51
- export function resolveLedgerFile(root,{env,openReader}){
52
- return registeredLedgerFile(root,env,openReader)??unregisteredLedgerFile(root,env,openReader);
53
- }
54
-
55
- const SYNTHETIC_LEDGER_ID=/^00000000-0000-4000-8000-[0-9a-f]{12}$/;
56
- const fixtureRefused=message=>{throw new TypeError(`ledger-fixture-refused: ${message}`);};
57
- /** Validate a fresh, initialization-only sample identity. Samples carry portable metadata,
58
- * never a repository/machine binding; the ordinary project resolver is not bypassed. */
59
- export function ledgerFixtureInit(fixture,{file,repoRoot,product,machine,mapped,checkpointer,now}){
60
- if(fixture===null){
61
- if(typeof file==='string'&&SYNTHETIC_LEDGER_ID.test(path.basename(path.dirname(path.resolve(file)))))fixtureRefused('synthetic identity is reserved for a sample');
62
- return null;
63
- }
64
- if(!fixture||typeof fixture!=='object'||Array.isArray(fixture)
65
- ||![Object.prototype,null].includes(Object.getPrototypeOf(fixture))
66
- ||Object.keys(fixture).sort().join(',')!=='blobRoot,createdAt,ledgerId')fixtureRefused('needs exactly ledgerId, createdAt and blobRoot');
67
- if(typeof fixture.ledgerId!=='string'||!SYNTHETIC_LEDGER_ID.test(fixture.ledgerId))fixtureRefused('ledgerId must be a synthetic UUID');
68
- if(!Number.isSafeInteger(fixture.createdAt)||fixture.createdAt<=0||!Number.isFinite(new Date(fixture.createdAt).getTime()))fixtureRefused('createdAt must be a positive safe epoch millisecond');
69
- const root=fixture.blobRoot;
70
- if(typeof root!=='string'||!root||root.includes('\\')||root.trim()!==root||/[<>:"|?*\x00-\x1f]/.test(root)
71
- ||path.posix.isAbsolute(root)||root.split('/').some(p=>!p||p==='.'||p==='..'))fixtureRefused('blobRoot must be a portable relative path');
72
- if(repoRoot!==null||product!==null||machine!==null||mapped)fixtureRefused('a sample cannot bind a repository, product, machine or cached project path');
73
- if(!checkpointer||now!==Date.now)fixtureRefused('initialization owns its frozen clock and requires a checkpointer');
74
- if(typeof file!=='string'||!path.isAbsolute(file)||path.basename(file)!=='runtime.sqlite')fixtureRefused('file must be an explicit absolute runtime.sqlite');
75
- for(const suffix of ['','-wal','-shm'])if(fs.lstatSync(file+suffix,{throwIfNoEntry:false}))fixtureRefused('file and WAL/SHM must be absent');
76
- const parent=path.dirname(file),stat=fs.lstatSync(parent,{throwIfNoEntry:false});
77
- if(!stat?.isDirectory()||stat.isSymbolicLink()||fs.realpathSync.native(parent)!==path.resolve(parent))fixtureRefused('sample parent must be an existing physical directory');
78
- return Object.freeze({...fixture,marker:'starci/basic-runtime-fixture@1'});
79
- }
80
- /** Refuse sample bytes as an operational ledger before opening a writer or beginning a transaction. */
81
- export function assertOperationalLedger(meta){
82
- if(meta.fixture||SYNTHETIC_LEDGER_ID.test(meta.ledger_id??''))throw new Error('ledger-fixture-read-only: preserve the sample; initialize a new live project store');
83
- }