rcf-lite 0.17.0 → 0.19.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 (161) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/blueprints/application-api-rest/README.md +5 -1
  3. package/blueprints/application-api-rest/blueprint.json +12 -4
  4. package/blueprints/application-api-rest/contributions/adrs/adr-304-application-api-rest-logging.json +3 -3
  5. package/blueprints/application-api-rest/contributions/requirements/application-api-rest-req-006.json +4 -4
  6. package/blueprints/application-api-rest/contributions/tacs/tac-306-application-api-rest-operability.json +9 -8
  7. package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2103.json +3 -3
  8. package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2108.json +27 -27
  9. package/blueprints/application-api-rest/docs/topics.md +6 -4
  10. package/blueprints/application-api-rest/guide/application-api-rest.md +5 -1
  11. package/blueprints/application-error-handling/README.md +42 -0
  12. package/blueprints/application-error-handling/assets/schemas/error-record.schema.json +17 -0
  13. package/blueprints/application-error-handling/blueprint.json +27 -0
  14. package/blueprints/application-error-handling/contributions/adrs/adr-1701-application-error-handling-record-shape.json +25 -0
  15. package/blueprints/application-error-handling/contributions/adrs/adr-1702-application-error-handling-classification-vocabulary.json +20 -0
  16. package/blueprints/application-error-handling/contributions/adrs/adr-1703-application-error-handling-transport-mapping.json +20 -0
  17. package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-001.json +15 -0
  18. package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-002.json +15 -0
  19. package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-003.json +15 -0
  20. package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-004.json +15 -0
  21. package/blueprints/application-error-handling/contributions/tacs/tac-1701-application-error-handling-boundary.json +45 -0
  22. package/blueprints/application-error-handling/contributions/tacs/tac-1702-application-error-handling-record-factory.json +40 -0
  23. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16101.json +25 -0
  24. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16102.json +25 -0
  25. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16103.json +34 -0
  26. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16104.json +25 -0
  27. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16105.json +25 -0
  28. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16106.json +25 -0
  29. package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16107.json +25 -0
  30. package/blueprints/application-error-handling/docs/topics.md +24 -0
  31. package/blueprints/application-error-handling/guide/application-error-handling.md +35 -0
  32. package/blueprints/application-spa/blueprint.json +12 -2
  33. package/blueprints/application-spa/docs/topics.md +4 -2
  34. package/blueprints/delivery-ci-workflows/docs/topics.md +2 -2
  35. package/blueprints/deploy-cloudflare-workers/docs/topics.md +2 -2
  36. package/blueprints/email-smtp-resend/docs/topics.md +2 -2
  37. package/blueprints/observability-essentials/README.md +6 -2
  38. package/blueprints/observability-essentials/blueprint.json +133 -33
  39. package/blueprints/observability-essentials/contributions/adrs/adr-801-observability-essentials-health-probes.json +4 -4
  40. package/blueprints/observability-essentials/contributions/adrs/adr-802-observability-essentials-readiness-semantics.json +4 -4
  41. package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-001.json +4 -4
  42. package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-002.json +4 -4
  43. package/blueprints/observability-essentials/contributions/tacs/tac-801-observability-essentials-liveness-probe.json +10 -9
  44. package/blueprints/observability-essentials/contributions/tacs/tac-802-observability-essentials-readiness-probe.json +17 -11
  45. package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7101.json +12 -3
  46. package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7102.json +12 -3
  47. package/blueprints/observability-essentials/docs/topics.md +15 -8
  48. package/blueprints/observability-essentials/guide/observability-essentials.md +9 -3
  49. package/blueprints/observability-logging/README.md +44 -0
  50. package/blueprints/observability-logging/assets/samples/log-line.json +13 -0
  51. package/blueprints/observability-logging/blueprint.json +27 -0
  52. package/blueprints/observability-logging/contributions/adrs/adr-1601-observability-logging-line-shape.json +25 -0
  53. package/blueprints/observability-logging/contributions/adrs/adr-1602-observability-logging-correlation-id-header.json +25 -0
  54. package/blueprints/observability-logging/contributions/adrs/adr-1603-observability-logging-redaction-categories.json +25 -0
  55. package/blueprints/observability-logging/contributions/adrs/adr-1604-observability-logging-level-vocabulary.json +20 -0
  56. package/blueprints/observability-logging/contributions/requirements/observability-logging-req-001.json +15 -0
  57. package/blueprints/observability-logging/contributions/requirements/observability-logging-req-002.json +15 -0
  58. package/blueprints/observability-logging/contributions/requirements/observability-logging-req-003.json +15 -0
  59. package/blueprints/observability-logging/contributions/requirements/observability-logging-req-004.json +15 -0
  60. package/blueprints/observability-logging/contributions/requirements/observability-logging-req-005.json +15 -0
  61. package/blueprints/observability-logging/contributions/tacs/tac-1601-observability-logging-logger-factory.json +46 -0
  62. package/blueprints/observability-logging/contributions/tacs/tac-1602-observability-logging-redaction-boundary.json +27 -0
  63. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15101.json +34 -0
  64. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15102.json +43 -0
  65. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15103.json +34 -0
  66. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15104.json +25 -0
  67. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15105.json +34 -0
  68. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15106.json +25 -0
  69. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15107.json +25 -0
  70. package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15108.json +25 -0
  71. package/blueprints/observability-logging/docs/topics.md +21 -0
  72. package/blueprints/observability-logging/guide/observability-logging.md +36 -0
  73. package/blueprints/observability-probe-endpoints/README.md +5 -1
  74. package/blueprints/observability-probe-endpoints/blueprint.json +116 -24
  75. package/blueprints/observability-probe-endpoints/contributions/adrs/adr-1503-observability-probe-endpoints-kubernetes-default.json +5 -5
  76. package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14102.json +11 -2
  77. package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14107.json +11 -2
  78. package/blueprints/observability-probe-endpoints/docs/topics.md +6 -6
  79. package/blueprints/observability-probe-endpoints/guide/observability-probe-endpoints.md +10 -0
  80. package/blueprints/persistence-data-d1/docs/topics.md +2 -2
  81. package/blueprints/persistence-data-sqlite/docs/topics.md +2 -2
  82. package/blueprints/security-auth-clerk/docs/topics.md +2 -2
  83. package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
  84. package/blueprints/security-auth-magic-link/docs/topics.md +2 -2
  85. package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
  86. package/blueprints/security-secrets-management/docs/topics.md +2 -2
  87. package/fixtures/canary-manifest.json +6 -6
  88. package/guidance/harness-template.md +8 -0
  89. package/guidance/managed/agent-instructions-block.hash +1 -1
  90. package/guidance/managed/agent-instructions-block.md +8 -0
  91. package/package.json +1 -1
  92. package/rcf/code-nodes/cn-074.json +19 -0
  93. package/rcf/code-nodes/cn-075.json +15 -0
  94. package/rcf/code-nodes/cn-076.json +14 -0
  95. package/rcf/code-nodes/cn-077.json +14 -0
  96. package/rcf/code-nodes/cn-078.json +15 -0
  97. package/rcf/code-nodes/cn-079.json +14 -0
  98. package/rcf/code-nodes/cn-080.json +15 -0
  99. package/rcf/code-nodes/cn-081.json +15 -0
  100. package/rcf/code-nodes/cn-082.json +15 -0
  101. package/rcf/code-nodes/cn-083.json +14 -0
  102. package/rcf/code-nodes/cn-084.json +15 -0
  103. package/rcf/code-nodes/cn-085.json +14 -0
  104. package/rcf/code-nodes/cn-086.json +16 -0
  105. package/rcf/code-nodes/cn-087.json +14 -0
  106. package/rcf/code-nodes/cn-088.json +14 -0
  107. package/rcf/code-nodes/cn-089.json +14 -0
  108. package/rcf/code-nodes/cn-090.json +14 -0
  109. package/rcf/code-nodes/cn-091.json +14 -0
  110. package/rcf/code-nodes/cn-092.json +15 -0
  111. package/rcf/code-nodes/cn-093.json +15 -0
  112. package/rcf/code-nodes/cn-094.json +14 -0
  113. package/rcf/code-nodes/cn-095.json +14 -0
  114. package/rcf/code-nodes/cn-096.json +14 -0
  115. package/rcf/code-nodes/cn-097.json +14 -0
  116. package/rcf/fbs/fbs-024.json +24 -0
  117. package/rcf/fbs/fbs-025.json +25 -0
  118. package/rcf/fbs/fbs-026.json +27 -0
  119. package/rcf/fbs/fbs-027.json +25 -0
  120. package/rcf/fbs/fbs-028.json +27 -0
  121. package/rcf/fbs/fbs-029.json +27 -0
  122. package/rcf/fbs/fbs-030.json +27 -0
  123. package/rcf/fbs/fbs-031.json +27 -0
  124. package/rcf/fbs/fbs-032.json +27 -0
  125. package/rcf/fbs/fbs-033.json +27 -0
  126. package/rcf/fbs/fbs-034.json +27 -0
  127. package/rcf/requirements/req-012.json +22 -0
  128. package/rcf/requirements/req-013.json +22 -0
  129. package/rcf/requirements/req-014.json +22 -0
  130. package/rcf/requirements/req-015.json +21 -0
  131. package/rcf/test-suites/ts-034.json +32 -0
  132. package/rcf/test-suites/ts-035.json +23 -0
  133. package/rcf/test-suites/ts-036.json +65 -0
  134. package/rcf/test-suites/ts-037.json +55 -0
  135. package/rcf/test-suites/ts-038.json +41 -0
  136. package/rcf/test-suites/ts-039.json +41 -0
  137. package/rcf/test-suites/ts-040.json +57 -0
  138. package/rcf/test-suites/ts-041.json +57 -0
  139. package/rcf/test-suites/ts-042.json +57 -0
  140. package/rcf/test-suites/ts-043.json +49 -0
  141. package/rcf/test-suites/ts-044.json +41 -0
  142. package/rcf/user-stories/us-1201.json +34 -0
  143. package/rcf/user-stories/us-1202.json +25 -0
  144. package/rcf/user-stories/us-1203.json +43 -0
  145. package/rcf/user-stories/us-1204.json +25 -0
  146. package/rcf/user-stories/us-1301.json +43 -0
  147. package/rcf/user-stories/us-1302.json +43 -0
  148. package/rcf/user-stories/us-1401.json +43 -0
  149. package/rcf/user-stories/us-1402.json +43 -0
  150. package/rcf/user-stories/us-1403.json +43 -0
  151. package/rcf/user-stories/us-1404.json +43 -0
  152. package/rcf/user-stories/us-1501.json +43 -0
  153. package/releases/releases.yaml +21 -1
  154. package/src/blueprint/apply.js +8 -0
  155. package/src/blueprint/companions.js +485 -0
  156. package/src/blueprint/index.js +17 -0
  157. package/src/blueprint/loader.js +243 -1
  158. package/src/blueprint/remove-resolution.js +104 -0
  159. package/src/cli/blueprint.js +350 -0
  160. package/src/cli/doctor.js +76 -1
  161. package/src/cli/validate.js +7 -0
