rcf-lite 0.22.0 → 0.23.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 (151) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/rcf.js +13 -2
  3. package/blueprints/application-account-settings/docs/topics.md +2 -0
  4. package/blueprints/application-admin-console/docs/topics.md +2 -0
  5. package/blueprints/application-api-rest/docs/topics.md +2 -0
  6. package/blueprints/application-charts/docs/topics.md +2 -0
  7. package/blueprints/application-dashboard/docs/topics.md +2 -0
  8. package/blueprints/application-datatable/docs/topics.md +2 -0
  9. package/blueprints/application-empty-error-states/docs/topics.md +2 -0
  10. package/blueprints/application-error-handling/docs/topics.md +2 -0
  11. package/blueprints/application-file-upload/docs/topics.md +2 -0
  12. package/blueprints/application-forms-wizard/docs/topics.md +2 -0
  13. package/blueprints/application-notifications-in-app/docs/topics.md +2 -0
  14. package/blueprints/application-onboarding-tour/docs/topics.md +2 -0
  15. package/blueprints/application-spa/docs/topics.md +3 -1
  16. package/blueprints/delivery-ci-workflows/docs/topics.md +2 -0
  17. package/blueprints/deploy-cloudflare-workers/CHANGELOG.md +69 -0
  18. package/blueprints/deploy-cloudflare-workers/README.md +12 -0
  19. package/blueprints/deploy-cloudflare-workers/blueprint.json +234 -36
  20. package/blueprints/deploy-cloudflare-workers/contributions/adrs/adr-1306-deploy-cloudflare-workers-spa-shape.json +30 -0
  21. package/blueprints/deploy-cloudflare-workers/contributions/probes/assets-manifest-scan.mjs +207 -0
  22. package/blueprints/deploy-cloudflare-workers/contributions/requirements/deploy-cloudflare-workers-req-013.json +18 -0
  23. package/blueprints/deploy-cloudflare-workers/contributions/requirements/deploy-cloudflare-workers-req-014.json +18 -0
  24. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12113.json +36 -0
  25. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12114.json +27 -0
  26. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12115.json +27 -0
  27. package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -1
  28. package/blueprints/deploy-cloudflare-workers/guide/deploy-cloudflare-workers.md +25 -0
  29. package/blueprints/email-smtp-resend/docs/topics.md +2 -0
  30. package/blueprints/jobs-background/CHANGELOG.md +16 -0
  31. package/blueprints/jobs-background/README.md +105 -0
  32. package/blueprints/jobs-background/blueprint.json +149 -0
  33. package/blueprints/jobs-background/contributions/adrs/adr-3101-jobs-background-model.json +27 -0
  34. package/blueprints/jobs-background/contributions/adrs/adr-3102-jobs-background-scheduler-mode.json +25 -0
  35. package/blueprints/jobs-background/contributions/adrs/adr-3103-jobs-background-requires-applied-capabilities.json +25 -0
  36. package/blueprints/jobs-background/contributions/adrs/adr-3104-jobs-background-timeout-floor.json +25 -0
  37. package/blueprints/jobs-background/contributions/probes/apply-time-override.mjs +95 -0
  38. package/blueprints/jobs-background/contributions/probes/apply-time-refusal.mjs +82 -0
  39. package/blueprints/jobs-background/contributions/probes/event-secrecy.mjs +69 -0
  40. package/blueprints/jobs-background/contributions/probes/fake-clock-cron.mjs +65 -0
  41. package/blueprints/jobs-background/contributions/probes/probe-utils.mjs +91 -0
  42. package/blueprints/jobs-background/contributions/probes/retry-and-fail.mjs +65 -0
  43. package/blueprints/jobs-background/contributions/probes/run-apply-time-override.mjs +10 -0
  44. package/blueprints/jobs-background/contributions/probes/run-apply-time-refusal.mjs +11 -0
  45. package/blueprints/jobs-background/contributions/probes/run-event-secrecy.mjs +11 -0
  46. package/blueprints/jobs-background/contributions/probes/run-fake-clock-cron.mjs +11 -0
  47. package/blueprints/jobs-background/contributions/probes/run-retry-and-fail.mjs +11 -0
  48. package/blueprints/jobs-background/contributions/requirements/jobs-background-req-001.json +18 -0
  49. package/blueprints/jobs-background/contributions/requirements/jobs-background-req-002.json +18 -0
  50. package/blueprints/jobs-background/contributions/requirements/jobs-background-req-003.json +18 -0
  51. package/blueprints/jobs-background/contributions/requirements/jobs-background-req-004.json +18 -0
  52. package/blueprints/jobs-background/contributions/requirements/jobs-background-req-005.json +18 -0
  53. package/blueprints/jobs-background/contributions/requirements/jobs-background-req-006.json +18 -0
  54. package/blueprints/jobs-background/contributions/tacs/tac-3101-jobs-background-job-definition.json +50 -0
  55. package/blueprints/jobs-background/contributions/tacs/tac-3102-jobs-background-scheduler.json +50 -0
  56. package/blueprints/jobs-background/contributions/tacs/tac-3103-jobs-background-run-log.json +49 -0
  57. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30101.json +25 -0
  58. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30102.json +25 -0
  59. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30103.json +25 -0
  60. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30104.json +25 -0
  61. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30105.json +25 -0
  62. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30106.json +25 -0
  63. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30107.json +25 -0
  64. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30108.json +25 -0
  65. package/blueprints/jobs-background/contributions/user-stories/jobs-background-us-30109.json +25 -0
  66. package/blueprints/jobs-background/docs/topics.md +62 -0
  67. package/blueprints/jobs-background/guide/jobs-background.md +147 -0
  68. package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +15 -0
  69. package/blueprints/messaging-queue-cloudflare/README.md +125 -0
  70. package/blueprints/messaging-queue-cloudflare/blueprint.json +67 -0
  71. package/blueprints/messaging-queue-cloudflare/contributions/adrs/adr-3001-messaging-queue-cloudflare-adapter.json +30 -0
  72. package/blueprints/messaging-queue-cloudflare/contributions/adrs/adr-3002-messaging-queue-cloudflare-delivery-semantics.json +25 -0
  73. package/blueprints/messaging-queue-cloudflare/contributions/adrs/adr-3003-messaging-queue-cloudflare-max-attempts-floor.json +25 -0
  74. package/blueprints/messaging-queue-cloudflare/contributions/adrs/adr-3004-messaging-queue-cloudflare-batch-defaults.json +25 -0
  75. package/blueprints/messaging-queue-cloudflare/contributions/probes/event-secrecy.mjs +137 -0
  76. package/blueprints/messaging-queue-cloudflare/contributions/probes/probe-utils.mjs +82 -0
  77. package/blueprints/messaging-queue-cloudflare/contributions/probes/producer-facade-ready.mjs +35 -0
  78. package/blueprints/messaging-queue-cloudflare/contributions/probes/publish-to-delivery.mjs +94 -0
  79. package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +54 -0
  80. package/blueprints/messaging-queue-cloudflare/contributions/probes/retry-and-dlq.mjs +87 -0
  81. package/blueprints/messaging-queue-cloudflare/contributions/probes/run-event-secrecy.mjs +5 -0
  82. package/blueprints/messaging-queue-cloudflare/contributions/probes/run-producer-facade-ready.mjs +11 -0
  83. package/blueprints/messaging-queue-cloudflare/contributions/probes/run-publish-to-delivery.mjs +5 -0
  84. package/blueprints/messaging-queue-cloudflare/contributions/probes/run-real-account-concurrency-smoke.mjs +5 -0
  85. package/blueprints/messaging-queue-cloudflare/contributions/probes/run-retry-and-dlq.mjs +5 -0
  86. package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-001.json +18 -0
  87. package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-002.json +18 -0
  88. package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-003.json +18 -0
  89. package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-004.json +18 -0
  90. package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-005.json +18 -0
  91. package/blueprints/messaging-queue-cloudflare/contributions/requirements/messaging-queue-cloudflare-req-006.json +18 -0
  92. package/blueprints/messaging-queue-cloudflare/contributions/tacs/tac-3001-messaging-queue-cloudflare-producer-facade.json +56 -0
  93. package/blueprints/messaging-queue-cloudflare/contributions/tacs/tac-3002-messaging-queue-cloudflare-consumer-registration.json +40 -0
  94. package/blueprints/messaging-queue-cloudflare/contributions/tacs/tac-3003-messaging-queue-cloudflare-event-sink.json +39 -0
  95. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29101.json +34 -0
  96. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29102.json +34 -0
  97. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29103.json +25 -0
  98. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29104.json +25 -0
  99. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29105.json +25 -0
  100. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29106.json +25 -0
  101. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29107.json +25 -0
  102. package/blueprints/messaging-queue-cloudflare/contributions/user-stories/messaging-queue-cloudflare-us-29108.json +25 -0
  103. package/blueprints/messaging-queue-cloudflare/docs/topics.md +62 -0
  104. package/blueprints/messaging-queue-cloudflare/guide/messaging-queue-cloudflare.md +110 -0
  105. package/blueprints/object-storage-s3/docs/topics.md +2 -0
  106. package/blueprints/observability-essentials/docs/topics.md +2 -0
  107. package/blueprints/observability-logging/docs/topics.md +2 -0
  108. package/blueprints/observability-probe-endpoints/docs/topics.md +2 -0
  109. package/blueprints/persistence-data-d1/docs/topics.md +2 -0
  110. package/blueprints/persistence-data-postgres/docs/topics.md +2 -0
  111. package/blueprints/persistence-data-sqlite/docs/topics.md +2 -0
  112. package/blueprints/security-auth-clerk/docs/topics.md +2 -0
  113. package/blueprints/security-auth-keycloak/docs/topics.md +2 -0
  114. package/blueprints/security-auth-magic-link/docs/topics.md +2 -0
  115. package/blueprints/security-auth-oauth2/docs/topics.md +2 -0
  116. package/blueprints/security-secrets-management/docs/topics.md +2 -0
  117. package/fixtures/canary-manifest.json +6 -6
  118. package/package.json +1 -1
  119. package/rcf/code-nodes/cn-206.json +14 -0
  120. package/rcf/code-nodes/cn-207.json +13 -0
  121. package/rcf/code-nodes/cn-208.json +14 -0
  122. package/rcf/code-nodes/cn-209.json +14 -0
  123. package/rcf/code-nodes/cn-210.json +13 -0
  124. package/rcf/code-nodes/cn-211.json +12 -0
  125. package/rcf/code-nodes/cn-212.json +12 -0
  126. package/rcf/code-nodes/cn-213.json +12 -0
  127. package/rcf/code-nodes/cn-214.json +15 -0
  128. package/rcf/code-nodes/cn-215.json +13 -0
  129. package/rcf/code-nodes/cn-216.json +14 -0
  130. package/rcf/code-nodes/cn-217.json +13 -0
  131. package/rcf/fbs/fbs-062.json +20 -0
  132. package/rcf/fbs/fbs-063.json +22 -0
  133. package/rcf/fbs/fbs-064.json +23 -0
  134. package/rcf/requirements/req-029.json +57 -0
  135. package/rcf/requirements/req-030.json +67 -0
  136. package/rcf/requirements/req-042.json +59 -0
  137. package/rcf/requirements/req-043.json +39 -0
  138. package/rcf/test-suites/ts-072.json +62 -0
  139. package/rcf/test-suites/ts-073.json +38 -0
  140. package/rcf/test-suites/ts-074.json +30 -0
  141. package/rcf/test-suites/ts-075.json +38 -0
  142. package/rcf/test-suites/ts-076.json +70 -0
  143. package/rcf/user-stories/us-2901.json +45 -0
  144. package/rcf/user-stories/us-2902.json +36 -0
  145. package/rcf/user-stories/us-3001.json +45 -0
  146. package/rcf/user-stories/us-4201.json +69 -0
  147. package/rcf/user-stories/us-4301.json +78 -0
  148. package/releases/releases.yaml +11 -1
  149. package/src/blueprint/apply.js +7 -4
  150. package/src/blueprint/capabilities.js +25 -0
  151. package/src/cli/blueprint.js +8 -1
