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.
- package/CHANGELOG.md +60 -0
- package/blueprints/application-api-rest/README.md +5 -1
- package/blueprints/application-api-rest/blueprint.json +12 -4
- package/blueprints/application-api-rest/contributions/adrs/adr-304-application-api-rest-logging.json +3 -3
- package/blueprints/application-api-rest/contributions/requirements/application-api-rest-req-006.json +4 -4
- package/blueprints/application-api-rest/contributions/tacs/tac-306-application-api-rest-operability.json +9 -8
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2103.json +3 -3
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2108.json +27 -27
- package/blueprints/application-api-rest/docs/topics.md +6 -4
- package/blueprints/application-api-rest/guide/application-api-rest.md +5 -1
- package/blueprints/application-error-handling/README.md +42 -0
- package/blueprints/application-error-handling/assets/schemas/error-record.schema.json +17 -0
- package/blueprints/application-error-handling/blueprint.json +27 -0
- package/blueprints/application-error-handling/contributions/adrs/adr-1701-application-error-handling-record-shape.json +25 -0
- package/blueprints/application-error-handling/contributions/adrs/adr-1702-application-error-handling-classification-vocabulary.json +20 -0
- package/blueprints/application-error-handling/contributions/adrs/adr-1703-application-error-handling-transport-mapping.json +20 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-001.json +15 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-002.json +15 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-003.json +15 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-004.json +15 -0
- package/blueprints/application-error-handling/contributions/tacs/tac-1701-application-error-handling-boundary.json +45 -0
- package/blueprints/application-error-handling/contributions/tacs/tac-1702-application-error-handling-record-factory.json +40 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16101.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16102.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16103.json +34 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16104.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16105.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16106.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16107.json +25 -0
- package/blueprints/application-error-handling/docs/topics.md +24 -0
- package/blueprints/application-error-handling/guide/application-error-handling.md +35 -0
- package/blueprints/application-spa/blueprint.json +12 -2
- package/blueprints/application-spa/docs/topics.md +4 -2
- package/blueprints/delivery-ci-workflows/docs/topics.md +2 -2
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +2 -2
- package/blueprints/email-smtp-resend/docs/topics.md +2 -2
- package/blueprints/observability-essentials/README.md +6 -2
- package/blueprints/observability-essentials/blueprint.json +133 -33
- package/blueprints/observability-essentials/contributions/adrs/adr-801-observability-essentials-health-probes.json +4 -4
- package/blueprints/observability-essentials/contributions/adrs/adr-802-observability-essentials-readiness-semantics.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-001.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-002.json +4 -4
- package/blueprints/observability-essentials/contributions/tacs/tac-801-observability-essentials-liveness-probe.json +10 -9
- package/blueprints/observability-essentials/contributions/tacs/tac-802-observability-essentials-readiness-probe.json +17 -11
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7101.json +12 -3
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7102.json +12 -3
- package/blueprints/observability-essentials/docs/topics.md +15 -8
- package/blueprints/observability-essentials/guide/observability-essentials.md +9 -3
- package/blueprints/observability-logging/README.md +44 -0
- package/blueprints/observability-logging/assets/samples/log-line.json +13 -0
- package/blueprints/observability-logging/blueprint.json +27 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1601-observability-logging-line-shape.json +25 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1602-observability-logging-correlation-id-header.json +25 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1603-observability-logging-redaction-categories.json +25 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1604-observability-logging-level-vocabulary.json +20 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-001.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-002.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-003.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-004.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-005.json +15 -0
- package/blueprints/observability-logging/contributions/tacs/tac-1601-observability-logging-logger-factory.json +46 -0
- package/blueprints/observability-logging/contributions/tacs/tac-1602-observability-logging-redaction-boundary.json +27 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15101.json +34 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15102.json +43 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15103.json +34 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15104.json +25 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15105.json +34 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15106.json +25 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15107.json +25 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15108.json +25 -0
- package/blueprints/observability-logging/docs/topics.md +21 -0
- package/blueprints/observability-logging/guide/observability-logging.md +36 -0
- package/blueprints/observability-probe-endpoints/README.md +5 -1
- package/blueprints/observability-probe-endpoints/blueprint.json +116 -24
- package/blueprints/observability-probe-endpoints/contributions/adrs/adr-1503-observability-probe-endpoints-kubernetes-default.json +5 -5
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14102.json +11 -2
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14107.json +11 -2
- package/blueprints/observability-probe-endpoints/docs/topics.md +6 -6
- package/blueprints/observability-probe-endpoints/guide/observability-probe-endpoints.md +10 -0
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-sqlite/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/docs/topics.md +2 -2
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/docs/topics.md +2 -2
- package/fixtures/canary-manifest.json +6 -6
- package/guidance/harness-template.md +8 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +8 -0
- package/package.json +1 -1
- package/rcf/code-nodes/cn-074.json +19 -0
- package/rcf/code-nodes/cn-075.json +15 -0
- package/rcf/code-nodes/cn-076.json +14 -0
- package/rcf/code-nodes/cn-077.json +14 -0
- package/rcf/code-nodes/cn-078.json +15 -0
- package/rcf/code-nodes/cn-079.json +14 -0
- package/rcf/code-nodes/cn-080.json +15 -0
- package/rcf/code-nodes/cn-081.json +15 -0
- package/rcf/code-nodes/cn-082.json +15 -0
- package/rcf/code-nodes/cn-083.json +14 -0
- package/rcf/code-nodes/cn-084.json +15 -0
- package/rcf/code-nodes/cn-085.json +14 -0
- package/rcf/code-nodes/cn-086.json +16 -0
- package/rcf/code-nodes/cn-087.json +14 -0
- package/rcf/code-nodes/cn-088.json +14 -0
- package/rcf/code-nodes/cn-089.json +14 -0
- package/rcf/code-nodes/cn-090.json +14 -0
- package/rcf/code-nodes/cn-091.json +14 -0
- package/rcf/code-nodes/cn-092.json +15 -0
- package/rcf/code-nodes/cn-093.json +15 -0
- package/rcf/code-nodes/cn-094.json +14 -0
- package/rcf/code-nodes/cn-095.json +14 -0
- package/rcf/code-nodes/cn-096.json +14 -0
- package/rcf/code-nodes/cn-097.json +14 -0
- package/rcf/fbs/fbs-024.json +24 -0
- package/rcf/fbs/fbs-025.json +25 -0
- package/rcf/fbs/fbs-026.json +27 -0
- package/rcf/fbs/fbs-027.json +25 -0
- package/rcf/fbs/fbs-028.json +27 -0
- package/rcf/fbs/fbs-029.json +27 -0
- package/rcf/fbs/fbs-030.json +27 -0
- package/rcf/fbs/fbs-031.json +27 -0
- package/rcf/fbs/fbs-032.json +27 -0
- package/rcf/fbs/fbs-033.json +27 -0
- package/rcf/fbs/fbs-034.json +27 -0
- package/rcf/requirements/req-012.json +22 -0
- package/rcf/requirements/req-013.json +22 -0
- package/rcf/requirements/req-014.json +22 -0
- package/rcf/requirements/req-015.json +21 -0
- package/rcf/test-suites/ts-034.json +32 -0
- package/rcf/test-suites/ts-035.json +23 -0
- package/rcf/test-suites/ts-036.json +65 -0
- package/rcf/test-suites/ts-037.json +55 -0
- package/rcf/test-suites/ts-038.json +41 -0
- package/rcf/test-suites/ts-039.json +41 -0
- package/rcf/test-suites/ts-040.json +57 -0
- package/rcf/test-suites/ts-041.json +57 -0
- package/rcf/test-suites/ts-042.json +57 -0
- package/rcf/test-suites/ts-043.json +49 -0
- package/rcf/test-suites/ts-044.json +41 -0
- package/rcf/user-stories/us-1201.json +34 -0
- package/rcf/user-stories/us-1202.json +25 -0
- package/rcf/user-stories/us-1203.json +43 -0
- package/rcf/user-stories/us-1204.json +25 -0
- package/rcf/user-stories/us-1301.json +43 -0
- package/rcf/user-stories/us-1302.json +43 -0
- package/rcf/user-stories/us-1401.json +43 -0
- package/rcf/user-stories/us-1402.json +43 -0
- package/rcf/user-stories/us-1403.json +43 -0
- package/rcf/user-stories/us-1404.json +43 -0
- package/rcf/user-stories/us-1501.json +43 -0
- package/releases/releases.yaml +21 -1
- package/src/blueprint/apply.js +8 -0
- package/src/blueprint/companions.js +485 -0
- package/src/blueprint/index.js +17 -0
- package/src/blueprint/loader.js +243 -1
- package/src/blueprint/remove-resolution.js +104 -0
- package/src/cli/blueprint.js +350 -0
- package/src/cli/doctor.js +76 -1
- package/src/cli/validate.js +7 -0
package/src/blueprint/loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|