@@ -26,6 +26,24 @@ const CONTRIBUTABLE_KINDS = new Set(['req', 'us', 'tac', 'adr', 'ts', 'cn']);
26
26
  const ROOT_SINGLETON_KINDS = new Set(['prd', 'tad', 'bs']);
27
27
  const EXCLUDED_KINDS = new Set(['fbs']);
28
28
 
29
+ // Role name grammar for providesRoles[] and suggestedCompanions[].role.
30
+ // Lower camelCase: starts with a lowercase letter, contains only letters
31
+ // and digits, no separators. Same shape as global-topic strings by
32
+ // design (a role name IS the topic name the paired ADR claims, per
33
+ // core-companions spec section 2.2), so `providesRoles: ["logging"]`
34
+ // and the ADR contribution `{"scope":"global","topic":"logging"}`
35
+ // co-locate the two facts on one string.
36
+ const ROLE_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
37
+
38
+ // Em-dash sentinel + emoji-ish detection for the suggestedCompanions
39
+ // reason string, per the estate-wide banned-tells baseline. Em-dash
40
+ // (U+2014) is refused outright; emojis approximated by a broad
41
+ // symbols / pictographs range. Reason is operator-facing prose, so the
42
+ // same discipline that applies to READMEs and guides applies here.
43
+ // Refused shapes surface as a validation rcfError before apply.
44
+ const EM_DASH = /—/;
45
+ const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F000}-\u{1F2FF}]/u;
46
+
29
47
  /**
30
48
  * @typedef {object} BlueprintContribution
31
49
  * @property {string} id canonical id (bare or already namespaced)
@@ -33,6 +51,32 @@ const EXCLUDED_KINDS = new Set(['fbs']);
33
51
  * @property {string} path relative to the blueprint's contributions/
34
52
  * @property {'global'} [scope] ADR only; marks whole-project decisions
35
53
  * @property {string} [topic] ADR only when scope=global; conflict key
54
+ * @property {boolean} [recommendedDefault] ADR only. Standards-derived
55
+ * discipline (spec 3.2): marks a SHOULD
56
+ * clause, or a choice-shaped MUST per
57
+ * amendment A2 (Baz 2026-09-04T12:20:31Z).
58
+ * @property {boolean} [elicited] ADR only. Marks a MAY clause: the
59
+ * applying operator supplies the value
60
+ * at apply.
61
+ * @property {string} [standardsTraceClause] ADR only. Standard clause
62
+ * identifier verbatim (`WSD-001 clause 3.1`,
63
+ * `RFC 7807 section 3.1`) or the sentinel
64
+ * `"generic enterprise practice"`. Required
65
+ * on every ADR contribution when the
66
+ * blueprint declares standardsTrace[].
67
+ */
68
+
69
+ /**
70
+ * @typedef {object} SuggestedCompanion
71
+ * @property {string} role lower camelCase role name
72
+ * @property {string} reason one-sentence operator-facing reason string
73
+ * (no em-dashes, no emojis)
74
+ */
75
+
76
+ /**
77
+ * @typedef {object} StandardsTraceEntry
78
+ * @property {string} id standard identifier (e.g. `WSD-001`)
79
+ * @property {string} version standard version (free-form)
36
80
  */