@@ -0,0 +1,30 @@
1
+ {
2
+ "adrId": "ADR-1306-deploy-cloudflare-workers-spa-shape",
3
+ "prdId": "PRD-001",
4
+ "tadId": "TAD-001",
5
+ "version": "1.2.0",
6
+ "status": "accepted",
7
+ "title": "Workers-with-static-assets is the ratified SPA-on-Workers deploy shape; Pages is a documented alternative held for legacy projects only",
8
+ "context": "Cloudflare ships two shapes for browsing static assets in front of a Workers-tier deploy target: Cloudflare Pages (a workflow-shaped product with its own build pipeline and its own preview URL model) and Workers-with-static-assets (a wrangler.toml [assets] block declaring a directory the Cloudflare runtime serves at the edge). Cloudflare's own Pages landing page states, verbatim (fetched 2026-09-06): 'Workers supports most Pages use cases and offers a broader feature set. It is Cloudflare's primary platform for building applications. Start new projects with Workers.' The prior v1.1.0 blueprint blessed neither shape as the default; v1.1.0 taught the wrangler [assets] block in an asset sample but left the shape choice implicit. The T-0 round bumps the blueprint to v1.2.0 to close the gap: a rcf-lite project choosing SPA-on-Workers gets a single, ratified wrangler shape rather than a hand-rolled convention, and the ratified shape follows Cloudflare's own recommendation for new projects.",
9
+ "decision": "The blueprint blesses Workers-with-static-assets as the ratified SPA-on-Workers deploy shape (recommendedDefault true for new SPA projects). A project answering the elicited assets-directory parameter with a non-empty path emits an [assets] block on wrangler.toml whose directory value carries the elicited path; a project answering with an empty path keeps the bare-Worker shape (no [assets] block). The elicited run-worker-first (boolean, default false) surfaces the SPA fallback discipline: truthy answers emit run_worker_first = true under the [assets] block; falsy or unanswered answers omit the field, letting Cloudflare's runtime run asset lookup before the Worker fetch handler. The blueprint does not mint a Cloudflare Pages blueprint; a future deploy-cloudflare-pages-legacy sibling mints on demand only if a project cannot make the jump. Every existing v1.1.0 contribution carries through unchanged.",
10
+ "consequences": "One ratified shape lands on the shelf for greenfield SPA-on-Workers projects; the shape's standards trace is the Cloudflare Pages landing-page recommendation (2026-09-06 snapshot). The two new elicits become part of the operator surface at blueprint add: assets-directory names the static-assets root, run-worker-first names the SPA fallback discipline. A project generating both an [assets] block and a Pages-only pages_build_output_dir field is a mixed-shape drift the T-0 probe (assets-manifest-scan.mjs) refuses. A project on the legacy Pages shape stays on v1.1.0 semantics until it migrates; the guide's new Workers-with-static-assets section links Cloudflare's migrate-from-Pages walkthrough for projects that choose to jump. Future round-6 blueprints (T-1 KV, T-2 cron, T-3 DO, T-4 Access, T-5 Turnstile, T-6 rate-limiting) all compose on this shape via the shared cf-platform sample-app fixture.",
11
+ "alternativesConsidered": [
12
+ {
13
+ "name": "Ship a Cloudflare Pages blueprint alongside deploy-cloudflare-workers",
14
+ "summary": "Mint a fresh deploy-cloudflare-pages blueprint that owns the Pages workflow surface (build pipeline, preview URLs, deploy hooks) and lets the operator pick shapes at apply.",
15
+ "reasonNotChosen": "Cloudflare's Pages landing page tells new projects to start with Workers. A Pages blueprint would ship a shape Cloudflare tells greenfield projects not to start. The maintenance load of a second deploy blueprint (its own ADRs, its own vendor URL trace, its own promote workflow) buys a shape Cloudflare itself recommends against; the round-6 spec (ruling 1) rejects the sibling and folds Workers-with-static-assets into this blueprint's minor bump instead."
16
+ },
17
+ {
18
+ "name": "Leave the shape choice implicit as the v1.1.0 blueprint does",
19
+ "summary": "Keep the wrangler-shape asset sample as an example only; do not mint an ADR blessing Workers-with-static-assets and do not elicit the assets-directory or run-worker-first parameters.",
20
+ "reasonNotChosen": "The v1.1.0 sample teaches the shape but does not commit to it; a project applying the blueprint has to reach for the sample and decide alone. Naming the ratified default as an ADR with a standards trace makes the shape decision legible in the applied project, and eliciting the two knobs surfaces the ratified shape's parameters where the operator can see them at apply."
21
+ },
22
+ {
23
+ "name": "Bless Workers-with-static-assets only through the guide, without an ADR contribution",
24
+ "summary": "Add the guide section but do not mint an ADR; keep the shape as prose guidance rather than an architected decision.",
25
+ "reasonNotChosen": "The standards-derived-discipline discipline (blueprint-authoring.md section 8a) requires an ADR with standardsTraceClause for any decision that traces to a vendor standard. Blessing the shape only in the guide leaves no traceable decision record and no place for a superseding project-level ADR to hang off. The blueprint's own trace mechanism is exactly this shape."
26
+ }
27
+ ],
28
+ "createdAt": "2026-09-07T00:00:00Z",
29
+ "updatedAt": "2026-09-07T00:00:00Z"
30
+ }
@@ -0,0 +1,207 @@
1
+ // Source-tree scan probe for the deploy-cloudflare-workers v1.2.0
2
+ // Workers-with-static-assets shape (T-0 of the Cloudflare round 6
3
+ // spec, section 5.0). accountBound false: this probe reads the
4
+ // applied fixture wrangler.toml and asserts three properties:
5
+ //
6
+ // 1. When the elicited assets-directory answer is non-empty, the
7
+ // [assets] table's directory field equals the elicited answer
8
+ // (assetsBlockEmitted, canonical AC-12113-1).
9
+ // 2. When the elicited run-worker-first answer is truthy, the
10
+ // [assets] table carries run_worker_first equal to true; when
11
+ // falsy or unanswered, run_worker_first is absent
12
+ // (spaFallbackShape facet of AC-12113-1).
13
+ // 3. The manifest never carries a Pages-only pages_build_output_dir
14
+ // field alongside [assets]; the two shapes are mutually
15
+ // exclusive per Cloudflare's static-assets doc.
16
+ //
17
+ // Verdict envelope per spec section 3.2: { verdict, detail,
18
+ // anchorAcId, aggregateVerdict } where anchorAcId is
19
+ // AC-12113-1 (the assetsBlockEmitted anchor). aggregateVerdict is
20
+ // pass when every result is pass, warn when any is warn (unused
21
+ // here), fail when any is fail. Cleans up any temporary files it
22
+ // wrote on exit.
23
+
24
+ import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
25
+ import { dirname, resolve, join } from 'node:path';
26
+
27
+ export const anchorAcId = 'AC-12113-1';
28
+ export const accountBound = false;
29
+
30
+ // Minimal wrangler.toml reader. Handles the small subset of TOML the
31
+ // blueprint's manifest declares (top-level scalars, one [assets]
32
+ // table, and boolean/string literals). Reaches for no runtime dep;
33
+ // keeps the probe accountBound-false and dependency-free.
34
+ export function parseWranglerToml(text) {
35
+ const lines = text.split(/\r?\n/);
36
+ const root = {};
37
+ let currentTable = root;
38
+ let currentTableName = '';
39
+ for (const raw of lines) {
40
+ const line = raw.replace(/^\s+/, '').replace(/\s+$/, '');
41
+ if (line === '' || line.startsWith('#')) continue;
42
+ const tableMatch = line.match(/^\[([A-Za-z0-9_.\-]+)\]$/);
43
+ if (tableMatch) {
44
+ currentTableName = tableMatch[1];
45
+ root[currentTableName] = root[currentTableName] || {};
46
+ currentTable = root[currentTableName];
47
+ continue;
48
+ }
49
+ const kv = line.match(/^([A-Za-z0-9_\-]+)\s*=\s*(.+)$/);
50
+ if (!kv) continue;
51
+ const key = kv[1];
52
+ let value = kv[2].trim();
53
+ // Strip trailing comments (a naive #-then-space split; sufficient
54
+ // for the wrangler shapes this blueprint generates).
55
+ const hashAt = value.indexOf(' #');
56
+ if (hashAt !== -1) value = value.slice(0, hashAt).trim();
57
+ if (value === 'true') {
58
+ currentTable[key] = true;
59
+ } else if (value === 'false') {
60
+ currentTable[key] = false;
61
+ } else if (/^-?\d+$/.test(value)) {
62
+ currentTable[key] = Number(value);
63
+ } else if (value.startsWith('"') && value.endsWith('"')) {
64
+ currentTable[key] = value.slice(1, -1);
65
+ } else if (value.startsWith("'") && value.endsWith("'")) {
66
+ currentTable[key] = value.slice(1, -1);
67
+ } else {
68
+ currentTable[key] = value;
69
+ }
70
+ }
71
+ return root;
72
+ }
73
+
74
+ // Drive the scan against the fixture. `opts.fixtureRoot` names the
75
+ // directory the wrangler.toml lives in; `opts.elicited` names the
76
+ // answers the fixture baked in (assets-directory string, run-worker-
77
+ // first boolean). Optionally `opts.simulate` is one of
78
+ // 'mixed-shape' or 'empty-assets': the caller rewrites wrangler.toml
79
+ // in a scratch copy for the simulate branches so the base fixture
80
+ // stays honest.
81
+ export async function scan(opts) {
82
+ const fixtureRoot = opts.fixtureRoot;
83
+ const elicited = opts.elicited || {};
84
+ const manifestPath = opts.manifestPath || join(fixtureRoot, 'wrangler.toml');
85
+ const text = await readFile(manifestPath, 'utf8');
86
+ const parsed = parseWranglerToml(text);
87
+ const results = [];
88
+ const detail = {
89
+ manifestPath,
90
+ parsed,
91
+ elicited: {
92
+ assetsDirectory: elicited['assets-directory'] || '',
93
+ runWorkerFirst: elicited['run-worker-first'] === true,
94
+ },
95
+ };
96
+
97
+ const assets = parsed.assets || null;
98
+ const answered = detail.elicited.assetsDirectory !== '';
99
+ if (answered) {
100
+ if (!assets) {
101
+ results.push({
102
+ verdict: 'fail',
103
+ detail: 'assets table absent: elicited assets-directory is "' + detail.elicited.assetsDirectory + '" but wrangler.toml carries no [assets] block',
104
+ anchorAcId,
105
+ });
106
+ } else if (assets.directory !== detail.elicited.assetsDirectory) {
107
+ results.push({
108
+ verdict: 'fail',
109
+ detail: 'assets.directory mismatch: wrangler.toml carries directory=' + JSON.stringify(assets.directory) + ' but elicited answer is ' + JSON.stringify(detail.elicited.assetsDirectory),
110
+ anchorAcId,
111
+ });
112
+ } else {
113
+ results.push({
114
+ verdict: 'pass',
115
+ detail: 'assets.directory equals elicited assets-directory answer ' + JSON.stringify(assets.directory),
116
+ anchorAcId,
117
+ });
118
+ }
119
+ const runWorkerFirstOnManifest = assets && Object.prototype.hasOwnProperty.call(assets, 'run_worker_first');
120
+ if (detail.elicited.runWorkerFirst) {
121
+ if (!runWorkerFirstOnManifest || assets.run_worker_first !== true) {
122
+ results.push({
123
+ verdict: 'fail',
124
+ detail: 'run_worker_first mismatch: elicited answer is true but manifest carries ' + (runWorkerFirstOnManifest ? String(assets.run_worker_first) : 'no run_worker_first key'),
125
+ anchorAcId,
126
+ });
127
+ } else {
128
+ results.push({
129
+ verdict: 'pass',
130
+ detail: 'assets.run_worker_first equals true, matching the elicited run-worker-first answer',
131
+ anchorAcId,
132
+ });
133
+ }
134
+ } else {
135
+ if (runWorkerFirstOnManifest && assets.run_worker_first !== false) {
136
+ results.push({
137
+ verdict: 'fail',
138
+ detail: 'run_worker_first mismatch: elicited answer is falsy but manifest carries run_worker_first=' + JSON.stringify(assets.run_worker_first),
139
+ anchorAcId,
140
+ });
141
+ } else {
142
+ results.push({
143
+ verdict: 'pass',
144
+ detail: 'assets.run_worker_first is absent (or false), matching the falsy elicited run-worker-first answer',
145
+ anchorAcId,
146
+ });
147
+ }
148
+ }
149
+ } else {
150
+ if (assets) {
151
+ results.push({
152
+ verdict: 'fail',
153
+ detail: 'assets table present without an elicited assets-directory answer: bare-Worker shape expected, wrangler.toml carries [assets] block',
154
+ anchorAcId,
155
+ });
156
+ } else {
157
+ results.push({
158
+ verdict: 'pass',
159
+ detail: 'no [assets] block on wrangler.toml, matching the bare-Worker shape (assets-directory elicit unanswered)',
160
+ anchorAcId,
161
+ });
162
+ }
163
+ }
164
+
165
+ const pagesFieldAtRoot = Object.prototype.hasOwnProperty.call(parsed, 'pages_build_output_dir');
166
+ const pagesFieldUnderAssets = assets && Object.prototype.hasOwnProperty.call(assets, 'pages_build_output_dir');
167
+ if (pagesFieldAtRoot || pagesFieldUnderAssets) {
168
+ results.push({
169
+ verdict: 'fail',
170
+ detail: 'mixed-shape drift: wrangler.toml carries pages_build_output_dir (' + (pagesFieldAtRoot ? 'top-level' : 'inside [assets]') + ') alongside the Workers-with-static-assets shape; the two are mutually exclusive per Cloudflare static-assets doc',
171
+ anchorAcId,
172
+ });
173
+ } else {
174
+ results.push({
175
+ verdict: 'pass',
176
+ detail: 'no pages_build_output_dir field on wrangler.toml (Workers-with-static-assets shape is exclusive of the Pages shape)',
177
+ anchorAcId,
178
+ });
179
+ }
180
+
181
+ const aggregateVerdict = results.some((r) => r.verdict === 'fail')
182
+ ? 'fail'
183
+ : results.some((r) => r.verdict === 'warn')
184
+ ? 'warn'
185
+ : 'pass';
186
+
187
+ return { verdict: aggregateVerdict, aggregateVerdict, anchorAcId, results, detail };
188
+ }
189
+
190
+ // Convenience for the run-shim: write a report file next to a
191
+ // project-relative .rcf/reports/ path. Creates the directory if it
192
+ // does not exist; the shim removes any scratch file it wrote to
193
+ // simulate the mixed-shape or empty-assets branch before exit.
194
+ export async function writeReport(reportPath, report) {
195
+ await mkdir(dirname(reportPath), { recursive: true });
196
+ await writeFile(reportPath, JSON.stringify(report, null, 2) + '\n', 'utf8');
197
+ }
198
+
199
+ export async function removeIfExists(path) {
200
+ try {
201
+ await rm(path, { force: true });
202
+ } catch (_err) {
203
+ // Nothing to clean.
204
+ }
205
+ }
206
+
207
+ export default { scan, writeReport, removeIfExists, parseWranglerToml, anchorAcId, accountBound };
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-cloudflare-workers-REQ-013",
3
+ "prdId": "PRD-001",
4
+ "title": "Workers-with-static-assets shape: an [assets] directory declared under wrangler blesses static assets served by the Worker",
5
+ "description": "The blueprint blesses the Workers-with-static-assets deploy shape as the ratified SPA-on-Workers shape (per ADR-1306, quoting the Cloudflare Pages landing-page recommendation verbatim). A project answering the elicited `assets-directory` parameter with a non-empty path emits an `[assets]` block on `wrangler.toml` whose `directory` value carries the elicited path (for example `directory = \"./dist\"`). Cloudflare's Workers runtime serves files from that directory at the edge before the Worker fetch handler runs (unless `run_worker_first` is set truthy per REQ-014). A project answering with an empty path keeps the bare-Worker shape (no `[assets]` block, no static-asset lookup). The wrangler manifest MUST NOT carry any Pages-only field (`pages_build_output_dir`) alongside `[assets]`; the two shapes are mutually exclusive per Cloudflare's static-assets doc.",
6
+ "category": "technical",
7
+ "domain": "deploy",
8
+ "priority": "must",
9
+ "rationale": "Cloudflare's own Pages landing page tells new projects to start with Workers; the Workers-with-static-assets shape is the primary SPA-on-Workers deploy shape. Blessing it here means a rcf-lite project choosing SPA-on-Workers gets a single, ratified wrangler shape rather than a hand-rolled convention. The mutual exclusion of `[assets]` and `pages_build_output_dir` keeps a project from silently drifting into a mixed-shape wrangler that Cloudflare's runtime refuses at deploy time.",
10
+ "tags": [
11
+ "blueprint:deploy-cloudflare-workers",
12
+ "category:13-static-assets"
13
+ ],
14
+ "version": "1.2.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-07T00:00:00Z",
17
+ "updatedAt": "2026-09-07T00:00:00Z"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-cloudflare-workers-REQ-014",
3
+ "prdId": "PRD-001",
4
+ "title": "SPA fallback discipline: run_worker_first flips the assets pipeline off the fetch handler's critical path",
5
+ "description": "The blueprint elicits `run-worker-first` (boolean, default false) alongside the `[assets] directory` shape from REQ-013. When the operator answers truthy, the generated wrangler manifest emits `run_worker_first = true` under the `[assets]` block: Cloudflare's runtime runs the Worker's fetch handler BEFORE looking up a static asset for the requested path. That is the shape a SPA-on-Workers project uses when the Worker owns arbitrary routing or when the Worker's auth middleware must gate every path a static file could shadow. When the operator answers falsy or leaves the elicit unanswered, `run_worker_first` is absent from the manifest and Cloudflare's runtime looks up a static asset first, falling back to the Worker only when no asset matches. The Worker code and the guide teach both shapes; the elicit surface is one boolean, the wrangler shape is one line.",
6
+ "category": "technical",
7
+ "domain": "deploy",
8
+ "priority": "should",
9
+ "rationale": "The default (asset lookup first, Worker on miss) is right for a static-content-heavy project. The truthy branch is what keeps auth middleware from being silently bypassed by an unauthenticated request for a file the assets pipeline serves from the edge cache. Naming the discipline as its own REQ makes the shape trade legible; anchoring the elicit to a wrangler line keeps the choice observable in the applied project.",
10
+ "tags": [
11
+ "blueprint:deploy-cloudflare-workers",
12
+ "category:13-static-assets"
13
+ ],
14
+ "version": "1.2.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-07T00:00:00Z",
17
+ "updatedAt": "2026-09-07T00:00:00Z"
18
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "usId": "deploy-cloudflare-workers-US-12113",
3
+ "prdId": "PRD-001",
4
+ "reqId": "deploy-cloudflare-workers-REQ-013",
5
+ "version": "1.2.0",
6
+ "status": "approved",
7
+ "title": "SPA-on-Workers deploy shape: [assets] block emitted with the elicited directory and the run_worker_first hint",
8
+ "asA": "operator deploying a SPA on Cloudflare Workers",
9
+ "iWant": "the generated wrangler.toml to carry an [assets] block whose directory matches my elicited answer and whose run_worker_first field carries my elicited SPA fallback choice",
10
+ "soThat": "my SPA-on-Workers project ships with a single, ratified wrangler shape and my static assets and my auth-gated routes both behave the way Cloudflare's runtime documents them",
11
+ "acceptanceCriteria": [
12
+ {
13
+ "id": "AC-12113-1",
14
+ "description": "Given the operator answers assets-directory with a non-empty path P at blueprint add, when the wrangler manifest generator emits wrangler.toml, then the manifest contains exactly one [assets] block whose directory field equals P (with the leading `./` preserved when the operator answered `./P`) and the manifest carries no pages_build_output_dir field. When the operator answers assets-directory with an empty string or leaves the elicit unanswered, the manifest carries no [assets] block and no pages_build_output_dir field. Anchored to TAC-1302-deploy-cloudflare-workers-wrangler-manifest; probed by assets-manifest-scan.mjs (source-tree scan against the applied fixture wrangler.toml) with anchorAcId AC-12113-1.",
15
+ "given": "an applied fixture whose operator answered assets-directory = ./dist at blueprint add",
16
+ "when": "the probe module assets-manifest-scan.mjs parses the fixture wrangler.toml",
17
+ "then": "the parsed structure contains an assets table whose directory string equals ./dist and no top-level pages_build_output_dir key; the probe returns verdict pass",
18
+ "testable": true,
19
+ "scope": "runtime"
20
+ },
21
+ {
22
+ "id": "AC-12113-2",
23
+ "description": "Given the operator answers run-worker-first with true alongside a non-empty assets-directory, when the manifest is generated, then the [assets] block carries run_worker_first = true. Given the operator answers run-worker-first with false or leaves it unanswered, when the manifest is generated, then the [assets] block does NOT carry a run_worker_first key (Cloudflare's runtime treats the absent field as the default asset-lookup-first shape). Anchored to TAC-1302; probed by assets-manifest-scan.mjs with anchorAcId AC-12113-1 (the same module surfaces both facets of the shape).",
24
+ "given": "an applied fixture whose operator answered assets-directory = ./dist and run-worker-first = true",
25
+ "when": "the probe module parses the fixture wrangler.toml",
26
+ "then": "the parsed [assets] table carries run_worker_first equal to true; when the fixture is switched with SIMULATE_EMPTY_ASSETS=true the run_worker_first field is absent (the whole [assets] block is absent) and the probe still returns verdict pass because the bare-Worker shape is legal",
27
+ "testable": true,
28
+ "scope": "runtime"
29
+ }
30
+ ],
31
+ "tacIds": [
32
+ "TAC-1302-deploy-cloudflare-workers-wrangler-manifest"
33
+ ],
34
+ "createdAt": "2026-09-07T00:00:00Z",
35
+ "updatedAt": "2026-09-07T00:00:00Z"
36
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "usId": "deploy-cloudflare-workers-US-12114",
3
+ "prdId": "PRD-001",
4
+ "reqId": "deploy-cloudflare-workers-REQ-013",
5
+ "version": "1.2.0",
6
+ "status": "approved",
7
+ "title": "assets-directory elicitation: the operator picks the static-assets root at blueprint add",
8
+ "asA": "operator applying deploy-cloudflare-workers on a fresh project",
9
+ "iWant": "the blueprint to prompt me for the static-assets directory at blueprint add and remember my answer on my project's sidecar so re-apply picks it back up",
10
+ "soThat": "my wrangler.toml reflects my project's build output shape without me having to hand-edit the [assets] block",
11
+ "acceptanceCriteria": [
12
+ {
13
+ "id": "AC-12114-1",
14
+ "description": "The blueprint.json elicits[] block declares an entry with id assets-directory, kind string, default empty. The blueprint loader accepts the entry (validateElicits shape gate on src/blueprint/loader.js). The CLI accepts --answer assets-directory=<path> at blueprint add; the applied sidecar records the answered value on appliedElicitations.assets-directory. When the operator does not pass --answer assets-directory, the effective value is the empty-string default and the wrangler generator emits no [assets] block.",
15
+ "given": "a fresh rcf init scratch project after rcf define blueprint add ./blueprints/deploy-cloudflare-workers --answer assets-directory=./public --answer run-worker-first=false",
16
+ "when": "the applied sidecar records the operator's answers and a wrangler.toml is generated",
17
+ "then": "appliedElicitations.assets-directory equals ./public; the generated wrangler.toml carries [assets] directory = \"./public\" and no run_worker_first field; the manifest carries no pages_build_output_dir field",
18
+ "testable": true,
19
+ "scope": "runtime"
20
+ }
21
+ ],
22
+ "tacIds": [
23
+ "TAC-1302-deploy-cloudflare-workers-wrangler-manifest"
24
+ ],
25
+ "createdAt": "2026-09-07T00:00:00Z",
26
+ "updatedAt": "2026-09-07T00:00:00Z"
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "usId": "deploy-cloudflare-workers-US-12115",
3
+ "prdId": "PRD-001",
4
+ "reqId": "deploy-cloudflare-workers-REQ-014",
5
+ "version": "1.2.0",
6
+ "status": "approved",
7
+ "title": "Migrate-from-Pages guidance: the guide links Cloudflare's own migrate-from-Pages walkthrough and pins the ratified shape choice",
8
+ "asA": "operator on a Cloudflare Pages project deciding whether to migrate",
9
+ "iWant": "the blueprint's guide to name the Workers-with-static-assets shape as the ratified default, quote Cloudflare's own landing-page recommendation verbatim, and link the migrate-from-Pages walkthrough Cloudflare publishes",
10
+ "soThat": "I can read the decision, the standards trace and the migration path in one place without stitching them together from three vendor pages",
11
+ "acceptanceCriteria": [
12
+ {
13
+ "id": "AC-12115-1",
14
+ "description": "The guide file blueprints/deploy-cloudflare-workers/guide/deploy-cloudflare-workers.md gains one new section titled Workers-with-static-assets: the SPA-on-Workers shape. The section carries the verbatim Cloudflare Pages landing-page quote (Workers supports most Pages use cases and offers a broader feature set. It is Cloudflare's primary platform for building applications. Start new projects with Workers.), the two elicit signatures (assets-directory string default empty; run-worker-first boolean default false), a copy-paste wrangler.toml snippet showing the [assets] block with directory and the run_worker_first hint, and a link to the Cloudflare migrate-from-Pages guide at https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/. The Cloudflare Workers static-assets doc at https://developers.cloudflare.com/workers/static-assets/ is cited in the same section. Both URLs return HTTP 200 at gate time.",
15
+ "given": "the updated guide file on the T-0 branch",
16
+ "when": "a reader opens the guide and grep locates the section heading, the verbatim quote and the two URLs",
17
+ "then": "the section heading appears once; the verbatim landing-page quote appears once; both Cloudflare URLs appear at least once each; a curl -fsSI on each URL returns HTTP 200",
18
+ "testable": true,
19
+ "scope": "library"
20
+ }
21
+ ],
22
+ "tacIds": [
23
+ "TAC-1302-deploy-cloudflare-workers-wrangler-manifest"
24
+ ],
25
+ "createdAt": "2026-09-07T00:00:00Z",
26
+ "updatedAt": "2026-09-07T00:00:00Z"
27
+ }
@@ -32,7 +32,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
32
32
  | security-auth-clerk | 9101-9899 | 10xx | shipped v1.0.0 | `authModel` |
