rcf-lite 0.24.0 → 0.26.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 (313) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/blueprints/application-account-settings/docs/topics.md +4 -1
  3. package/blueprints/application-admin-console/docs/topics.md +4 -1
  4. package/blueprints/application-api-rest/docs/topics.md +4 -1
  5. package/blueprints/application-charts/docs/topics.md +4 -1
  6. package/blueprints/application-dashboard/docs/topics.md +4 -1
  7. package/blueprints/application-datatable/docs/topics.md +4 -1
  8. package/blueprints/application-empty-error-states/docs/topics.md +4 -1
  9. package/blueprints/application-error-handling/docs/topics.md +4 -1
  10. package/blueprints/application-file-upload/docs/topics.md +4 -1
  11. package/blueprints/application-forms-wizard/docs/topics.md +4 -1
  12. package/blueprints/application-notifications-in-app/docs/topics.md +4 -1
  13. package/blueprints/application-onboarding-tour/docs/topics.md +4 -1
  14. package/blueprints/application-spa/docs/topics.md +4 -1
  15. package/blueprints/delivery-ci-workflows/docs/topics.md +4 -1
  16. package/blueprints/deploy-cloudflare-workers/docs/topics.md +4 -1
  17. package/blueprints/deploy-hetzner-server/CHANGELOG.md +55 -0
  18. package/blueprints/deploy-hetzner-server/README.md +202 -0
  19. package/blueprints/deploy-hetzner-server/blueprint.json +233 -0
  20. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +14 -0
  21. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +14 -0
  22. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +20 -0
  23. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +14 -0
  24. package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +71 -0
  25. package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +164 -0
  26. package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +182 -0
  27. package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +112 -0
  28. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +64 -0
  29. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +60 -0
  30. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +58 -0
  31. package/blueprints/deploy-hetzner-server/contributions/probes/run-cloud-init-render-lint.mjs +9 -0
  32. package/blueprints/deploy-hetzner-server/contributions/probes/run-hcloud-dry-run-mock.mjs +9 -0
  33. package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs +9 -0
  34. package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-cloud-init-hardened.mjs +9 -0
  35. package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-snapshot-on-demand.mjs +9 -0
  36. package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-throwaway-server-provision.mjs +9 -0
  37. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-001.json +18 -0
  38. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-002.json +18 -0
  39. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-003.json +18 -0
  40. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-004.json +18 -0
  41. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-005.json +18 -0
  42. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-006.json +18 -0
  43. package/blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json +108 -0
  44. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3801-deploy-hetzner-server-provisioner.json +27 -0
  45. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +26 -0
  46. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +26 -0
  47. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +25 -0
  48. package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +113 -0
  49. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37101.json +24 -0
  50. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37102.json +24 -0
  51. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37103.json +25 -0
  52. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37104.json +24 -0
  53. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37105.json +24 -0
  54. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37106.json +25 -0
  55. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37107.json +24 -0
  56. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37108.json +24 -0
  57. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37109.json +24 -0
  58. package/blueprints/deploy-hetzner-server/docs/topics.md +48 -0
  59. package/blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md +180 -0
  60. package/blueprints/edge-cloudflare-access/docs/topics.md +4 -1
  61. package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +4 -1
  62. package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +23 -0
  63. package/blueprints/edge-cloudflare-tunnel/README.md +257 -0
  64. package/blueprints/edge-cloudflare-tunnel/blueprint.json +168 -0
  65. package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +23 -0
  66. package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +19 -0
  67. package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +19 -0
  68. package/blueprints/edge-cloudflare-tunnel/contributions/probes/aud-presence-check.mjs +114 -0
  69. package/blueprints/edge-cloudflare-tunnel/contributions/probes/cloudflared-config-lint.mjs +80 -0
  70. package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +237 -0
  71. package/blueprints/edge-cloudflare-tunnel/contributions/probes/probe-utils.mjs +231 -0
  72. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +143 -0
  73. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +166 -0
  74. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-aud-presence-check.mjs +4 -0
  75. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-cloudflared-config-lint.mjs +4 -0
  76. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-manifest-schema-validate.mjs +4 -0
  77. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-connector-healthy.mjs +4 -0
  78. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-tunnel-hostname-routes.mjs +4 -0
  79. package/blueprints/edge-cloudflare-tunnel/contributions/probes/tunnel-manifest.schema.json +55 -0
  80. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-001.json +18 -0
  81. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-002.json +18 -0
  82. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-003.json +18 -0
  83. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-004.json +18 -0
  84. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-005.json +18 -0
  85. package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +26 -0
  86. package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +26 -0
  87. package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +25 -0
  88. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39101.json +24 -0
  89. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39102.json +24 -0
  90. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39103.json +24 -0
  91. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39104.json +24 -0
  92. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39105.json +24 -0
  93. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39106.json +24 -0
  94. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39107.json +24 -0
  95. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39108.json +24 -0
  96. package/blueprints/edge-cloudflare-tunnel/docs/topics.md +72 -0
  97. package/blueprints/edge-cloudflare-tunnel/guide/edge-cloudflare-tunnel.md +204 -0
  98. package/blueprints/edge-cloudflare-turnstile/docs/topics.md +4 -1
  99. package/blueprints/email-smtp-resend/docs/topics.md +4 -1
  100. package/blueprints/jobs-background/docs/topics.md +4 -1
  101. package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +19 -0
  102. package/blueprints/messaging-queue-cloudflare/README.md +2 -2
  103. package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
  104. package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +192 -37
  105. package/blueprints/messaging-queue-cloudflare/docs/topics.md +4 -1
  106. package/blueprints/object-storage-s3/CHANGELOG.md +11 -0
  107. package/blueprints/object-storage-s3/README.md +14 -1
  108. package/blueprints/object-storage-s3/blueprint.json +12 -1
  109. package/blueprints/object-storage-s3/contributions/adrs/adr-2905-object-storage-s3-hetzner-object-storage-provider.json +30 -0
  110. package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +136 -0
  111. package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +11 -0
  112. package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-101.json +18 -0
  113. package/blueprints/object-storage-s3/contributions/tacs/tac-2904-object-storage-s3-hetzner-endpoint-helper.json +34 -0
  114. package/blueprints/object-storage-s3/contributions/user-stories/object-storage-s3-us-28110.json +25 -0
  115. package/blueprints/object-storage-s3/docs/topics.md +4 -1
  116. package/blueprints/object-storage-s3/guide/object-storage-s3.md +36 -0
  117. package/blueprints/observability-essentials/docs/topics.md +4 -1
  118. package/blueprints/observability-logging/docs/topics.md +4 -1
  119. package/blueprints/observability-probe-endpoints/docs/topics.md +4 -1
  120. package/blueprints/persistence-data-d1/docs/topics.md +4 -1
  121. package/blueprints/persistence-data-postgres/docs/topics.md +4 -1
  122. package/blueprints/persistence-data-sqlite/docs/topics.md +4 -1
  123. package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +4 -1
  124. package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +15 -0
  125. package/blueprints/platform-cloudflare-durable-objects/blueprint.json +1 -1
  126. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/namespace-facade-ready.mjs +3 -3
  127. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/probe-utils.mjs +28 -14
  128. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/real-account-storage-smoke.mjs +132 -26
  129. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/sole-reader-scan.mjs +40 -39
  130. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/storage-round-trip.mjs +26 -16
  131. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/websocket-hub-broadcast.mjs +31 -34
  132. package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +4 -1
  133. package/blueprints/platform-cloudflare-kv/CHANGELOG.md +40 -1
  134. package/blueprints/platform-cloudflare-kv/README.md +41 -13
  135. package/blueprints/platform-cloudflare-kv/blueprint.json +1 -1
  136. package/blueprints/platform-cloudflare-kv/contributions/probes/cache-aside-hit-then-miss.mjs +26 -26
  137. package/blueprints/platform-cloudflare-kv/contributions/probes/event-secrecy.mjs +49 -31
  138. package/blueprints/platform-cloudflare-kv/contributions/probes/facade-round-trip.mjs +58 -16
  139. package/blueprints/platform-cloudflare-kv/contributions/probes/list-with-prefix.mjs +6 -5
  140. package/blueprints/platform-cloudflare-kv/contributions/probes/real-account-eventual-consistency-smoke.mjs +131 -67
  141. package/blueprints/platform-cloudflare-kv/docs/topics.md +4 -1
  142. package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
  143. package/blueprints/platform-docker-compose-host/README.md +76 -0
  144. package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
  145. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
  146. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
  147. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
  148. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
  149. package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
  150. package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
  151. package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
  152. package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
  153. package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
  154. package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
  155. package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
  156. package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
  157. package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
  158. package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
  159. package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
  160. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
  161. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
  162. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
  163. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
  164. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
  165. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
  166. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
  167. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
  168. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
  169. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
  170. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
  171. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
  172. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
  173. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
  174. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
  175. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
  176. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
  177. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
  178. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
  179. package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
  180. package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
  181. package/blueprints/security-auth-clerk/docs/topics.md +4 -1
  182. package/blueprints/security-auth-keycloak/docs/topics.md +4 -1
  183. package/blueprints/security-auth-magic-link/docs/topics.md +4 -1
  184. package/blueprints/security-auth-oauth2/docs/topics.md +4 -1
  185. package/blueprints/security-secrets-management/docs/topics.md +4 -1
  186. package/fixtures/canary-manifest.json +9 -9
  187. package/guidance/harness-template.md +67 -0
  188. package/guidance/managed/agent-instructions-block.hash +1 -1
  189. package/guidance/managed/agent-instructions-block.md +67 -0
  190. package/package.json +2 -2
  191. package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
  192. package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
  193. package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
  194. package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
  195. package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
  196. package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
  197. package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
  198. package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
  199. package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
  200. package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
  201. package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
  202. package/rcf/code-nodes/cn-410.json +20 -0
  203. package/rcf/code-nodes/cn-440.json +20 -0
  204. package/rcf/code-nodes/cn-470.json +19 -0
  205. package/rcf/code-nodes/cn-500.json +12 -0
  206. package/rcf/code-nodes/cn-510.json +12 -0
  207. package/rcf/code-nodes/cn-520.json +14 -0
  208. package/rcf/code-nodes/cn-521.json +12 -0
  209. package/rcf/code-nodes/cn-522.json +13 -0
  210. package/rcf/code-nodes/cn-523.json +12 -0
  211. package/rcf/code-nodes/cn-524.json +14 -0
  212. package/rcf/code-nodes/cn-525.json +12 -0
  213. package/rcf/code-nodes/cn-526.json +14 -0
  214. package/rcf/fbs/fbs-130.json +23 -0
  215. package/rcf/fbs/fbs-140.json +23 -0
  216. package/rcf/fbs/fbs-150.json +22 -0
  217. package/rcf/fbs/fbs-160.json +15 -0
  218. package/rcf/fbs/fbs-165.json +15 -0
  219. package/rcf/fbs/fbs-170.json +16 -0
  220. package/rcf/fbs/fbs-171.json +16 -0
  221. package/rcf/fbs/fbs-172.json +15 -0
  222. package/rcf/fbs/fbs-173.json +17 -0
  223. package/rcf/fbs/fbs-174.json +17 -0
  224. package/rcf/requirements/req-110.json +58 -0
  225. package/rcf/requirements/req-111.json +32 -0
  226. package/rcf/requirements/req-112.json +42 -0
  227. package/rcf/requirements/req-113.json +33 -0
  228. package/rcf/requirements/req-114.json +20 -0
  229. package/rcf/requirements/req-115.json +38 -0
  230. package/rcf/requirements/req-120.json +20 -0
  231. package/rcf/requirements/req-121.json +20 -0
  232. package/rcf/requirements/req-122.json +20 -0
  233. package/rcf/requirements/req-123.json +42 -0
  234. package/rcf/requirements/req-124.json +27 -0
  235. package/rcf/requirements/req-125.json +20 -0
  236. package/rcf/requirements/req-130.json +32 -0
  237. package/rcf/requirements/req-131.json +54 -0
  238. package/rcf/requirements/req-132.json +27 -0
  239. package/rcf/requirements/req-133.json +20 -0
  240. package/rcf/requirements/req-134.json +42 -0
  241. package/rcf/requirements/req-140.json +149 -0
  242. package/rcf/requirements/req-145.json +53 -0
  243. package/rcf/requirements/req-150.json +20 -0
  244. package/rcf/requirements/req-151.json +85 -0
  245. package/rcf/requirements/req-152.json +20 -0
  246. package/rcf/requirements/req-153.json +59 -0
  247. package/rcf/requirements/req-154.json +44 -0
  248. package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
  249. package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
  250. package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
  251. package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
  252. package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
  253. package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
  254. package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
  255. package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
  256. package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
  257. package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
  258. package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
  259. package/rcf/test-suites/ts-071.json +2 -2
  260. package/rcf/test-suites/ts-086.json +2 -2
  261. package/rcf/test-suites/ts-140.json +86 -0
  262. package/rcf/test-suites/ts-150.json +86 -0
  263. package/rcf/test-suites/ts-160.json +78 -0
  264. package/rcf/test-suites/ts-170.json +22 -0
  265. package/rcf/test-suites/ts-175.json +22 -0
  266. package/rcf/test-suites/ts-180.json +30 -0
  267. package/rcf/test-suites/ts-181.json +30 -0
  268. package/rcf/test-suites/ts-182.json +22 -0
  269. package/rcf/test-suites/ts-183.json +38 -0
  270. package/rcf/test-suites/ts-184.json +38 -0
  271. package/rcf/user-stories/us-11001.json +23 -0
  272. package/rcf/user-stories/us-11101.json +23 -0
  273. package/rcf/user-stories/us-11102.json +24 -0
  274. package/rcf/user-stories/us-11201.json +23 -0
  275. package/rcf/user-stories/us-11202.json +23 -0
  276. package/rcf/user-stories/us-11301.json +24 -0
  277. package/rcf/user-stories/us-11401.json +23 -0
  278. package/rcf/user-stories/us-11402.json +23 -0
  279. package/rcf/user-stories/us-11501.json +23 -0
  280. package/rcf/user-stories/us-12001.json +23 -0
  281. package/rcf/user-stories/us-12101.json +23 -0
  282. package/rcf/user-stories/us-12102.json +23 -0
  283. package/rcf/user-stories/us-12201.json +23 -0
  284. package/rcf/user-stories/us-12202.json +24 -0
  285. package/rcf/user-stories/us-12301.json +23 -0
  286. package/rcf/user-stories/us-12401.json +23 -0
  287. package/rcf/user-stories/us-12402.json +23 -0
  288. package/rcf/user-stories/us-12501.json +23 -0
  289. package/rcf/user-stories/us-13001.json +23 -0
  290. package/rcf/user-stories/us-13002.json +23 -0
  291. package/rcf/user-stories/us-13101.json +23 -0
  292. package/rcf/user-stories/us-13102.json +23 -0
  293. package/rcf/user-stories/us-13201.json +23 -0
  294. package/rcf/user-stories/us-13301.json +23 -0
  295. package/rcf/user-stories/us-13302.json +23 -0
  296. package/rcf/user-stories/us-13401.json +23 -0
  297. package/rcf/user-stories/us-14001.json +21 -0
  298. package/rcf/user-stories/us-14501.json +24 -0
  299. package/rcf/user-stories/us-15001.json +26 -0
  300. package/rcf/user-stories/us-15101.json +26 -0
  301. package/rcf/user-stories/us-15201.json +21 -0
  302. package/rcf/user-stories/us-15301.json +31 -0
  303. package/rcf/user-stories/us-15401.json +31 -0
  304. package/releases/releases.yaml +21 -1
  305. package/src/blueprint/apply.js +64 -0
  306. package/src/blueprint/consistency-lint.js +432 -0
  307. package/src/blueprint/disposition-ledger.js +224 -0
  308. package/src/blueprint/dispositions.js +44 -0
  309. package/src/blueprint/index.js +19 -0
  310. package/src/browser-verify/manifest-writer.js +53 -2
  311. package/src/browser-verify/pack-runner.js +18 -0
  312. package/src/cli/blueprint.js +103 -0
  313. package/src/setup/identity-seed.js +18 -0