37
81
 
38
82
  /**
@@ -51,6 +95,12 @@ const EXCLUDED_KINDS = new Set(['fbs']);
51
95
  * vocabulary, so a new category can be
52
96
  * minted by adding it to the standard
53
97
  * without a code change.
98
+ * @property {string[]} [providesRoles] core-companions spec 2.1
99
+ * @property {SuggestedCompanion[]} [suggestedCompanions] core-companions spec 2.1
100
+ * @property {StandardsTraceEntry[]} [standardsTrace] standards-derived
101
+ * discipline (spec 3.2). When set, every
102
+ * ADR contribution MUST carry a non-null
103
+ * standardsTraceClause.
54
104
  * @property {BlueprintContribution[]} contributions
55
105
  */
56
106
 
@@ -94,10 +144,33 @@ export async function loadBlueprint(source) {
94
144
  version: doc.version,
95
145
  source: root,
96
146
  ...(typeof doc.category === 'string' ? { category: doc.category } : {}),
97
- contributions: Array.isArray(doc.contributions) ? doc.contributions : [],
147
+ ...(Array.isArray(doc.providesRoles) ? { providesRoles: doc.providesRoles.slice() } : {}),
148
+ ...(Array.isArray(doc.suggestedCompanions)
149
+ ? { suggestedCompanions: doc.suggestedCompanions.map((s) => ({ role: s.role, reason: s.reason })) }
150
+ : {}),
151
+ ...(Array.isArray(doc.standardsTrace)
152
+ ? { standardsTrace: doc.standardsTrace.map((s) => ({ id: s.id, version: s.version })) }
153
+ : {}),
154
+ contributions: Array.isArray(doc.contributions) ? doc.contributions.map(preserveAdrDisciplineFields) : [],
98
155
  };