33
33
  | security-auth-oauth2 | 10101-10899 | 11xx | shipped v1.0.0 | `authModel` |
34
34
  | security-auth-keycloak | 11101-11899 | 12xx | shipped v1.0.0 | `authModel` |
35
- | deploy-cloudflare-workers | 12101-12899 | 13xx | shipped v1.0.0 | `deploymentTarget` |
35
+ | deploy-cloudflare-workers | 12101-12899 | 13xx | shipped v1.2.0 | `deploymentTarget` |
36
36
  | persistence-data-d1 | 13101-13899 | 14xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
37
37
  | observability-probe-endpoints | 14101-14899 | 15xx | shipped v1.1.0 | `healthProbes`, `readinessSemantics` |
38
38
  | application-datatable | 17101-17899 | 18xx | shipped v1.0.0 | none |
@@ -47,6 +47,8 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
47
47
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
48
48
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
49
49
  | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
50
+ | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
51
+ | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
50
52
 
51
53
  US 12101-12112 sit at the LOW end of the 12101-12899 band on purpose. A project-side story that mechanically derives from a deploy-cloudflare-workers REQ id into the number `12112` would collide against deploy-cloudflare-workers-US-12112 in this package; the band leaves headroom at the HIGH end (US 12181-12899) so a project's own stories anchored to deploy-cloudflare-workers REQs can allocate without conflict. The watchpost run4 lesson applies here too. Band spacing between the last shipped block (security-secrets-management, 8101-8899, 9xx) and this one (12101-12899, 13xx) leaves the 9xxx-11xxx US bands and the 10xx-12xx suffix blocks open for concurrent-lane authoring so a peer authoring another blueprint in the same PR window does not collide with this one.