@@ -0,0 +1,432 @@
1
+ // Blueprint chain-consistency lint (spec section 5 of the integration
2
+ // and contradiction protocol, 2026-09-09).
3
+ //
4
+ // Two passes over ONE blueprint's own JSON and markdown. Pure functions;
5
+ // the CLI seam loads the blueprint from disk and hands the model in.
6
+ //
7
+ // PASS 1: single-definition ownership, MECHANICAL ONLY.
8
+ // The check is intentionally conservative. Pass 1 fires when the
9
+ // same identifier-shaped literal appears on more than one artefact
10
+ // and one of the appearances is case-INCONSISTENT with the owning
11
+ // TAC on a case-sensitive axis (HTTP header, env-var, camelCase
12
+ // JSON field name in a strict-parse context). A byte-identical
13
+ // restatement is only flagged when the owning TAC declares the
14
+ // token on its `interfaces[]` (the strict interface surface) and
15
+ // the restating AC carries no `ownerRef` back to that TAC. Every
16
+ // other kind of appearance is treated as a reference, not a
17
+ // restatement (the mechanical detector cannot reliably tell a
18
+ // duplicate literal from a legitimate mention without domain
19
+ // knowledge, and the spec explicitly names four human-read shapes
20
+ // the lint does not catch, per section 5.7).
21
+ //
22
+ // PASS 2: REQ delivery, MECHANICAL.
23
+ // A REQ whose description carries a promise phrase (must / requires
24
+ // / guarantees / ceiling / never / invariant / on boot / at boot /
25
+ // event / boundary) must carry a `deliveredBy` link into a TAC or
26
+ // ADR. When the link points at a TAC in the same blueprint, the
27
+ // TAC's responsibilities and interfaces must reference the field
28
+ // (or the REQ's subject, when no `field` sub-path is given).
29
+ //
30
+ // Every finding carries a stable `id` the operator can quote in the
31
+ // blueprint README under "Known chain-consistency-lint suppressions"
32
+ // (pass 1 only; pass 2 findings are not suppressible per spec 5.8).
33
+
34
+ import { readFile, readdir } from 'node:fs/promises';
35
+ import { join } from 'node:path';
36
+
37
+ /**
38
+ * @typedef {object} ConsistencyFinding
39
+ * @property {string} id
40
+ * @property {'pass1'|'pass2'} pass
41
+ * @property {'contractDrift'|'undeclaredRestatement'|'reqNoDelivery'|'reqDeliveryNotCarried'} kind
42
+ * @property {string} subject
43
+ * @property {string[]} refs
44
+ * @property {string} message
45
+ * @property {boolean} [suppressed]
46
+ * @property {string} [suppressionReason]
47
+ */
48
+
49
+ const HTTP_HEADER_RE = /^[A-Z][A-Za-z0-9]*(-[A-Z][A-Za-z0-9]*)+$/;
50
+ const HTTP_HEADER_LOWER_RE = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)+$/;
51
+ const ENV_VAR_RE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
52
+ const CAMEL_JSON_FIELD_RE = /^[a-z][a-zA-Z0-9]*$/;
53
+
54
+ /**
55
+ * Extract identifier-shaped tokens from a body of text: dashed
56
+ * identifiers (Cf-Turnstile-Response, run_worker_first),
57
+ * ALL_CAPS_ENV_VARS, and camelCase / PascalCase runs. Duplicates are
58
+ * preserved so callers can index by occurrence.
59
+ *
60
+ * @param {string} text
61
+ */
62
+ function extractTokens(text) {
63
+ if (typeof text !== 'string' || text.length === 0) return [];
64
+ const out = [];
65
+ const re = /[A-Za-z][A-Za-z0-9]*(?:[-_][A-Za-z0-9]+)+|[A-Z][a-z]+(?:[A-Z][a-z]*)+|[a-z]+(?:[A-Z][a-z]*){1,}/g;
66
+ for (const m of text.matchAll(re)) out.push(m[0]);
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * Classify a token for case-sensitivity purposes.
72
+ *
73
+ * @param {string} token
74
+ * @returns {'header'|'headerLower'|'envVar'|'jsonField'|'identifier'}
75
+ */
76
+ function tokenClass(token) {
77
+ if (ENV_VAR_RE.test(token)) return 'envVar';
78
+ if (HTTP_HEADER_RE.test(token)) return 'header';
79
+ if (HTTP_HEADER_LOWER_RE.test(token)) return 'headerLower';
80
+ if (CAMEL_JSON_FIELD_RE.test(token)) return 'jsonField';
81
+ return 'identifier';
82
+ }
83
+
84
+ // TAC / ADR / REQ / US / PRD / TAD / BS / FBS id prefixes: these read
85
+ // as identifier-shaped but they are meta references, not contract
86
+ // literals. Skip them in both directions of the drift check.
87
+ const ID_TOKEN_RE = /^(?:TAC|ADR|REQ|US|PRD|TAD|BS|FBS|TS|CN|AC)[-_A-Z0-9-]*$/;
88
+
89
+ // Small stop-list of ordinary English hyphenated / camel-cased words
90
+ // that appear as identifier-shape but are prose, not contracts.
91
+ const PROSE_TOKENS = new Set([
92
+ 'stable-coded', 'per-environment', 'wall-clock', 'operator-authored',
93
+ 'project-authored', 'vendor-resolved', 'required-absent',
94
+ 'deployable-to-slice', 'rotation-days', 'admin-UI',
95
+ 'read-through', 'write-through', 'follow-up',
96
+ ]);
97
+
98
+ function isNoise(token) {
99
+ if (token.length < 4) return true;
100
+ if (ID_TOKEN_RE.test(token)) return true;
101
+ if (PROSE_TOKENS.has(token)) return true;
102
+ return false;
103
+ }
104
+
105
+ /**
106
+ * Owned-token dictionary. Keyed by lower-case token; the value is the
107
+ * canonical spelling on the owning TAC's interface surface, plus the
108
+ * TAC id and field where it lives.
109
+ *
110
+ * Only `interfaces[].name` and `interfaces[].summary` (backtick-quoted
111
+ * spans only) plus `internalStructure` (backtick-quoted spans only)
112
+ * count as owning surfaces. `responsibilities[]` and `purpose` are
113
+ * prose narrative where a mention is a reference, not a declaration.
114
+ */
115
+ function collectOwnedTokens(tacs) {
116
+ /** @type {Map<string, { canonical: string, tacId: string, field: string, klass: string }>} */
117
+ const owned = new Map();
118
+ // record() takes an `onlySensitive` flag. For strict interface
119
+ // surfaces (`interfaces[].name`, `internalStructure` backtick spans,
120
+ // `responsibilities[]` backtick spans) we record every non-prose
121
+ // token including camelCase field names. For less-strict prose
122
+ // surfaces (interface summaries, purpose, responsibilities prose)
123
+ // we only record tokens classified as `header` / `headerLower` /
124
+ // `envVar`, because a prose mention of a camelCase word is usually
125
+ // a reference and only case-sensitive axes reliably indicate a
126
+ // literal declaration.
127
+ const record = (token, tacId, field, onlySensitive) => {
128
+ if (isNoise(token)) return;
129
+ const klass = tokenClass(token);
130
+ if (klass === 'identifier') return;
131
+ if (onlySensitive && klass === 'jsonField') return;
132
+ const key = token.toLowerCase();
133
+ if (!owned.has(key)) owned.set(key, { canonical: token, tacId, field, klass });
134
+ };
135
+ for (const tac of tacs) {
136
+ const tacId = tac.tacId || tac.id || '(unknown-tac)';
137
+ // Strict interface surfaces first so their spellings win the
138
+ // Map's first-write-wins semantics over any weaker prose mention.
139
+ if (Array.isArray(tac.interfaces)) {
140
+ for (let i = 0; i < tac.interfaces.length; i += 1) {
141
+ const iface = tac.interfaces[i];
142
+ if (iface && typeof iface.name === 'string') {
143
+ for (const t of extractTokens(iface.name)) record(t, tacId, `interfaces[${i}].name`, false);
144
+ }
145
+ if (iface && typeof iface.summary === 'string') {
146
+ for (const m of iface.summary.matchAll(/`+([^`]+)`+/g)) {
147
+ for (const t of extractTokens(m[1])) record(t, tacId, `interfaces[${i}].summary`, false);
148
+ }
149
+ // Plain-text summary: case-sensitive axes only. This is what
150
+ // catches `Cf-Turnstile-Response` on TAC-3602's guard summary.
151
+ for (const t of extractTokens(iface.summary)) record(t, tacId, `interfaces[${i}].summary`, true);
152
+ }
153
+ }
154
+ }
155
+ if (typeof tac.internalStructure === 'string') {
156
+ for (const m of tac.internalStructure.matchAll(/`+([^`]+)`+/g)) {
157
+ for (const t of extractTokens(m[1])) record(t, tacId, 'internalStructure', false);
158
+ }
159
+ for (const t of extractTokens(tac.internalStructure)) record(t, tacId, 'internalStructure', true);
160
+ }
161
+ if (Array.isArray(tac.responsibilities)) {
162
+ for (let i = 0; i < tac.responsibilities.length; i += 1) {
163
+ const s = tac.responsibilities[i];
164
+ if (typeof s !== 'string') continue;
165
+ for (const m of s.matchAll(/`+([^`]+)`+/g)) {
166
+ for (const t of extractTokens(m[1])) record(t, tacId, `responsibilities[${i}]`, false);
167
+ }
168
+ for (const t of extractTokens(s)) record(t, tacId, `responsibilities[${i}]`, true);
169
+ }
170
+ }
171
+ if (typeof tac.purpose === 'string') {
172
+ // Purpose prose: header / envVar only (never fires on ordinary
173
+ // English or camelCase identifiers).
174
+ for (const t of extractTokens(tac.purpose)) record(t, tacId, 'purpose', true);
175
+ }
176
+ }
177
+ return owned;
178
+ }
179
+
180
+ /**
181
+ * Collect artefact surfaces (holderId, holderKind, field, text,
182
+ * optional ownerRef on ACs). Used by pass 1's cross-surface walk.
183
+ */
184
+ function collectArtefactSurfaces(input) {
185
+ const surfaces = [];
186
+ for (const req of input.reqs) {
187
+ const rid = req.reqId || req.id || '(unknown-req)';
188
+ if (typeof req.description === 'string') surfaces.push({ holderId: rid, holderKind: 'req', field: 'description', text: req.description });
189
+ }
190
+ for (const us of input.userStories) {
191
+ const usid = us.usId || us.id || '(unknown-us)';
192
+ if (typeof us.iWant === 'string') surfaces.push({ holderId: usid, holderKind: 'us', field: 'iWant', text: us.iWant });
193
+ if (Array.isArray(us.acceptanceCriteria)) {
194
+ for (const ac of us.acceptanceCriteria) {
195
+ const acid = ac.id || `${usid}-AC?`;
196
+ const ownerRef = ac.ownerRef && typeof ac.ownerRef === 'object' ? ac.ownerRef : null;
197
+ for (const f of ['description', 'given', 'when', 'then']) {
198
+ if (typeof ac[f] === 'string') surfaces.push({ holderId: acid, holderKind: 'ac', field: f, text: ac[f], parentUsId: usid, ownerRef });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ for (const adr of input.adrs) {
204
+ const aid = adr.adrId || adr.id || '(unknown-adr)';
205
+ for (const f of ['context', 'decision', 'consequences']) {
206
+ if (typeof adr[f] === 'string') surfaces.push({ holderId: aid, holderKind: 'adr', field: f, text: adr[f] });
207
+ }
208
+ }
209
+ if (typeof input.guideMarkdown === 'string' && input.guideMarkdown.length > 0) {
210
+ surfaces.push({ holderId: 'guide', holderKind: 'guide', field: 'body', text: input.guideMarkdown });
211
+ }
212
+ return surfaces;
213
+ }
214
+
215
+ /**
216
+ * @param {{ slug: string, sourcePath: string, tacs: object[], reqs: object[], userStories: object[], adrs: object[], guideMarkdown?: string }} input
217
+ * @returns {ConsistencyFinding[]}
218
+ */
219
+ export function runPass1(input) {
220
+ const owned = collectOwnedTokens(input.tacs);
221
+ if (owned.size === 0) return [];
222
+ const surfaces = collectArtefactSurfaces(input);
223
+ /** @type {ConsistencyFinding[]} */
224
+ const findings = [];
225
+ const seen = new Set();
226
+
227
+ for (const surface of surfaces) {
228
+ for (const raw of extractTokens(surface.text)) {
229
+ if (isNoise(raw)) continue;
230
+ const key = raw.toLowerCase();
231
+ const owner = owned.get(key);
232
+ if (!owner) continue;
233
+ const isSensitive = owner.klass === 'header' || owner.klass === 'envVar' || owner.klass === 'headerLower';
234
+ // Contract drift: the same lower-cased token, spelled
235
+ // differently, on a case-sensitive axis (HTTP header or
236
+ // env-var). This is the primary mechanical catch and includes
237
+ // the `Cf-Turnstile-Response` / `cf-turnstile-response`
238
+ // specimen shape.
239
+ if (isSensitive && owner.canonical !== raw) {
240
+ // Sentence-start capitalisation is not contract drift.
241
+ // Skip when the two tokens differ ONLY in the case of the
242
+ // very first character (rest of the token is byte-identical).
243
+ if (owner.canonical.length === raw.length
244
+ && owner.canonical.slice(1) === raw.slice(1)
245
+ && owner.canonical[0].toLowerCase() === raw[0].toLowerCase()) continue;
246
+ const id = `pass1-drift-${owner.tacId}-${owner.canonical}`;
247
+ const dedupe = `${id}::${surface.holderId}::${raw}`;
248
+ if (!seen.has(dedupe)) {
249
+ seen.add(dedupe);
250
+ findings.push({
251
+ id, pass: 'pass1', kind: 'contractDrift',
252
+ subject: owner.canonical,
253
+ refs: [owner.tacId, surface.holderId],
254
+ message: `${surface.holderId}.${surface.field} spells '${owner.canonical}' as '${raw}'; owner ${owner.tacId}.${owner.field} declares '${owner.canonical}'.`,
255
+ });
256
+ }
257
+ continue;
258
+ }
259
+ // Undeclared restatement: only fires for AC surfaces (the
260
+ // schema field `ownerRef` lives on AC records), only when the
261
+ // owning surface is an `interfaces[].name` (the strict
262
+ // interface surface), and only when the AC does not name an
263
+ // `ownerRef.tacId` matching the owning TAC. Every other cross
264
+ // surface mention is read as a reference, not a restatement.
265
+ if (surface.holderKind !== 'ac') continue;
266
+ if (!owner.field.startsWith('interfaces[') || !owner.field.endsWith('].name')) continue;
267
+ const ownerRef = surface.ownerRef;
268
+ const declared = ownerRef && typeof ownerRef.tacId === 'string' && ownerRef.tacId === owner.tacId;
269
+ if (declared) continue;
270
+ const id = `pass1-restated-${owner.tacId}-${owner.canonical}`;
271
+ const dedupe = `${id}::${surface.holderId}`;
272
+ if (seen.has(dedupe)) continue;
273
+ seen.add(dedupe);
274
+ findings.push({
275
+ id, pass: 'pass1', kind: 'undeclaredRestatement',
276
+ subject: owner.canonical,
277
+ refs: [owner.tacId, surface.holderId],
278
+ message: `${surface.holderId}.${surface.field} restates '${owner.canonical}' owned by ${owner.tacId}.${owner.field}; declare ownerRef or turn the mention into a reference.`,
279
+ });
280
+ }
281
+ }
282
+ return findings;
283
+ }
284
+
285
+ // REQ description phrases that read as an externally observable
286
+ // promise the lint asks a TAC or ADR to deliver.
287
+ const PROMISE_TERMS = [
288
+ 'must', 'requires', 'required', 'guarantee', 'guarantees', 'guaranteed',
289
+ 'ceiling', 'never', 'invariant', 'on boot', 'at boot', 'boot event',
290
+ 'always', 'ensures', 'refuse', 'refuses', 'forbid', 'forbidden',
291
+ ];
292
+
293
+ export function runPass2(input) {
294
+ const findings = [];
295
+ const tacsById = new Map();
296
+ for (const t of input.tacs) tacsById.set(t.tacId || t.id, t);
297
+ for (const req of input.reqs) {
298
+ const rid = req.reqId || req.id || '(unknown-req)';
299
+ const desc = typeof req.description === 'string' ? req.description : '';
300
+ const promises = PROMISE_TERMS.some((term) => new RegExp(`(^|[^A-Za-z])${term}([^A-Za-z]|$)`, 'i').test(desc));
301
+ if (!promises) continue;
302
+ const delivery = req.deliveredBy && typeof req.deliveredBy === 'object' ? req.deliveredBy : null;
303
+ if (!delivery || (!delivery.tacId && !delivery.adrId)) {
304
+ findings.push({
305
+ id: `pass2-no-delivery-${rid}`, pass: 'pass2', kind: 'reqNoDelivery',
306
+ subject: rid, refs: [rid],
307
+ message: `${rid}.description promises an externally observable property but declares no deliveredBy link into a TAC or ADR.`,
308
+ });
309
+ continue;
310
+ }
311
+ if (delivery.tacId) {
312
+ const target = tacsById.get(delivery.tacId);
313
+ if (!target) {
314
+ findings.push({
315
+ id: `pass2-delivery-missing-tac-${rid}-${delivery.tacId}`, pass: 'pass2', kind: 'reqDeliveryNotCarried',
316
+ subject: rid, refs: [rid, delivery.tacId],
317
+ message: `${rid}.deliveredBy names TAC ${delivery.tacId} which is not present in the blueprint.`,
318
+ });
319
+ continue;
320
+ }
321
+ const carried = collectTacSurfaceText(target).toLowerCase();
322
+ const field = typeof delivery.field === 'string' ? delivery.field.toLowerCase() : '';
323
+ const fieldTail = field ? field.split('.').pop() : '';
324
+ if (fieldTail && !carried.includes(fieldTail)) {
325
+ findings.push({
326
+ id: `pass2-delivery-field-missing-${rid}-${delivery.tacId}`, pass: 'pass2', kind: 'reqDeliveryNotCarried',
327
+ subject: rid, refs: [rid, delivery.tacId, fieldTail],
328
+ message: `${rid}.deliveredBy points at ${delivery.tacId}.${delivery.field} but that field is not present on the TAC's responsibilities or interfaces.`,
329
+ });
330
+ }
331
+ }
332
+ }
333
+ return findings;
334
+ }
335
+
336
+ function collectTacSurfaceText(tac) {
337
+ const parts = [];
338
+ if (typeof tac.purpose === 'string') parts.push(tac.purpose);
339
+ if (typeof tac.internalStructure === 'string') parts.push(tac.internalStructure);
340
+ if (Array.isArray(tac.responsibilities)) parts.push(tac.responsibilities.filter((s) => typeof s === 'string').join(' '));
341
+ if (Array.isArray(tac.interfaces)) {
342
+ for (const iface of tac.interfaces) {
343
+ if (iface && typeof iface.name === 'string') parts.push(iface.name);
344
+ if (iface && typeof iface.summary === 'string') parts.push(iface.summary);
345
+ }
346
+ }
347
+ return parts.join(' ');
348
+ }
349
+
350
+ /**
351
+ * Load a blueprint from disk into the shape the passes consume.
352
+ *
353
+ * @param {string} sourcePath
354
+ * @returns {Promise<{ slug: string, sourcePath: string, tacs: object[], reqs: object[], userStories: object[], adrs: object[], guideMarkdown: string } | { error: string }>}
355
+ */
356
+ export async function loadForLint(sourcePath) {
357
+ let meta;
358
+ try {
359
+ const raw = await readFile(join(sourcePath, 'blueprint.json'), 'utf8');
360
+ meta = JSON.parse(raw);
361
+ } catch (err) {
362
+ return { error: `cannot read blueprint.json under ${sourcePath}: ${err.message}` };
363
+ }
364
+ const slug = meta.slug || '(unknown-slug)';
365
+ const contributions = Array.isArray(meta.contributions) ? meta.contributions : [];
366
+ const tacs = []; const reqs = []; const userStories = []; const adrs = [];
367
+ for (const c of contributions) {
368
+ if (!c || typeof c.path !== 'string') continue;
369
+ const abs = join(sourcePath, 'contributions', c.path);
370
+ let doc;
371
+ try { doc = JSON.parse(await readFile(abs, 'utf8')); } catch { continue; }
372
+ switch (c.kind) {
373
+ case 'tac': tacs.push(doc); break;
374
+ case 'req': reqs.push(doc); break;
375
+ case 'us': userStories.push(doc); break;
376
+ case 'adr': adrs.push(doc); break;
377
+ default: break;
378
+ }
379
+ }
380
+ let guideMarkdown = '';
381
+ try {
382
+ const entries = await readdir(join(sourcePath, 'guide'));
383
+ for (const name of entries) {
384
+ if (!name.endsWith('.md')) continue;
385
+ guideMarkdown += `${await readFile(join(sourcePath, 'guide', name), 'utf8')}\n`;
386
+ }
387
+ } catch { /* no guide directory is fine */ }
388
+ return { slug, sourcePath, tacs, reqs, userStories, adrs, guideMarkdown };
389
+ }
390
+
391
+ /**
392
+ * Extract "Known chain-consistency-lint suppressions" entries from a
393
+ * blueprint's README.md. Returns a Map from finding id to reason.
394
+ */
395
+ export async function loadSuppressions(sourcePath) {
396
+ const out = new Map();
397
+ let readme;
398
+ try { readme = await readFile(join(sourcePath, 'README.md'), 'utf8'); } catch { return out; }
399
+ const m = readme.match(/##\s+Known chain-consistency-lint suppressions([\s\S]*?)(\n##\s+|$)/);
400
+ if (!m) return out;
401
+ for (const line of m[1].split('\n')) {
402
+ const rm = line.match(/^\s*[-*]\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s*:\s*(.+?)\s*$/);
403
+ if (rm) out.set(rm[1], rm[2]);
404
+ }
405
+ return out;
406
+ }
407
+
408
+ /**
409
+ * Run both passes and shape the result envelope. Pass 1 findings that
410
+ * appear in `suppressions` are marked `suppressed: true`; pass 2
411
+ * findings are never suppressible.
412
+ */
413
+ export function runLint(input, suppressions = new Map()) {
414
+ const pass1 = runPass1(input);
415
+ const pass2 = runPass2(input);
416
+ let suppressedCount = 0;
417
+ for (const f of pass1) {
418
+ if (suppressions.has(f.id)) {
419
+ f.suppressed = true;
420
+ f.suppressionReason = suppressions.get(f.id);
421
+ suppressedCount += 1;
422
+ }
423
+ }
424
+ const findings = [...pass1, ...pass2];
425
+ const unsuppressed = findings.filter((f) => !f.suppressed);
426
+ return {
427
+ blueprint: input.slug,
428
+ findings,
429
+ verdict: unsuppressed.length === 0 ? 'pass' : 'fail',
430
+ passCounts: { pass1: pass1.length, pass2: pass2.length, suppressed: suppressedCount },
431
+ };
432
+ }
@@ -0,0 +1,224 @@
1
+ // Per-slug disposition ledger for applied blueprints. Sits alongside
2
+ // the existing `<slug>.applied.json` sidecar (see capabilities.js) and
3
+ // records one entry per AC contributed by the blueprint, plus the
4
+ // disposition the applying agent (or the operator) recorded.
5
+ //
6
+ // Schema follow-up: a `$defs/blueprintDispositionLedger` shape lands
7
+ // on `@stravica-ai/rcf-schemas` alongside the four fields the 0.6.2
8
+ // bump already ships (deliveredBy, ownerRef, disposition,
9
+ // vendorCitation). Until it does, the ledger validates itself via the
10
+ // local `validateRecord` guard so a malformed write refuses before
11
+ // touching the file.
12
+ //
13
+ // Ledger location: `rcf/blueprints/<slug>.disposition.json`.
14
+
15
+ import { readFile, mkdir, writeFile, stat } from 'node:fs/promises';
16
+ import { join, dirname } from 'node:path';
17
+
18
+ const ACTIONS = new Set([
19
+ 'pending-disposition',
20
+ 'pending-operator',
21
+ 'accepted',
22
+ 'adjusted',
23
+ 'dropped',
24
+ 'escalated-as-defect',
25
+ ]);
26
+
27
+ /**
28
+ * @typedef {object} DispositionRecord
29
+ * @property {string} acId
30
+ * @property {string} storyId
31
+ * @property {'fixed'|'template'} [sourceDisposition]
32
+ * @property {'pending-disposition'|'pending-operator'|'accepted'|'adjusted'|'dropped'|'escalated-as-defect'} action
33
+ * @property {string} [reason]
34
+ * @property {string} [resolvedAt]
35
+ * @property {string} [resolvedBy]
36
+ * @property {string} [escalatedAt]
37
+ * @property {boolean} [escalatedToOperator]
38
+ * @property {string} [defectUrl]
39
+ */
40
+
41
+ /**
42
+ * Ledger file path (relative to project root).
43
+ *
44
+ * @param {string} slug
45
+ */
46
+ export function ledgerRelPath(slug) {
47
+ return join('rcf', 'blueprints', `${slug}.disposition.json`);
48
+ }
49
+
50
+ /**
51
+ * Absolute path helper.
52
+ */
53
+ export function ledgerAbsPath(projectRoot, slug) {
54
+ return join(projectRoot, ledgerRelPath(slug));
55
+ }
56
+
57
+ /**
58
+ * Best-effort shape check for a single ledger record. Returns null on
59
+ * ok, an error string on any refusal.
60
+ *
61
+ * @param {any} rec
62
+ * @returns {string | null}
63
+ */
64
+ function validateRecord(rec) {
65
+ if (!rec || typeof rec !== 'object') return 'record is not an object';
66
+ if (typeof rec.acId !== 'string' || rec.acId.length === 0) return 'acId is missing';
67
+ if (typeof rec.storyId !== 'string' || rec.storyId.length === 0) return 'storyId is missing';
68
+ if (typeof rec.action !== 'string' || !ACTIONS.has(rec.action)) return `action must be one of ${[...ACTIONS].join(', ')}; got '${rec.action}'`;
69
+ if (rec.sourceDisposition !== undefined && rec.sourceDisposition !== 'fixed' && rec.sourceDisposition !== 'template') {
70
+ return `sourceDisposition must be 'fixed' or 'template' when set; got '${rec.sourceDisposition}'`;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Read a slug's ledger from disk. Missing file returns null.
77
+ *
78
+ * @param {string} projectRoot
79
+ * @param {string} slug
80
+ * @returns {Promise<{ slug: string, schemaVersion: number, records: DispositionRecord[] } | null>}
81
+ */
82
+ export async function readLedger(projectRoot, slug) {
83
+ const abs = ledgerAbsPath(projectRoot, slug);
84
+ try {
85
+ const raw = await readFile(abs, 'utf8');
86
+ return JSON.parse(raw);
87
+ } catch (err) {
88
+ if (err && err.code === 'ENOENT') return null;
89
+ throw err;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Presence check for the ledger file. Does not parse.
95
+ *
96
+ * @param {string} projectRoot
97
+ * @param {string} slug
98
+ * @returns {Promise<boolean>}
99
+ */
100
+ export async function ledgerExists(projectRoot, slug) {
101
+ try { await stat(ledgerAbsPath(projectRoot, slug)); return true; } catch { return false; }
102
+ }
103
+
104
+ /**
105
+ * Write a whole ledger document (overwrites). Validates every record
106
+ * before touching disk; a bad record returns { error: string } and
107
+ * writes nothing.
108
+ *
109
+ * @param {object} args
110
+ * @param {string} args.projectRoot
111
+ * @param {string} args.slug
112
+ * @param {DispositionRecord[]} args.records
113
+ * @returns {Promise<{ path: string } | { error: string }>}
114
+ */
115
+ export async function writeLedger({ projectRoot, slug, records }) {
116
+ if (!Array.isArray(records)) return { error: 'records must be an array' };
117
+ for (const r of records) {
118
+ const err = validateRecord(r);
119
+ if (err) return { error: `invalid ledger record for AC '${r?.acId ?? '?'}': ${err}` };
120
+ }
121
+ const abs = ledgerAbsPath(projectRoot, slug);
122
+ await mkdir(dirname(abs), { recursive: true });
123
+ const payload = { slug, schemaVersion: 1, records };
124
+ await writeFile(abs, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
125
+ return { path: ledgerRelPath(slug) };
126
+ }
127
+
128
+ /**
129
+ * Initialise a ledger for a freshly-applied blueprint. One record
130
+ * per AC contributed by the blueprint. Existing ledger (re-apply)
131
+ * is left byte-identical so operator disposition edits are never
132
+ * clobbered; the return value carries `alreadyExisted: true`.
133
+ *
134
+ * A `fixed` AC (per rcf-schemas 0.6.2 `$defs.acDisposition`) lands as
135
+ * `accepted` with the sentinel reason `fixed-mechanism-inherited`,
136
+ * per spec section 3.6.
137
+ *
138
+ * @param {object} args
139
+ * @param {string} args.projectRoot
140
+ * @param {string} args.slug
141
+ * @param {Array<{ id: string, storyId: string, sourceDisposition?: 'fixed'|'template' }>} args.acDescriptors
142
+ * @param {Date} [args.now]
143
+ * @returns {Promise<{ path: string, alreadyExisted: boolean, recordCount: number }>}
144
+ */
145
+ export async function initialiseLedger({ projectRoot, slug, acDescriptors, now = new Date() }) {
146
+ if (await ledgerExists(projectRoot, slug)) {
147
+ const existing = await readLedger(projectRoot, slug);
148
+ return { path: ledgerRelPath(slug), alreadyExisted: true, recordCount: existing?.records?.length ?? 0 };
149
+ }
150
+ const iso = now.toISOString().slice(0, 10);
151
+ const records = acDescriptors.map((d) => {
152
+ const base = { acId: d.id, storyId: d.storyId };
153
+ if (d.sourceDisposition === 'fixed') {
154
+ return { ...base, sourceDisposition: 'fixed', action: 'accepted', reason: 'fixed-mechanism-inherited', resolvedAt: iso, resolvedBy: 'blueprint-author' };
155
+ }
156
+ if (d.sourceDisposition === 'template') {
157
+ return { ...base, sourceDisposition: 'template', action: 'pending-disposition' };
158
+ }
159
+ return { ...base, action: 'pending-disposition' };
160
+ });
161
+ const res = await writeLedger({ projectRoot, slug, records });
162
+ if ('error' in res) throw new Error(`ledger init failed: ${res.error}`);
163
+ return { path: res.path, alreadyExisted: false, recordCount: records.length };
164
+ }
165
+
166
+ /**
167
+ * Update or append a single AC record. If the acId is present, its
168
+ * fields are merged (later fields win); otherwise the record is
169
+ * appended. Returns { updated: boolean, path } on success.
170
+ */
171
+ export async function upsertLedgerRecord({ projectRoot, slug, record }) {
172
+ const err = validateRecord(record);
173
+ if (err) return { error: err };
174
+ const doc = (await readLedger(projectRoot, slug)) ?? { slug, schemaVersion: 1, records: [] };
175
+ let updated = false;
176
+ const next = doc.records.map((r) => {
177
+ if (r.acId === record.acId) { updated = true; return { ...r, ...record }; }
178
+ return r;
179
+ });
180
+ if (!updated) next.push(record);
181
+ const res = await writeLedger({ projectRoot, slug, records: next });
182
+ if ('error' in res) return res;
183
+ return { updated, path: res.path };
184
+ }
185
+
186
+ // The prompt the operator sees at apply time. Kept a canonical string
187
+ // so tests can assert against it and the dispatch spec can echo it
188
+ // verbatim. Contains no em-dashes and no internal identifiers so it
189
+ // reads cleanly in third-party output.
190
+ export const APPLY_DISPOSITION_PROMPT = (slug, version, count) => (
191
+ `Blueprint '${slug}' v${version} contributed ${count} acceptance criteria. `
192
+ + `For each, you or the applying agent must record a disposition (accept as authored, adjust with a stated reason, `
193
+ + `drop with a stated reason, or escalate to the operator). `
194
+ + `Run 'rcf define blueprint dispositions ${slug}' at any time to see the current state; `
195
+ + `the applying agent will drive the walk unless you want to do it yourself.`
196
+ );
197
+
198
+ /**
199
+ * Ledger schema documentation the follow-up rcf-schemas PR will land.
200
+ * Kept close to the writer so the local guard and the eventual schema
201
+ * definition stay in step.
202
+ */
203
+ export const LEDGER_SCHEMA_NOTE = `
204
+ The per-slug disposition ledger is a JSON object with:
205
+ { slug: string,
206
+ schemaVersion: 1,
207
+ records: [
208
+ { acId: string,
209
+ storyId: string,
210
+ sourceDisposition?: 'fixed' | 'template',
211
+ action: 'pending-disposition' | 'pending-operator' | 'accepted' | 'adjusted' | 'dropped' | 'escalated-as-defect',
212
+ reason?: string,
213
+ resolvedAt?: string (ISO date),
214
+ resolvedBy?: string,
215
+ escalatedAt?: string (ISO date),
216
+ escalatedToOperator?: boolean,
217
+ defectUrl?: string
218
+ }, ...
219
+ ]
220
+ }
221
+ Follow-up: a $defs/blueprintDispositionLedger schema on @stravica-ai/rcf-schemas
222
+ will make the shape validator-checkable; today the ledger writer guards it
223
+ inline (see validateRecord).
224
+ `.trim();