99
156
  }
100
157
 
158
+ // Copy the standards-derived-discipline ADR fields (recommendedDefault,
159
+ // elicited, standardsTraceClause) onto the returned contribution shape
160
+ // verbatim so consumers (apply, tests, tooling) can read them without
161
+ // re-loading the blueprint.json. Non-ADR contributions ignore these
162
+ // fields; the loader does not enforce a kind gate on them because a
163
+ // blueprint author can meaningfully attach recommendedDefault to any
164
+ // ADR-flavoured contribution (the discipline is prose in section 8a,
165
+ // not code, per amendment A2).
166
+ function preserveAdrDisciplineFields(c) {
167
+ const out = { ...c };
168
+ if (out.recommendedDefault !== undefined) out.recommendedDefault = c.recommendedDefault === true;
169
+ if (out.elicited !== undefined) out.elicited = c.elicited === true;
170
+ if (typeof c.standardsTraceClause === 'string') out.standardsTraceClause = c.standardsTraceClause;
171
+ return out;
172
+ }
173
+
101
174
  function validateMetadata(doc, metaPath) {
102
175
  if (typeof doc !== 'object' || doc === null) {
103
176
  return rcfError({ kind: 'validation', message: 'blueprint.json must be a JSON object', filePath: metaPath });
@@ -169,6 +242,175 @@ function validateMetadata(doc, metaPath) {
169
242
  if (c.scope === 'global' && typeof c.topic !== 'string') {
170
243
  return rcfError({ kind: 'validation', message: `blueprint.json: scope=global contribution ${c.id} requires a topic`, filePath: metaPath });
171
244
  }
245
+ // Standards-derived discipline (spec 3.2) per-ADR fields. Shape
246
+ // gates only; whether a MUST clause landed on an AC vs an ADR is
247
+ // prose in blueprint-authoring.md section 8a, not code (amendment
248
+ // A2 Baz 2026-09-04T12:20:31Z).
249
+ if (c.recommendedDefault !== undefined && typeof c.recommendedDefault !== 'boolean') {
250
+ return rcfError({ kind: 'validation', message: `blueprint.json: contribution ${c.id} recommendedDefault must be a boolean when set`, filePath: metaPath });
251
+ }
252
+ if (c.elicited !== undefined && typeof c.elicited !== 'boolean') {
253
+ return rcfError({ kind: 'validation', message: `blueprint.json: contribution ${c.id} elicited must be a boolean when set`, filePath: metaPath });
254
+ }
255
+ if (c.standardsTraceClause !== undefined) {
256
+ if (typeof c.standardsTraceClause !== 'string' || c.standardsTraceClause.trim().length === 0) {
257
+ return rcfError({ kind: 'validation', message: `blueprint.json: contribution ${c.id} standardsTraceClause must be a non-empty string when set`, filePath: metaPath });
258
+ }
259
+ }
260
+ }
261
+ // Companion-suggestion mechanism fields (core-companions spec 2.1).
262
+ const rolesError = validateProvidesRoles(doc, metaPath);
263
+ if (rolesError) return rolesError;
264
+ const suggestedError = validateSuggestedCompanions(doc, metaPath);
265
+ if (suggestedError) return suggestedError;
266
+ // Paired-ADR gate (spec 2.1): a blueprint declaring a role in
267
+ // providesRoles[] MUST carry a scope:global ADR whose topic string
268
+ // equals the role name. Enforced after per-contribution validation
269
+ // so a mis-authored contribution fails first with the more specific
270
+ // shape error.
271
+ const pairedError = validateProvidesRolesPairedAdrs(doc, metaPath);
272
+ if (pairedError) return pairedError;
273
+ // Standards-derived discipline (spec 3.3): if standardsTrace[] is
274
+ // set, every ADR contribution MUST carry a non-null
275
+ // standardsTraceClause. The loader does NOT cross-check clause
276
+ // severity to kind (per amendment A2); the discipline is prose.
277
+ const stError = validateStandardsTrace(doc, metaPath);
278
+ if (stError) return stError;
279
+ return null;
280
+ }
281
+
282
+ /**
283
+ * Validate providesRoles[] shape (spec 2.1). Optional; when present
284
+ * must be a non-empty array of lower camelCase strings on the pattern
285
+ * ^[a-z][a-zA-Z0-9]*$.
286
+ */
287
+ function validateProvidesRoles(doc, metaPath) {
288
+ if (doc.providesRoles === undefined) return null;
289
+ if (!Array.isArray(doc.providesRoles) || doc.providesRoles.length === 0) {
290
+ return rcfError({ kind: 'validation', message: 'blueprint.json: providesRoles must be a non-empty array when set', filePath: metaPath });
291
+ }
292
+ for (let i = 0; i < doc.providesRoles.length; i += 1) {
293
+ const role = doc.providesRoles[i];
294
+ if (typeof role !== 'string' || !ROLE_NAME_RE.test(role)) {
295
+ return rcfError({
296
+ kind: 'validation',
297
+ message: `blueprint.json: providesRoles[${i}] '${role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).`,
298
+ filePath: metaPath,
299
+ });
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+
305
+ /**
306
+ * Validate suggestedCompanions[] shape (spec 2.1). Optional; when
307
+ * present must be a non-empty array of `{role, reason}` objects; role
308
+ * is a lower camelCase string; reason is a non-empty string with no
309
+ * em-dashes and no emojis (banned-tells baseline applies to operator-
310
+ * facing prose).
311
+ */
312
+ function validateSuggestedCompanions(doc, metaPath) {
313
+ if (doc.suggestedCompanions === undefined) return null;
314
+ if (!Array.isArray(doc.suggestedCompanions) || doc.suggestedCompanions.length === 0) {
315
+ return rcfError({ kind: 'validation', message: 'blueprint.json: suggestedCompanions must be a non-empty array when set', filePath: metaPath });
316
+ }
317
+ for (let i = 0; i < doc.suggestedCompanions.length; i += 1) {
318
+ const entry = doc.suggestedCompanions[i];
319
+ if (typeof entry !== 'object' || entry === null) {
320
+ return rcfError({ kind: 'validation', message: `blueprint.json: suggestedCompanions[${i}] must be an object with role and reason`, filePath: metaPath });
321
+ }
322
+ if (typeof entry.role !== 'string' || !ROLE_NAME_RE.test(entry.role)) {
323
+ return rcfError({
324
+ kind: 'validation',
325
+ message: `blueprint.json: suggestedCompanions[${i}].role '${entry.role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).`,
326
+ filePath: metaPath,
327
+ });
328
+ }
329
+ if (typeof entry.reason !== 'string' || entry.reason.trim().length === 0) {
330
+ return rcfError({
331
+ kind: 'validation',
332
+ message: `blueprint.json: suggestedCompanions[${i}].reason for role '${entry.role}' must be a non-empty string.`,
333
+ filePath: metaPath,
334
+ });
335
+ }
336
+ if (EM_DASH.test(entry.reason)) {
337
+ return rcfError({
338
+ kind: 'validation',
339
+ message: `blueprint.json: suggestedCompanions[${i}].reason for role '${entry.role}' contains an em-dash; use a comma, a full stop, or a colon.`,
340
+ filePath: metaPath,
341
+ });
342
+ }
343
+ if (EMOJI_RE.test(entry.reason)) {
344
+ return rcfError({
345
+ kind: 'validation',
346
+ message: `blueprint.json: suggestedCompanions[${i}].reason for role '${entry.role}' contains an emoji; operator-facing prose must be plain text.`,
347
+ filePath: metaPath,
348
+ });
349
+ }
350
+ }
351
+ return null;
352
+ }
353
+
354
+ /**
355
+ * Paired-ADR gate for providesRoles (spec 2.1). A blueprint that
356
+ * declares a role MUST also carry a scope:global ADR whose topic
357
+ * string equals the role name. The check runs after per-contribution
358
+ * shape validation so the more specific per-contribution message
359
+ * fires first on a mis-authored file.
360
+ */
361
+ function validateProvidesRolesPairedAdrs(doc, metaPath) {
362
+ if (!Array.isArray(doc.providesRoles) || doc.providesRoles.length === 0) return null;
363
+ const globalTopics = new Set(
364
+ (doc.contributions ?? [])
365
+ .filter((c) => c.kind === 'adr' && c.scope === 'global' && typeof c.topic === 'string')
366
+ .map((c) => c.topic),
367
+ );
368
+ for (const role of doc.providesRoles) {
369
+ if (!globalTopics.has(role)) {
370
+ return rcfError({
371
+ kind: 'validation',
372
+ message: `blueprint.json: providesRoles[] names '${role}' but no scope:global ADR carries topic '${role}'.`,
373
+ filePath: metaPath,
374
+ });
375
+ }
376
+ }
377
+ return null;
378
+ }
379
+
380
+ /**
381
+ * Standards-derived-discipline gate (spec 3.3). If standardsTrace[]
382
+ * is declared, every ADR contribution MUST carry a non-null
383
+ * standardsTraceClause. No cross-check on severity-to-kind mapping
384
+ * (amendment A2 Baz 2026-09-04T12:20:31Z): the discipline is prose in
385
+ * blueprint-authoring.md section 8a, not code.
386
+ */
387
+ function validateStandardsTrace(doc, metaPath) {
388
+ if (doc.standardsTrace === undefined) return null;
389
+ if (!Array.isArray(doc.standardsTrace)) {
390
+ return rcfError({ kind: 'validation', message: 'blueprint.json: standardsTrace must be an array when set', filePath: metaPath });
391
+ }
392
+ for (let i = 0; i < doc.standardsTrace.length; i += 1) {
393
+ const entry = doc.standardsTrace[i];
394
+ if (typeof entry !== 'object' || entry === null) {
395
+ return rcfError({ kind: 'validation', message: `blueprint.json: standardsTrace[${i}] must be an object with id and version`, filePath: metaPath });
396
+ }
397
+ if (typeof entry.id !== 'string' || entry.id.trim().length === 0) {
398
+ return rcfError({ kind: 'validation', message: `blueprint.json: standardsTrace[${i}].id must be a non-empty string`, filePath: metaPath });
399
+ }
400
+ if (typeof entry.version !== 'string' || entry.version.trim().length === 0) {
401
+ return rcfError({ kind: 'validation', message: `blueprint.json: standardsTrace[${i}].version must be a non-empty string`, filePath: metaPath });
402
+ }
403
+ }
404
+ const slug = doc.slug;
405
+ for (const c of doc.contributions ?? []) {
406
+ if (c.kind !== 'adr') continue;
407
+ if (typeof c.standardsTraceClause !== 'string' || c.standardsTraceClause.trim().length === 0) {
408
+ return rcfError({
409
+ kind: 'validation',
410
+ message: `blueprint '${slug}' declares standardsTrace but ADR contribution '${c.id}' has no standardsTraceClause; every ADR must reference a standard clause or the sentinel 'generic enterprise practice'.`,
411
+ filePath: metaPath,
412
+ });
413
+ }
172
414
  }
173
415
  return null;
174
416
  }
@@ -0,0 +1,104 @@
1
+ // `rcf define blueprint remove-resolution <adr-id>` implementation.
2
+ //
3
+ // Removes a single entry from `manifest.resolutions[]` (and NOTHING
4
+ // else). The `<adr-id>` argument is matched against the entry's
5
+ // `resolvedByAdrId` field, which is how the doctor's probe-path-owner
6
+ // check (spec section 9) and the four-path resolution card (spec
7
+ // section 4) name the resolution to the operator.
8
+ //
9
+ // Behaviours (spec amendment A2, ratified 2026-09-04):
10
+ // - Removes the resolutions[] entry whose `resolvedByAdrId` equals
11
+ // the argument; leaves every other manifest section untouched. The
12
+ // project-level ADR file at `rcf/adrs/<adr-id>.json` is NOT
13
+ // deleted: an operator who wants to keep the ruling ADR as
14
+ // historical context after the redundant resolution goes away has
15
+ // that path open; an operator who wants the ADR gone can rm it
16
+ // themselves.
17
+ // - Refuses exit 2 when the argument is not a resolution entry on
18
+ // this manifest. Two flavours count as "not a resolution entry":
19
+ // the id is malformed (fails the ADR-\d{3,}(-<kebab-tail>)? grammar),
20
+ // or the id is well-formed but names no ADR anywhere on the
21
+ // tree AND is not present under any resolutions[] entry.
22
+ // - Idempotent on a second run: when the id is a well-formed ADR
23
+ // id that names an ADR present on the tree (the ruling ADR that
24
+ // the resolution had pointed at) but is not (any longer) present
25
+ // under any resolutions[] entry, the module returns
26
+ // `{ removed: false, alreadyAbsent: true }` and the CLI edge
27
+ // prints "nothing to remove" and exits 0. The distinction is:
28
+ // the ruling ADR still exists on disk, so the operator is running
29
+ // the SAME operation a second time, not typing a bogus id.
30
+ //
31
+ // The verb never touches project ADR files, blueprint records, or any
32
+ // other manifest section. That keeps the scope narrow enough that the
33
+ // operator can reason about the write without reading the module.
34
+
35
+ import { isRcfError, rcfError } from '../core/errors/index.js';
36
+ import { updateManifest } from './manifest-writer.js';
37
+
38
+ const ADR_ID_PATTERN = /^ADR-\d{3,}(?:-[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
39
+
40
+ /**
41
+ * @typedef {object} RemoveResolutionResult
42
+ * @property {boolean} removed
43
+ * @property {boolean} [alreadyAbsent]
44
+ * @property {string} resolvedByAdrId
45
+ * @property {string} [resolutionId]
46
+ * @property {string} [topic]
47
+ */
48
+
49
+ /**
50
+ * @param {object} args
51
+ * @param {string} args.projectRoot
52
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
53
+ * @param {string} args.resolvedByAdrId
54
+ * @param {boolean} [args.dryRun]
55
+ * @returns {Promise<RemoveResolutionResult | import('../core/errors/index.js').RcfError>}
56
+ */
57
+ export async function removeResolution({ projectRoot, tree, resolvedByAdrId, dryRun = false }) {
58
+ if (typeof resolvedByAdrId !== 'string' || resolvedByAdrId.trim().length === 0) {
59
+ return rcfError({ kind: 'usage', message: `<adr-id> is required (e.g. rcf define blueprint remove-resolution ADR-011-health-probes).` });
60
+ }
61
+ if (!ADR_ID_PATTERN.test(resolvedByAdrId)) {
62
+ return rcfError({ kind: 'usage', message: `'${resolvedByAdrId}' is not a well-formed ADR id (grammar: ADR-\\d{3,}(-<kebab-tail>)?).` });
63
+ }
64
+ const manifest = tree.manifest ?? {};
65
+ const resolutions = Array.isArray(manifest.resolutions) ? manifest.resolutions : [];
66
+ const index = resolutions.findIndex((r) => r?.resolvedByAdrId === resolvedByAdrId);
67
+ if (index === -1) {
68
+ // Not on resolutions[]. Two branches:
69
+ // - the ruling ADR file exists on the tree: idempotent no-op
70
+ // (this is a re-run of the same operation).
71
+ // - no ADR by that id anywhere: refuse. The operator has typed
72
+ // an id that this project has no record of, either as a
73
+ // resolution entry or as a project ADR.
74
+ const adrPresent = tree.byId instanceof Map && tree.byId.has(resolvedByAdrId);
75
+ if (adrPresent) {
76
+ return { removed: false, alreadyAbsent: true, resolvedByAdrId };
77
+ }
78
+ return rcfError({
79
+ kind: 'usage',
80
+ message: `'${resolvedByAdrId}' is not a resolution entry on this manifest (no resolutions[] record names it as resolvedByAdrId, and no ADR by that id exists on the project tree).`,
81
+ });
82
+ }
83
+ const target = resolutions[index];
84
+ const result = await updateManifest({
85
+ projectRoot,
86
+ manifest,
87
+ mutate: (next) => {
88
+ const list = Array.isArray(next.resolutions) ? next.resolutions : [];
89
+ list.splice(index, 1);
90
+ // Drop the field entirely when empty so the manifest shape stays
91
+ // as compact as it was before any resolution was ever recorded.
92
+ if (list.length === 0) delete next.resolutions;
93
+ else next.resolutions = list;
94
+ },
95
+ dryRun,
96
+ });
97
+ if (isRcfError(result)) return result;
98
+ return {
99
+ removed: true,
100
+ resolvedByAdrId,
101
+ resolutionId: typeof target?.id === 'string' ? target.id : undefined,
102
+ topic: typeof target?.topic === 'string' ? target.topic : undefined,
103
+ };
104
+ }