52
54
 
@@ -72,3 +72,28 @@ A project applies the blueprint on a fresh tree, declares its Worker in `wrangle
72
72
  ## Cost-honesty paragraph
73
73
 
74
74
  Shipping this doc set costs the project the following. Every ship to production is now a two-step act (merge to main, then an operator-triggered promote) instead of a single-step merge-to-production; the discipline is intentional and adds a small friction to prototype-shaped work where merge-to-production is fine. The default Cloudflare Workers vendor commits the project to an edge-runtime shape; a project whose runtime needs a long-lived process cannot use the default and has to supersede ADR-1302. The served-surface verifier fails CI on real drift; a project that wants faster promotes buys them by tightening the probe set or by accepting a larger risk of a failed promote, neither is free. The three URL kinds (per-version-id, stable alias, production) are three hostnames the project has to reason about; a project that would rather have one hostname loses the class of leaks-through-collapsed-URL the blueprint's ADR-1303 closes. The wrangler manifest becomes a piece of documentation the project maintains as part of its normal change flow; a project that would rather push deploy config into ad-hoc shell scripts loses the manifest's own validation gate. The deploy-log record is a stream the project now maintains; wiring it to a real sink (a status page, a SIEM, a data warehouse) is a project cost the blueprint does not carry. The blueprint says nothing about the vendor's own dashboard workflows, about DNS and TLS provisioning outside the vendor's own custom-hostname primitive, about compliance reporting on deploy events, or about staged canary or blue-green rollouts; a project that needs any of those spends its own build cycles on them and this blueprint does not save it any work there.
75
+
76
+ ## Workers-with-static-assets: the SPA-on-Workers shape
77
+
78
+ Cloudflare's own Pages landing page states, verbatim (fetched 2026-09-06): "Workers supports most Pages use cases and offers a broader feature set. It is Cloudflare's primary platform for building applications. Start new projects with Workers." The `deploy-cloudflare-workers` v1.2.0 bump follows the recommendation: new SPA-on-Workers projects apply this blueprint in the Workers-with-static-assets shape rather than reaching for Cloudflare Pages. `ADR-1306-deploy-cloudflare-workers-spa-shape` is the ratified decision and carries the verbatim quote on its body plus the standards trace on the contribution entry (`standardsTraceClause: Cloudflare Pages landing-page recommendation (2026-09-06)`). Cloudflare's Workers static-assets docs at `https://developers.cloudflare.com/workers/static-assets/` are the operational reference for the shape; the migrate-from-Pages walkthrough at `https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/` is the path for a project on Pages that wants to jump.
79
+
80
+ The bump adds two elicits on the top-level `elicits[]` block:
81
+
82
+ - `assets-directory` (kind `string`, default empty): the path (relative to the Worker source root) whose files Cloudflare serves at the edge as static assets. An empty answer keeps the bare-Worker shape (no `[assets]` block on the generated `wrangler.toml`); a non-empty answer emits `[assets] directory = "<answered path>"`.
83
+ - `run-worker-first` (kind `boolean`, default `false`): the SPA fallback discipline. A truthy answer emits `run_worker_first = true` under the `[assets]` block; a falsy or unanswered answer omits the field, leaving Cloudflare's runtime to serve a matching static asset before the Worker fetch handler runs. The elicit is only meaningful when `assets-directory` is non-empty (a Worker with no assets has nothing to serve first); the loader-side `when-elicitedNonEmpty` predicate the spec calls for is a mechanism follow-up, so the elicit fires unconditionally today.
84
+
85
+ Copy-paste `wrangler.toml` snippet for the ratified shape:
86
+
87
+ ```
88
+ name = "my-spa-worker"
89
+ main = "src/index.mjs"
90
+ compatibility_date = "2026-09-06"
91
+
92
+ [assets]
93
+ directory = "./dist"
94
+ run_worker_first = true
95
+ ```
96
+
97
+ The `[assets]` block and Pages' `pages_build_output_dir` field are mutually exclusive per Cloudflare's static-assets doc; the T-0 probe `assets-manifest-scan.mjs` (`accountBound: false`, `anchorAcId: AC-12113-1`) refuses on a manifest that carries both. A project on the bare-Worker shape (no static assets served at the edge) leaves both elicits unanswered and the probe reports the bare shape as passing.
98
+
99
+ Migrating from Cloudflare Pages: read Cloudflare's own walkthrough at `https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/`, apply this blueprint at v1.2.0 with `assets-directory` set to the build-output directory the Pages project pointed at (typically `./dist`, `./build`, or `./public`), pick `run-worker-first` per the project's routing shape, and drop the Pages workflow. The Workers-with-static-assets shape covers the SPA and API surfaces the Pages workflow covered; the promote and rollback verbs on this blueprint carry through unchanged.
@@ -43,6 +43,8 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
43
43
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
44
44
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
45
45
  | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
46
+ | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
47
+ | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
46
48
 
47
49
  US 4101-4106 sit at the LOW end of the 4101-4899 band on purpose. A project-side story that mechanically derives from an email-smtp-resend REQ id into the number `4106` (leading `4` + sequence `106`) would collide against email-smtp-resend-US-4106 in this package; the band leaves headroom at the HIGH end (US 4181-4899) so a project's own stories anchored to email-smtp-resend REQs can allocate without conflict. The watchpost run4 lesson applies here too.
48
50
 
@@ -0,0 +1,16 @@
1
+ # jobs-background changelog
2
+
3
+ ## 1.0.0 - 2026-09-06
4
+
5
+ Initial release. Background-jobs discipline over an applied `queue` capability, with a job-definition contract, a POSIX-cron plus one-shot-delayed scheduler, a retry contract that inherits the applied queue's max-attempts ceiling, and a metadata-only run-log event stream at four moments. Composes on the T-5 capability-consumer mechanism from the visual round; refuses apply on a bare project with exit 3 and the stable message id `jobs-background-no-queue` per Baz decision 5. Landed via the infra round 5 spec (ratified 2026-09-06) as track T-4, chained after T-3 `messaging-queue-cloudflare` v1.0.0.
6
+
7
+ - 22 contributions: 6 REQs on `requiresAppliedCapabilities: [queue]` / job-definition / scheduler / retry / run-log / operator-facing surface; 9 USs at 30101-30109 (one per REQ plus three cross-cutting cases: apply-time refusal, scheduled fire on cron, retry-then-DLQ); 3 TACs (3101 job-definition module, 3102 scheduler, 3103 run-log event sink); 4 ADRs (3101 `scope: global` on new topic `backgroundJobModel` retry-and-schedule as the shipped shape, 3102 scheduler mode elicited across `inProcess` / `workerCron` / `external` with reserved v1.1.0 `workflows` slot per section 5.7 of the spec, 3103 requires `capabilities: ["queue"]` with `allowSkipFlag: "allow-no-queue-yet"` and refusal message id `jobs-background-no-queue`, 3104 default handler timeout 60 seconds floor 1 second ceiling bounded by the applied queue's consumer wall-clock which for Cloudflare Queues is 15 minutes per the fetched limits doc).
8
+ - Five Node-only probes under `contributions/probes/` proven against the T-3 in-memory queue-driver seam and a fake-clock scheduler seam (SDR-3-a on US-29107 accepts the in-memory realisation of the Cloudflare Queues binding shape as the shipped local seam): `apply-time-refusal`, `apply-time-override`, `fake-clock-cron`, `retry-and-fail`, `event-secrecy`. All five carry `accountBound: false`; the live `wrangler dev` cron-trigger path is documented as a per-AC mechanism-reach gap in the fixture README per round-3 checklist 6.g.
9
+ - T-4 slice of the shared sample-app fixture at `packages/rcf-lite/test/fixtures/infra-s3-and-queue/` adds a `jobs/` directory carrying two toy job-definition modules (`send-welcome-email.mjs` with `retryPolicy: { maxAttempts: 3, backoff: exponential }` and `timeoutMs: 60000`; `refresh-cache.mjs` with `retryPolicy: { maxAttempts: 5, backoff: constant }`, `timeoutMs: 10000`, `cron: '* * * * *'`), `src/jobs-runtime.mjs` realising the runtime, `src/scheduler.mjs` realising the `inProcess` scheduler with a fake-clock seam, and `src/job-run-log.mjs` realising the T-4-owned event sink (whitelist `event, jobId, jobName, attempts, duration, timestamp` plus optional `terminalErrorCode`). Two wired induced-failure switches (`SIMULATE_HANDLER_THROW`, `SIMULATE_PII_IN_JOB_INPUT`) drive the retry-and-fail and event-secrecy probe negative-run paths.
10
+ - Declares `capabilities: ["backgroundJobs"]`, `suggestedCompanions: [{role: "logging"}, {role: "errorHandling"}]`, `providesRoles` absent, `requiresAppliedCapabilities: { capabilities: ["queue"], allowSkipFlag: "allow-no-queue-yet", refusalMessageId: "jobs-background-no-queue" }`. The T-3 `messaging-queue-cloudflare` v1.0.0 is the shipped provider of the `queue` capability.
11
+ - Contributes ADR-3101 as `scope: global` on new topic `backgroundJobModel`. Future stateful-workflow siblings (Temporal-shape, Airflow-shape) will conflict here by design; the reserved round-6 Workflows adapter (per Baz section 5.7) lives INSIDE this blueprint as a v1.1.0 minor scheduler-mode value at ADR-3102, not as a sibling.
12
+ - Every ADR contribution entry on `blueprint.json` carries a `standardsTraceClause` per section 8a.2 (mandatory per HQ mid-flight ruling after PR #154 gate).
13
+ - Extends `packages/rcf-lite/docs/blueprint-authoring.md` section 6a capability-declaration table with one row for `backgroundJobs` naming the reserved stateful-workflow sibling posture per spec section 5.6.
14
+ - Extends `packages/rcf-lite/src/cli/blueprint.js` OPTION_SPEC with `--allow-no-queue-yet` boolean flag piped into `allowNoAuthYet` alongside `--allow-no-auth-yet` and `--allow-no-secrets-yet`, and `packages/rcf-lite/src/blueprint/apply.js` `buildOverrideNote` family derivation to detect `queue` from the flag name so the sidecar notes read "no queue yet: applied under --allow-no-queue-yet" instead of "no auth yet".
15
+ - Extends `packages/rcf-lite/test/blueprint/capabilities-apply.test.js` with four cases covering the queue-capability path end to end: bare-project refusal with tag and provider-name assertions, `--allow-no-queue-yet` override with queue-family sidecar-note assertion, happy path with `messaging-queue-cloudflare` applied producing `appliedCapabilities` including `queue`, and backward-compatible `--allow-no-auth-yet` also unblocks the override.
16
+ - Reserved v1.1.0 minor: adds `scheduler: "workflows"` mode under ADR-3102 wrapping the same job-definition contract with Cloudflare Workflows `step.do` orchestration per section 5.7 of the spec and the fetched Workflows documentation.
@@ -0,0 +1,105 @@
1
+ # jobs-background v1.0.0
2
+
3
+ Background-jobs discipline over an applied `queue` capability. Ships a job-definition contract, a POSIX-cron plus one-shot-delayed scheduler, a retry contract that inherits the applied queue's max-attempts ceiling, and a metadata-only run-log event stream at four moments. Composes on the T-5 capability-consumer mechanism from the visual round; refuses apply on a bare project with exit 3 and the stable message id `jobs-background-no-queue` per Baz decision 5 on the ratified round-5 spec.
4
+
5
+ ## What this gives you
6
+
7
+ - A job-definition module shape you export from `./jobs/*.mjs`: `{ name, handler, inputSchema, retryPolicy, timeoutMs }`.
8
+ - A scheduler with POSIX cron and one-shot-delayed shapes, elicited across `inProcess` (a Node long-lived process), `workerCron` (Cloudflare Cron Triggers) and `external` (a Kubernetes CronJob, a systemd timer, GitHub Actions schedule). A `workflows` mode is reserved for the v1.1.0 minor per section 5.7 of the spec.
9
+ - A retry contract that inherits the applied queue's max-attempts ceiling (Cloudflare Queues at 100 per the T-3 messaging-queue-cloudflare ADR-3003) and expresses the elicited backoff shape (`exponential`, `constant`, `linear`).
10
+ - A metadata-only run-log at four moments (`jobScheduled`, `jobStarted`, `jobCompleted`, `jobFailed`). Record whitelist: `{ jobId, jobName, attempts, duration, timestamp }` plus optional `terminalErrorCode`. No job input, no handler output, no user id, no email, no SSN, ever.
11
+ - An operator-facing surface elicited across `cli`, `httpEndpoint`, or `none`. The blueprint ships the contract; the applying project builds the thin shim.
12
+
13
+ ## The six REQs
14
+
15
+ | REQ | Contract |
16
+ |---|---|
17
+ | `jobs-background-REQ-001` | Requires an applied `queue` capability; refuses apply on a bare project with exit 3 and the stable message id `jobs-background-no-queue`. |
18
+ | `jobs-background-REQ-002` | Job-definition contract: name, handler, input schema (opaque per project), retry policy, timeout. |
19
+ | `jobs-background-REQ-003` | Scheduler contract: POSIX cron string and one-shot delayed. |
20
+ | `jobs-background-REQ-004` | Retry contract: handler throws retryable error re-delivers via the applied queue; max-attempts elicited within the applied queue's own ceiling. |
21
+ | `jobs-background-REQ-005` | Job-run event log: `jobScheduled`, `jobStarted`, `jobCompleted`, `jobFailed`; metadata-only whitelist. |
22
+ | `jobs-background-REQ-006` | Operator-facing surface (elicited): `cli`, `httpEndpoint`, or `none`. |
23
+
24
+ ## Capability composition
25
+
26
+ `requiresAppliedCapabilities` on `blueprint.json`:
27
+
28
+ ```json
29
+ {
30
+ "capabilities": ["queue"],
31
+ "allowSkipFlag": "allow-no-queue-yet",
32
+ "refusalMessageId": "jobs-background-no-queue"
33
+ }
34
+ ```
35
+
36
+ Apply refuses on a bare project (no applied blueprint declares `capabilities: ["queue"]`) with exit 3 and the stable first-line tag `[jobs-background-no-queue]`. The refusal message explicitly names `messaging-queue-cloudflare` as the shipped provider and `--allow-no-queue-yet` as the override.
37
+
38
+ The `--allow-no-queue-yet` override records a note on the sidecar `rcf/blueprints/jobs-background.applied.json` (`notes: "no queue yet: applied under --allow-no-queue-yet; surfaces gated on queue will refuse at runtime until a queue blueprint is applied."`). Note: the spec section 5.4 prose reference to `manifest.blueprints[jobs-background].source.notes` is superseded by the shipped T-5 mechanism which writes the note on the sidecar because the applied-blueprint-record schema in rcf-schemas 0.6.1 is closed and cannot carry a `notes` field. `rcf define validate` reads the sidecar to flag surfaces that never activated.
39
+
40
+ ## Scheduler mode ADR (ADR-3102)
41
+
42
+ Scheduler mode is elicited across three values at v1.0.0 and reserves a fourth for v1.1.0:
43
+
44
+ - `inProcess`: a long-lived Node process holding `setInterval` per cron string and `setTimeout` per one-shot delay. Fits Node-server deploys; unsuitable for Cloudflare Workers.
45
+ - `workerCron`: Cloudflare Cron Triggers wire the scheduled event to a Worker handler that publishes to the applied queue, per the Cloudflare Cron Triggers documentation. Cron-trigger semantics are per-cron-string on the Worker's wrangler config, not per-job-cron on the runtime; the guide names the alignment. Refuses `workerCron` when the applied queue has no cron surface.
46
+ - `external`: an external cron runner (Kubernetes CronJob, GitHub Actions schedule, systemd timer) invokes an operator surface with a fire-now command.
47
+ - Reserved `workflows` (v1.1.0): wraps the same job-definition contract with Cloudflare Workflows `step.do` orchestration per section 5.7 of the spec and the round-6 proposal decision (b). Adds one enum value at ADR-3102 and one section to the guide; no facade re-shape.
48
+
49
+ ## Timeout ceiling (ADR-3104)
50
+
51
+ Default handler `timeoutMs` is 60000 (60 seconds); floor is 1000 (1 second); ceiling is bounded by the applied queue's consumer wall-clock. Cloudflare Queues caps consumer wall-clock at 15 minutes per the platform limits documentation; a job-definition module declaring a `timeoutMs` above the ceiling refuses at boot with a clear error naming the ceiling and the applied provider.
52
+
53
+ ## Companions and capabilities
54
+
55
+ - `capabilities: ["backgroundJobs"]` (per section 6a of `packages/rcf-lite/docs/blueprint-authoring.md`; a consumer blueprint reads this capability to obtain the job-definition and scheduler surfaces at apply time).
56
+ - `providesRoles`: absent.
57
+ - `suggestedCompanions`:
58
+ - `logging`: every `jobStarted` / `jobCompleted` / `jobFailed` event writes through the applied logger; a logging companion supplies the factory.
59
+ - `errorHandling`: a `jobFailed` constructs an internal error record with the terminal error code and the correlation id; an error-handling companion supplies the record factory and the boundary.
60
+
61
+ ## The five probes
62
+
63
+ Each probe module lives under `contributions/probes/` and exports the spec section 3.2 verdict envelope with an `anchorAcId` matching a contributed AC id on the blueprint. Each has a matching `run-<probe-name>.mjs` shim writing a per-blueprint report at `.rcf/reports/blueprints/jobs-background/<probe-name>.json` per spec section 3.4.
64
+
65
+ - `apply-time-refusal.mjs` (anchors `AC-jobs-requiresQueue`): invokes `rcf define blueprint add ./blueprints/jobs-background` against a bare Node scratch fixture with NO queue applied; asserts exit 3, greps stderr for the first-line tag `[jobs-background-no-queue]`, greps stderr for the explicit provider name `messaging-queue-cloudflare` and the override flag `--allow-no-queue-yet`. Q3 default per spec section 10: asserts BOTH exit code AND stable message id.
66
+ - `apply-time-override.mjs` (anchors `AC-jobs-overrideRecorded`): same bare scratch fixture, but with `--allow-no-queue-yet`; asserts exit 0 and the sidecar `rcf/blueprints/jobs-background.applied.json` records `slug: jobs-background`, `allowNoAuthYet: true`, `appliedCapabilities: []`, and a `notes` field containing `no queue yet` and `--allow-no-queue-yet` and `queue` (but not the auth or secrets-management family words).
67
+ - `fake-clock-cron.mjs` (anchors `AC-jobs-scheduledRunsOnCron`): with `messaging-queue-cloudflare` and `jobs-background` both applied on the shared sample-app fixture, drives the fixture's fake-clock scheduler through one POSIX cron minute for the refresh-cache job; asserts `jobStarted` fires within the elicited `fireToleranceMs` window (default 30000 ms) and `jobCompleted` fires within the elicited `timeoutMs` (10000 ms for refresh-cache).
68
+ - `retry-and-fail.mjs` (anchors `AC-jobs-retryOnHandlerFailure`): with `SIMULATE_HANDLER_THROW=true` on the fixture, schedules a send-welcome-email job (`maxAttempts: 3`), drives the queue through re-deliveries; asserts three `jobStarted` events fire with the same `jobId` and attempts counter 1, 2, 3, followed by a terminal `jobFailed` with a `terminalErrorCode`.
69
+ - `event-secrecy.mjs` (anchors `AC-jobs-eventSecrecy`): with `SIMULATE_PII_IN_JOB_INPUT=true` on the fixture, schedules a job with a PII fixture input `{ userId: 1234, ssn: "123-45-6789", email: "test@example.com" }`; drives to completion; asserts every event record carries only the whitelist `{ event, jobId, jobName, attempts, duration, timestamp }` (plus optional `terminalErrorCode`) and NO PII literal.
70
+
71
+ ## Running the probes
72
+
73
+ The probes drive against the shared sample-app fixture at `packages/rcf-lite/test/fixtures/infra-s3-and-queue/` (extended with `jobs/`, `src/jobs-runtime.mjs`, `src/scheduler.mjs`, `src/job-run-log.mjs`). No Docker, no `wrangler dev` process required; the T-3 in-memory queue-driver seam is the shipped local seam per SDR-3-a.
74
+
75
+ Two-line boot for the two headline probes:
76
+
77
+ ```sh
78
+ node ../../../../blueprints/jobs-background/contributions/probes/run-apply-time-refusal.mjs
79
+ node ../../../../blueprints/jobs-background/contributions/probes/run-fake-clock-cron.mjs
80
+ ```
81
+
82
+ The remaining three shims (`run-apply-time-override.mjs`, `run-retry-and-fail.mjs`, `run-event-secrecy.mjs`) follow the same pattern.
83
+
84
+ ## Elicited parameters
85
+
86
+ - `jobsDir` (default `./jobs/`).
87
+ - `defaultRetryPolicy` (`{ maxAttempts: 3, backoff: "exponential" }`).
88
+ - `defaultTimeoutMs` (60000).
89
+ - `schedulerMode` (`inProcess` | `workerCron` | `external`; reserved `workflows` at v1.1.0).
90
+ - `operatorSurface` (`cli` | `httpEndpoint` | `none`).
91
+ - `fireToleranceMs` (default 30000).
92
+
93
+ ## Known limitations (per-AC mechanism-reach form, round-3 checklist 6.g)
94
+
95
+ - AC-jobs-requiresQueue: PROVEN via `apply-time-refusal.mjs` against a bare scratch project on the shipped head (exit code and message id assertions run in-process). No live-only gap.
96
+ - AC-jobs-overrideRecorded: PROVEN via `apply-time-override.mjs` on the shipped head (sidecar note grep asserts `no queue yet`, `--allow-no-queue-yet`, and family word `queue`). No live-only gap.
97
+ - AC-jobs-scheduledRunsOnCron: PROVEN via `fake-clock-cron.mjs` on the shipped head against the in-memory queue-driver seam plus the injected fake-clock scheduler seam. LIVE `wrangler dev` cron-trigger firing under `workerCron` scheduler mode is the HQ gate reviewer's follow-up run per SDR-3-a; the shipped local seam proves the scheduler and runtime dispatch chain without a Cloudflare Queues account.
98
+ - AC-jobs-retryOnHandlerFailure: PROVEN via `retry-and-fail.mjs` on the shipped head (three `jobStarted` records at attempts 1, 2, 3 followed by terminal `jobFailed`). The in-memory queue-driver's re-delivery loop matches Cloudflare Queues' retry semantics per T-3's opaque-adapter clause; a live-account run against Cloudflare Queues is the T-3 real-account concurrency smoke's territory, not T-4's.
99
+ - AC-jobs-eventSecrecy: PROVEN via `event-secrecy.mjs` on the shipped head (grep on the serialised run-log stream returns zero matches for every PII fixture literal). No live-only gap; the whitelist enforcement lives in code, not in a runtime environment.
100
+ - `workerCron` refuses on an applied queue with no cron surface: DOCUMENTED at ADR-3102 in this blueprint's contribution set; not exercised at v1.0.0 because the shipped provider (`messaging-queue-cloudflare` v1.0.0) does not itself claim a cron surface (the cron surface is Workers-side per Cloudflare Cron Triggers, not Queues-side). A follow-up train fires the refusal live once a `queue`-capability provider with a cron surface ships.
101
+ - Reserved v1.1.0 `workflows` scheduler mode: DOCUMENTED at ADR-3102; the `fake-clock-cron.mjs` probe grows a `workflows-scheduler` variant when the v1.1.0 minor lands per section 5.7 of the spec.
102
+
103
+ ## Reserved v1.1.0 minor: Workflows adapter
104
+
105
+ The v1.1.0 minor adds a fourth `scheduler` value (`workflows`) at ADR-3102, wrapping the same job-definition contract with Cloudflare Workflows `step.do` orchestration per section 5.7 of the spec. No topic conflict (the `backgroundJobModel` topic answer stays retry-and-schedule), no facade re-shape. Adds one section to this guide.