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
@@ -0,0 +1,43 @@
1
+ {
2
+ "usId": "US-1404",
3
+ "prdId": "PRD-001",
4
+ "reqId": "REQ-014",
5
+ "version": "0.1.0",
6
+ "status": "draft",
7
+ "title": "Resolution rule prefers applied > registered library > core shelf; two libraries on one role refuse exit 3; validate refuses an unresolvable pin exit 3",
8
+ "asA": "operator resolving a companion suggestion on a project with a registered library that provides the same role as the shelf",
9
+ "iWant": "the deterministic tier ladder (applied provider wins, single library candidate wins over shelf, two library candidates refuse with the explicit three-path resolution message, shelf provider is the terminal fallback) and validate to refuse a pin that names no known provider",
10
+ "soThat": "the operator's opt-in to a library is honoured over the shelf, ambiguity refuses with the explicit remedy paths rather than picking silently, and a stale pin cannot silently drift after a library is removed",
11
+ "acceptanceCriteria": [
12
+ {
13
+ "id": "AC-1404-1",
14
+ "description": "For each role suggested by an applied service blueprint, the resolver walks the tier ladder in order: (1) any applied blueprint whose `providesRoles[]` contains the role wins immediately with origin `already applied`; (2) otherwise, any registered library blueprint (from `rcf/blueprint-libraries.json` + each library's `library.json:blueprints[]`) whose source `providesRoles[]` contains the role wins with origin `registered library '<prefix>'`; (3) otherwise, the single core-shelf blueprint whose `providesRoles[]` contains the role wins with origin `shelf fallback`; (4) otherwise the role is unresolved (origin `unresolved`, provider null). A pin in `rcf/companions.json` overrides steps (2) and (3) with origin `pinned via rcf/companions.json` when the pin names a known provider.",
15
+ "given": "three fixture projects: (a) core-only; (b) one registered library providing logging + the core provider; (c) an already-applied library-side logging provider",
16
+ "when": "the resolver runs on the same suggestedCompanions[] from application-api-rest",
17
+ "then": "(a) resolves logging to observability-logging with `shelf fallback`; (b) resolves logging to the library provider with `registered library '<prefix>'`; (c) resolves logging to the applied library provider with `already applied`",
18
+ "testable": true,
19
+ "scope": "library"
20
+ },
21
+ {
22
+ "id": "AC-1404-2",
23
+ "description": "When two or more registered libraries each carry a `providesRoles[]` containing the same role and no explicit selector or pin is set, the resolver refuses with exit 3 and prints (on `rcf define blueprint add` and on `rcf define blueprint companions <slug>`, both) a message naming the ambiguous role, both library-qualified slugs, and three resolution paths verbatim per spec section 2.4: (1) `rcf define blueprint add <service-slug> --companion <role>=<lib>:<slug>` at apply; (2) `rcf define blueprint companions set <role> <lib>:<slug>` to pin at project level; (3) `rcf define blueprint library remove <prefix>` to remove one library.",
24
+ "given": "a fixture project with two registered libraries both providing role `logging`, no pin",
25
+ "when": "the operator runs `rcf define blueprint add <path>/application-api-rest`",
26
+ "then": "the exit code is 3 and stderr carries the refusal message with both library slugs and the three resolution paths verbatim",
27
+ "testable": true,
28
+ "scope": "library"
29
+ },
30
+ {
31
+ "id": "AC-1404-3",
32
+ "description": "`rcf define validate` refuses exit 3 when `rcf/companions.json` pins a role to a slug that no applied blueprint, no registered library and no shelf blueprint provides; message shape `rcf/companions.json pins role '<role>' to '<slug>' but no such provider is applied, registered or on the shelf.`. The check runs on every validate pass alongside the existing schema and referential-integrity checks; a missing `rcf/companions.json` file is not itself a validation error.",
33
+ "given": "a fixture project whose companions.json pins `logging` to `nope:nope`",
34
+ "when": "the operator runs `rcf define validate`",
35
+ "then": "the exit code is 3 and stderr carries the unresolvable-pin refusal message with the exact quoted role and slug",
36
+ "testable": true,
37
+ "scope": "library"
38
+ }
39
+ ],
40
+ "tacIds": [],
41
+ "createdAt": "2026-09-04T12:20:00Z",
42
+ "updatedAt": "2026-09-04T12:20:00Z"
43
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "usId": "US-1501",
3
+ "prdId": "PRD-001",
4
+ "reqId": "REQ-015",
5
+ "version": "0.1.0",
6
+ "status": "draft",
7
+ "title": "Loader accepts standardsTrace[], recommendedDefault, elicited and standardsTraceClause; refuses a declared standardsTrace missing a clause reference on any ADR contribution",
8
+ "asA": "library author writing a standards-derived blueprint (e.g. wsd-logging composing on WSD-001)",
9
+ "iWant": "the loader to accept a blueprint-level standardsTrace[] and per-ADR-contribution recommendedDefault, elicited and standardsTraceClause fields, and refuse the load when standardsTrace[] is declared but any ADR contribution has no standardsTraceClause",
10
+ "soThat": "the standards-derived discipline is machine-checkable at load time and a mis-authored library blueprint fails fast, before an apply lands into the tree",
11
+ "acceptanceCriteria": [
12
+ {
13
+ "id": "AC-1501-1",
14
+ "description": "The loader accepts optional top-level `standardsTrace[]` (array of `{id: string, version: string}` objects; `id` is a non-empty string; `version` is a non-empty string), optional ADR-contribution fields `recommendedDefault: true|false`, `elicited: true|false` and `standardsTraceClause: string`. `recommendedDefault` and `elicited` are mutually independent (a SHOULD may be elicited; a MAY need not have a recommended default). The loader does NOT cross-check clause severity against kind (no refusal for a MUST clause mapped to an ADR; the discipline is prose in section 8a, not code, per amendment A2 Baz 2026-09-04T12:20:31Z).",
15
+ "given": "a blueprint fixture declaring standardsTrace with WSD-001 v2026-05, and one ADR contribution with recommendedDefault: true, elicited: false and standardsTraceClause: 'WSD-001 clause 3.1'",
16
+ "when": "the loader parses the file",
17
+ "then": "the LoadedBlueprint object carries the fields verbatim and returns no rcfError",
18
+ "testable": true,
19
+ "scope": "library"
20
+ },
21
+ {
22
+ "id": "AC-1501-2",
23
+ "description": "The loader refuses (rcfError kind `validation`) when `standardsTrace[]` is set but any ADR contribution has no `standardsTraceClause` (or has it set to an empty or whitespace-only string). Refusal message shape: `blueprint '<slug>' declares standardsTrace but ADR contribution '<id>' has no standardsTraceClause; every ADR must reference a standard clause or the sentinel 'generic enterprise practice'.`. At the CLI edge the exit code is 2.",
24
+ "given": "a fixture blueprint with standardsTrace declared and one ADR contribution missing standardsTraceClause",
25
+ "when": "the loader parses the file",
26
+ "then": "the loader returns an rcfError of kind `validation` with the exact refusal message shape",
27
+ "testable": true,
28
+ "scope": "library"
29
+ },
30
+ {
31
+ "id": "AC-1501-3",
32
+ "description": "A blueprint that DOES NOT declare `standardsTrace[]` (every core shelf blueprint today, including the two new core-companion blueprints in REQ-013) loads clean regardless of whether ADR contributions carry `standardsTraceClause` (the discipline lands only when standardsTrace[] is present). The sentinel string `\"generic enterprise practice\"` is accepted as a standardsTraceClause value; the discipline documented in section 8a of blueprint-authoring.md names it explicitly.",
33
+ "given": "the two new core companion blueprints (observability-logging, application-error-handling) with no standardsTrace declared, and a fixture that declares standardsTrace and uses 'generic enterprise practice' on every ADR",
34
+ "when": "the loader parses each",
35
+ "then": "the core blueprints load clean; the fixture with the sentinel on every ADR loads clean",
36
+ "testable": true,
37
+ "scope": "library"
38
+ }
39
+ ],
40
+ "tacIds": [],
41
+ "createdAt": "2026-09-04T12:20:00Z",
42
+ "updatedAt": "2026-09-04T12:20:00Z"
43
+ }
@@ -40,8 +40,28 @@
40
40
  # `npm install rcf-lite`.
41
41
 
42
42
  feedVersion: 1
43
- latest: "0.17.0"
43
+ latest: "0.19.0"
44
44
  releases:
45
+ - version: "0.19.0"
46
+ date: "2026-09-04"
47
+ breaking: false
48
+ headlines:
49
+ - "The rcf-lite core shelf gains two new blueprints, observability-logging and application-error-handling, that carry the widely followed rules for structured logging and internal error handling so a service can compose them alongside its own blueprint."
50
+ - "Service blueprints can now name the roles they want alongside them and rcf define blueprint suggests the most specific provider for each at apply time, preferring an applied provider first, then a project-level pin, then a registered library, then the shipped shelf; the operator still applies each companion by hand."
51
+ - "The authoring standard grows a written discipline for blueprints that derive from an outside standard, so a must-clause that binds a testable behaviour becomes a test and a choice-shaped clause becomes a recommended default. Rerun rcf init to refresh the agent-instructions block."
52
+ minAgentAction: "rerun-init"
53
+ notesUrl: "https://stravica.ai/docs/rcf/changelog/"
54
+
55
+ - version: "0.18.0"
56
+ date: "2026-09-04"
57
+ breaking: true
58
+ headlines:
59
+ - "The rcf-lite blueprint shelf now names a single owner for probe paths across the observability, API and Kubernetes surfaces, so a project that composes more than one of these blueprints no longer has to reconcile duplicate claims by hand."
60
+ - "observability-essentials v2.0.0 and application-api-rest v2.0.0 drop their shipped literal /healthz and /readyz path defaults and defer to the resolved path set from observability-probe-endpoints v1.1.0, which also adds an optional Kubernetes /startup path (off by default, on per project via probeInterface.options.kubernetes.startup.enabled)."
61
+ - "rcf doctor grows a probe-path-owner check that flags a project applying more than one blueprint on the probe-path topic; rcf define blueprint remove-resolution <adr-id> is the paired remedy verb that drops a single manifest resolutions entry. Migration: a project on essentials v1.x alone needs one probeInterface.paths line per environment on re-apply unless it also composes probe-endpoints v1.1.0 or later; a project that applied both essentials v1.x and probe-endpoints v1.0.0 with a superseding project-level ADR will see the new doctor check flag those resolutions as redundant with the remove-resolution verb as the hint."
62
+ minAgentAction: null
63
+ notesUrl: "https://stravica.ai/docs/rcf/changelog/"
64
+
45
65
  - version: "0.17.0"
46
66
  date: "2026-09-04"
47
67
  breaking: false
@@ -307,6 +307,14 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
307
307
  slug: appliedSlug,
308
308
  version: blueprint.version,
309
309
  contributions: writtenContributions,
310
+ // Companion-suggestion mechanism (spec 2.6). Bubble the source
311
+ // blueprint's `suggestedCompanions[]` up on the result so the CLI
312
+ // can render the resolved-suggestion block after a successful
313
+ // apply. Unresolved here (the CLI holds the tree + libraries to
314
+ // run the resolver); apply owns only the raw copy.
315
+ ...(Array.isArray(blueprint.suggestedCompanions) && blueprint.suggestedCompanions.length > 0
316
+ ? { suggestedCompanions: blueprint.suggestedCompanions }
317
+ : {}),
310
318
  ...(duplicateResolveTopics.length > 0 ? { warnings: [{ kind: 'duplicateResolveTopic', topics: duplicateResolveTopics }] } : {}),
311
319
  };
312
320
  }
@@ -0,0 +1,485 @@
1
+ // Companion-suggestion mechanism (core-companions spec 2026-09-04
2
+ // sections 2 and 4). Additive on top of the loader, apply, CLI and
3
+ // validate surfaces. Two data shapes it owns end to end:
4
+ //
5
+ // 1. Resolution: given a service blueprint's `suggestedCompanions[]`,
6
+ // walk the deterministic tier ladder (applied provider >
7
+ // registered library > core shelf, with rcf/companions.json
8
+ // pin overriding the last two) and return an ordered result per
9
+ // role.
10
+ //
11
+ // 2. Pin file: rcf/companions.json with { schemaVersion: 1, roles:
12
+ // { <role>: { provider, pinnedAt } } }. Written and read only
13
+ // through this module; validate consults it via `resolvePin`.
14
+ //
15
+ // The mechanism is suggestion, never compulsion (spec 2.6): no code
16
+ // path here calls applyBlueprint. Reads only, plus the pin file write
17
+ // path invoked from the CLI verb and from the apply --companion flag.
18
+
19
+ import { existsSync } from 'node:fs';
20
+ import { mkdir, readFile, readdir, writeFile, unlink } from 'node:fs/promises';
21
+ import { dirname, join } from 'node:path';
22
+
23
+ import { rcfError } from '../core/errors/index.js';
24
+ import { loadBlueprint } from './loader.js';
25
+ import { readLibraryRegistry } from './library-registry.js';
26
+ import { packagedShelfPath } from './shelf-resolver.js';
27
+
28
+ export const COMPANIONS_PATH = 'rcf/companions.json';
29
+ export const COMPANIONS_SCHEMA_VERSION = 1;
30
+
31
+ /**
32
+ * @typedef {object} CompanionsPinFile
33
+ * @property {number} schemaVersion
34
+ * @property {Object<string, { provider: string, pinnedAt: string }>} roles
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} ResolvedCompanion
39
+ * @property {string} role lower camelCase role name
40
+ * @property {string} reason verbatim from the service blueprint's suggestedCompanions[]
41
+ * @property {string|null} provider library-qualified slug ("wsd:logging"),
42
+ * bare shelf slug ("observability-logging"),
43
+ * applied slug, or null when unresolved
44
+ * @property {'appliedProvider'|'pinnedLibrary'|'pinnedShelf'|'registeredLibrary'|'shelfFallback'|'ambiguousLibraries'|'unresolved'} origin
45
+ * @property {string} [notes] human-readable annotation for the render
46
+ * @property {string[]} [ambiguousProviders] present only when origin='ambiguousLibraries'
47
+ */
48
+
49
+ /**
50
+ * Read the pin file if present. Absent file returns null (no pin
51
+ * state exists yet). Malformed file returns an rcfError.
52
+ *
53
+ * @param {string} projectRoot
54
+ * @returns {Promise<CompanionsPinFile|null|import('../core/errors/index.js').RcfError>}
55
+ */
56
+ export async function readCompanionsFile(projectRoot) {
57
+ const path = join(projectRoot, COMPANIONS_PATH);
58
+ if (!existsSync(path)) return null;
59
+ let raw;
60
+ try {
61
+ raw = await readFile(path, 'utf8');
62
+ } catch (err) {
63
+ return rcfError({ kind: 'ioFailure', message: `companions.json: read failed: ${err.message}`, filePath: path });
64
+ }
65
+ let doc;
66
+ try {
67
+ doc = JSON.parse(raw);
68
+ } catch (err) {
69
+ return rcfError({ kind: 'parseFailure', message: `companions.json: JSON parse failed: ${err.message}`, filePath: path });
70
+ }
71
+ const err = validateCompanionsFile(doc, path);
72
+ if (err) return err;
73
+ return doc;
74
+ }
75
+
76
+ /**
77
+ * Validate the pin file shape. Returns null clean or an rcfError.
78
+ */
79
+ export function validateCompanionsFile(doc, filePath) {
80
+ if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
81
+ return rcfError({ kind: 'validation', message: 'companions.json must be a JSON object', filePath });
82
+ }
83
+ if (doc.schemaVersion !== COMPANIONS_SCHEMA_VERSION) {
84
+ return rcfError({
85
+ kind: 'validation',
86
+ message: `companions.json: schemaVersion must be ${COMPANIONS_SCHEMA_VERSION} (got ${JSON.stringify(doc.schemaVersion)})`,
87
+ filePath,
88
+ });
89
+ }
90
+ if (typeof doc.roles !== 'object' || doc.roles === null || Array.isArray(doc.roles)) {
91
+ return rcfError({ kind: 'validation', message: 'companions.json: roles must be an object', filePath });
92
+ }
93
+ for (const [role, entry] of Object.entries(doc.roles)) {
94
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(role)) {
95
+ return rcfError({
96
+ kind: 'validation',
97
+ message: `companions.json: role name '${role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).`,
98
+ filePath,
99
+ });
100
+ }
101
+ if (typeof entry !== 'object' || entry === null) {
102
+ return rcfError({ kind: 'validation', message: `companions.json: roles.${role} must be an object with provider and pinnedAt`, filePath });
103
+ }
104
+ if (typeof entry.provider !== 'string' || entry.provider.length === 0) {
105
+ return rcfError({ kind: 'validation', message: `companions.json: roles.${role}.provider must be a non-empty string`, filePath });
106
+ }
107
+ if (typeof entry.pinnedAt !== 'string' || entry.pinnedAt.length === 0) {
108
+ return rcfError({ kind: 'validation', message: `companions.json: roles.${role}.pinnedAt must be a non-empty ISO-8601 string`, filePath });
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * Write a pin to the file, creating it lazily. Overwrites an existing
116
+ * pin for the same role. Returns { written, previousProvider, path }.
117
+ *
118
+ * @param {object} args
119
+ * @param {string} args.projectRoot
120
+ * @param {string} args.role
121
+ * @param {string} args.provider
122
+ * @param {Date} [args.now]
123
+ * @param {boolean} [args.dryRun]
124
+ * @returns {Promise<{ written: boolean, previousProvider: string|null, path: string } | import('../core/errors/index.js').RcfError>}
125
+ */
126
+ export async function setCompanionPin({ projectRoot, role, provider, now = new Date(), dryRun = false }) {
127
+ if (typeof role !== 'string' || !/^[a-z][a-zA-Z0-9]*$/.test(role)) {
128
+ return rcfError({ kind: 'usage', message: `companions set: role '${role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).` });
129
+ }
130
+ if (typeof provider !== 'string' || provider.length === 0) {
131
+ return rcfError({ kind: 'usage', message: `companions set: provider slug must be a non-empty string.` });
132
+ }
133
+ const existing = await readCompanionsFile(projectRoot);
134
+ if (existing && existing.kind) return existing;
135
+ const doc = existing ?? { schemaVersion: COMPANIONS_SCHEMA_VERSION, roles: {} };
136
+ const previousProvider = doc.roles?.[role]?.provider ?? null;
137
+ doc.roles[role] = { provider, pinnedAt: now.toISOString() };
138
+ const path = join(projectRoot, COMPANIONS_PATH);
139
+ if (dryRun) return { written: false, previousProvider, path };
140
+ try {
141
+ await mkdir(dirname(path), { recursive: true });
142
+ await writeFile(path, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
143
+ } catch (err) {
144
+ return rcfError({ kind: 'ioFailure', message: `companions.json: write failed: ${err.message}`, filePath: path });
145
+ }
146
+ return { written: true, previousProvider, path };
147
+ }
148
+
149
+ /**
150
+ * Remove a pin. Returns { removed, path }. Refuses when no pin exists
151
+ * for the role (usage error, exit 2). Leaves the rest of the file
152
+ * intact; removes the file when the last pin is gone.
153
+ */
154
+ export async function unsetCompanionPin({ projectRoot, role, dryRun = false }) {
155
+ if (typeof role !== 'string' || !/^[a-z][a-zA-Z0-9]*$/.test(role)) {
156
+ return rcfError({ kind: 'usage', message: `companions unset: role '${role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).` });
157
+ }
158
+ const existing = await readCompanionsFile(projectRoot);
159
+ if (existing && existing.kind) return existing;
160
+ if (!existing || !Object.prototype.hasOwnProperty.call(existing.roles ?? {}, role)) {
161
+ return rcfError({ kind: 'usage', message: `companions unset: no pin for role '${role}' on this project.` });
162
+ }
163
+ const path = join(projectRoot, COMPANIONS_PATH);
164
+ delete existing.roles[role];
165
+ if (dryRun) return { removed: true, path };
166
+ try {
167
+ if (Object.keys(existing.roles).length === 0) {
168
+ await unlink(path).catch(() => {});
169
+ } else {
170
+ await writeFile(path, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
171
+ }
172
+ } catch (err) {
173
+ return rcfError({ kind: 'ioFailure', message: `companions.json: write failed: ${err.message}`, filePath: path });
174
+ }
175
+ return { removed: true, path };
176
+ }
177
+
178
+ /**
179
+ * Enumerate the packaged shelf providers for a role. Reads every
180
+ * blueprint.json under the shelf and returns the slugs whose
181
+ * providesRoles[] contains the role.
182
+ *
183
+ * @param {string} role
184
+ * @param {string} [shelfDir]
185
+ * @returns {Promise<string[]>}
186
+ */
187
+ export async function enumerateShelfProviders(role, shelfDir = packagedShelfPath()) {
188
+ const providers = [];
189
+ let entries;
190
+ try {
191
+ entries = await readdir(shelfDir, { withFileTypes: true });
192
+ } catch {
193
+ return providers;
194
+ }
195
+ for (const e of entries) {
196
+ if (!e.isDirectory()) continue;
197
+ const bp = await loadBlueprint(join(shelfDir, e.name));
198
+ if (bp.kind) continue;
199
+ if (Array.isArray(bp.providesRoles) && bp.providesRoles.includes(role)) {
200
+ providers.push(bp.slug);
201
+ }
202
+ }
203
+ return providers.sort();
204
+ }
205
+
206
+ /**
207
+ * Enumerate the registered libraries' providers for a role. Reads the
208
+ * project's registry, then each library's blueprints[] entries, and
209
+ * returns entries { libraryPrefix, slug } where the referenced
210
+ * blueprint declares providesRoles[] containing the role.
211
+ *
212
+ * @param {object} args
213
+ * @param {string} args.projectRoot
214
+ * @returns {Promise<Array<{libraryPrefix: string, slug: string, source: string}>>}
215
+ */
216
+ export async function enumerateLibraryProviders({ projectRoot, role }) {
217
+ const registry = await readLibraryRegistry(projectRoot);
218
+ if (registry.kind) return [];
219
+ const out = [];
220
+ for (const lib of registry.libraries ?? []) {
221
+ for (const entry of lib.blueprints ?? []) {
222
+ const bpPath = join(lib.cachePath, entry.path);
223
+ const bp = await loadBlueprint(bpPath);
224
+ if (bp.kind) continue;
225
+ if (Array.isArray(bp.providesRoles) && bp.providesRoles.includes(role)) {
226
+ out.push({ libraryPrefix: lib.libraryPrefix, slug: entry.slug, source: bpPath });
227
+ }
228
+ }
229
+ }
230
+ return out;
231
+ }
232
+
233
+ /**
234
+ * Enumerate applied providers for a role from the walker tree. Reads
235
+ * each applied blueprint's SOURCE blueprint.json to consult its
236
+ * providesRoles[] (the applied manifest record does not carry it, per
237
+ * spec section 2 "not on the applied record"). A source that no longer
238
+ * resolves is skipped silently; the resolver falls through to the
239
+ * library / shelf tiers.
240
+ */
241
+ export async function enumerateAppliedProviders({ tree, role }) {
242
+ const applied = tree?.manifest?.blueprints ?? [];
243
+ const out = [];
244
+ for (const bp of applied) {
245
+ if (typeof bp.source !== 'string' || bp.source.length === 0) continue;
246
+ // Library-qualified sources (wsd:auth-oauth2) resolve through the
247
+ // library cache; skipping them here is fine because the library
248
+ // enumeration also covers them (a library-applied provider is
249
+ // catchable in the applied tier only when a manual reader passes
250
+ // an absolute path). The safest fallback: try to loadBlueprint at
251
+ // the source as an absolute path.
252
+ if (bp.source.includes(':') && !bp.source.startsWith('/')) continue;
253
+ const src = bp.source;
254
+ // eslint-disable-next-line no-await-in-loop
255
+ const b = await loadBlueprint(src);
256
+ if (b.kind) continue;
257
+ if (Array.isArray(b.providesRoles) && b.providesRoles.includes(role)) {
258
+ out.push({ slug: bp.slug, source: src });
259
+ }
260
+ }
261
+ return out;
262
+ }
263
+
264
+ /**
265
+ * Resolve one role through the deterministic tier ladder (spec 2.3).
266
+ * Returns a ResolvedCompanion.
267
+ *
268
+ * The pin (rcf/companions.json) overrides steps 2 and 3 (registered
269
+ * library over shelf); it does NOT override step 1 (an applied
270
+ * provider is already the operator's realised choice, and pinning to
271
+ * something else would be a silent contradiction).
272
+ *
273
+ * @param {object} args
274
+ * @param {string} args.projectRoot
275
+ * @param {object} args.tree walker TreeModel
276
+ * @param {{role: string, reason: string}} args.suggestion
277
+ * @param {CompanionsPinFile|null} [args.pins]
278
+ * @returns {Promise<ResolvedCompanion>}
279
+ */
280
+ export async function resolveCompanionRole({ projectRoot, tree, suggestion, pins }) {
281
+ const role = suggestion.role;
282
+ const reason = suggestion.reason;
283
+ // Tier 1: applied providers.
284
+ const applied = await enumerateAppliedProviders({ tree, role });
285
+ if (applied.length > 0) {
286
+ const primary = applied[0];
287
+ return {
288
+ role,
289
+ reason,
290
+ provider: primary.slug,
291
+ origin: 'appliedProvider',
292
+ notes: `already applied on this project (${primary.slug})`,
293
+ };
294
+ }
295
+ // Pin tier (spec 2.4): overrides library / shelf when present.
296
+ const pin = pins?.roles?.[role];
297
+ if (pin) {
298
+ return {
299
+ role,
300
+ reason,
301
+ provider: pin.provider,
302
+ origin: pin.provider.includes(':') ? 'pinnedLibrary' : 'pinnedShelf',
303
+ notes: `pinned via rcf/companions.json to ${pin.provider}`,
304
+ };
305
+ }
306
+ // Tier 2: registered libraries.
307
+ const libraries = await enumerateLibraryProviders({ projectRoot, role });
308
+ if (libraries.length === 1) {
309
+ const [lib] = libraries;
310
+ const providerLabel = `${lib.libraryPrefix}:${lib.slug}`;
311
+ // Also enumerate the shelf so the notes can name the overridden shelf provider.
312
+ const shelfProviders = await enumerateShelfProviders(role);
313
+ const shelfNote = shelfProviders.length === 1 ? ` (overrides shelf provider ${shelfProviders[0]})` : '';
314
+ return {
315
+ role,
316
+ reason,
317
+ provider: providerLabel,
318
+ origin: 'registeredLibrary',
319
+ notes: `registered library '${lib.libraryPrefix}'${shelfNote}`,
320
+ };
321
+ }
322
+ if (libraries.length > 1) {
323
+ return {
324
+ role,
325
+ reason,
326
+ provider: null,
327
+ origin: 'ambiguousLibraries',
328
+ ambiguousProviders: libraries.map((l) => `${l.libraryPrefix}:${l.slug}`).sort(),
329
+ notes: `two or more registered libraries provide role '${role}'; explicit selection required`,
330
+ };
331
+ }
332
+ // Tier 3: core shelf.
333
+ const shelfProviders = await enumerateShelfProviders(role);
334
+ if (shelfProviders.length === 1) {
335
+ return {
336
+ role,
337
+ reason,
338
+ provider: shelfProviders[0],
339
+ origin: 'shelfFallback',
340
+ notes: `shelf fallback (no registered library provides this role)`,
341
+ };
342
+ }
343
+ if (shelfProviders.length > 1) {
344
+ // Not currently expected on the core shelf (one provider per
345
+ // role) but shape the resolver to disambiguate the same way.
346
+ return {
347
+ role,
348
+ reason,
349
+ provider: null,
350
+ origin: 'ambiguousLibraries',
351
+ ambiguousProviders: shelfProviders,
352
+ notes: `two or more shelf blueprints provide role '${role}'; explicit selection required`,
353
+ };
354
+ }
355
+ return {
356
+ role,
357
+ reason,
358
+ provider: null,
359
+ origin: 'unresolved',
360
+ notes: `no provider found for role '${role}' (no applied blueprint, no registered library, no shelf blueprint declares providesRoles containing '${role}').`,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Resolve every role in a service blueprint's suggestedCompanions[].
366
+ * Preserves the order of the suggestedCompanions[] array (spec 2.6).
367
+ *
368
+ * @param {object} args
369
+ * @param {string} args.projectRoot
370
+ * @param {object} args.tree
371
+ * @param {Array<{role: string, reason: string}>} args.suggestedCompanions
372
+ * @param {CompanionsPinFile|null} [args.pins]
373
+ * @returns {Promise<ResolvedCompanion[]>}
374
+ */
375
+ export async function resolveCompanions({ projectRoot, tree, suggestedCompanions, pins }) {
376
+ const out = [];
377
+ for (const suggestion of suggestedCompanions ?? []) {
378
+ // eslint-disable-next-line no-await-in-loop
379
+ out.push(await resolveCompanionRole({ projectRoot, tree, suggestion, pins }));
380
+ }
381
+ return out;
382
+ }
383
+
384
+ /**
385
+ * Render a resolved-companion list to a text block (spec 2.5 / 2.6
386
+ * output shape). Fixed 16-column role column for legibility.
387
+ *
388
+ * @param {ResolvedCompanion[]} resolved
389
+ * @returns {string}
390
+ */
391
+ export function renderCompanionLines(resolved) {
392
+ const lines = [];
393
+ for (const r of resolved) {
394
+ const rolePad = r.role.padEnd(16, ' ');
395
+ if (r.origin === 'ambiguousLibraries') {
396
+ lines.push(` ${rolePad} -> (ambiguous) ${r.notes}`);
397
+ } else if (r.origin === 'unresolved') {
398
+ lines.push(` ${rolePad} -> (unresolved) ${r.notes}`);
399
+ } else {
400
+ lines.push(` ${rolePad} -> ${r.provider} (${r.notes})`);
401
+ }
402
+ }
403
+ return lines.join('\n');
404
+ }
405
+
406
+ /**
407
+ * Two-libraries-refusal message shape (spec 2.4). Called by the CLI
408
+ * on ambiguousLibraries origin. Returns the multi-line text.
409
+ *
410
+ * @param {object} args
411
+ * @param {string} args.role
412
+ * @param {string[]} args.providers library-qualified slugs, e.g. ["wsd:logging","acme:log-emit"]
413
+ * @param {string} [args.serviceSlug] service blueprint being applied; used in the resolution paths
414
+ * @returns {string}
415
+ */
416
+ export function renderAmbiguousLibraryRefusal({ role, providers, serviceSlug }) {
417
+ const numbered = providers.map((p, i) => ` ${i + 1}. ${p} (in registered library '${p.split(':')[0]}')`).join('\n');
418
+ const applyLine = serviceSlug
419
+ ? ` rcf define blueprint add ${serviceSlug} --companion ${role}=${providers[0]}`
420
+ : ` rcf define blueprint add <service-slug> --companion ${role}=${providers[0]}`;
421
+ return [
422
+ `Two or more registered libraries provide role '${role}':`,
423
+ numbered,
424
+ '',
425
+ 'Resolve one of these ways:',
426
+ '',
427
+ ' 1. Adopt one explicitly at apply:',
428
+ applyLine,
429
+ ' 2. Pin one for every future apply on this project:',
430
+ ` rcf define blueprint companions set ${role} ${providers[0]}`,
431
+ ' (writes rcf/companions.json; the file is a project record and rides git)',
432
+ ' 3. Remove one of the libraries if the project does not need both:',
433
+ ' rcf define blueprint library remove <prefix>',
434
+ '',
435
+ ].join('\n');
436
+ }
437
+
438
+ /**
439
+ * Validate rcf/companions.json against the resolvable-providers gate
440
+ * (spec 5): a pin that names no known provider (no applied, no
441
+ * registered library, no shelf) refuses with a validation error.
442
+ * Called from `rcf define validate`; the walker has already run and
443
+ * has the tree. Returns null clean or an array of rcfError entries.
444
+ *
445
+ * @param {object} args
446
+ * @param {string} args.projectRoot
447
+ * @param {object} args.tree
448
+ * @returns {Promise<import('../core/errors/index.js').RcfError[]>}
449
+ */
450
+ export async function validateCompanionPinsResolvable({ projectRoot, tree }) {
451
+ const file = await readCompanionsFile(projectRoot);
452
+ if (file === null) return [];
453
+ if (file.kind) return [file];
454
+ const errors = [];
455
+ for (const [role, entry] of Object.entries(file.roles ?? {})) {
456
+ const provider = entry.provider;
457
+ // Applied?
458
+ const applied = (tree?.manifest?.blueprints ?? []).find((bp) => bp.slug === provider);
459
+ if (applied) continue;
460
+ // Library-qualified?
461
+ if (provider.includes(':')) {
462
+ const [prefix, slug] = provider.split(':');
463
+ const registry = await readLibraryRegistry(projectRoot);
464
+ if (!registry.kind) {
465
+ const lib = (registry.libraries ?? []).find((l) => l.libraryPrefix === prefix);
466
+ if (lib && (lib.blueprints ?? []).some((b) => b.slug === slug)) continue;
467
+ }
468
+ errors.push(rcfError({
469
+ kind: 'validation',
470
+ message: `rcf/companions.json pins role '${role}' to '${provider}' but no such provider is applied, registered or on the shelf.`,
471
+ filePath: join(projectRoot, COMPANIONS_PATH),
472
+ }));
473
+ continue;
474
+ }
475
+ // Shelf slug?
476
+ const shelfProviders = await enumerateShelfProviders(role);
477
+ if (shelfProviders.includes(provider)) continue;
478
+ errors.push(rcfError({
479
+ kind: 'validation',
480
+ message: `rcf/companions.json pins role '${role}' to '${provider}' but no such provider is applied, registered or on the shelf.`,
481
+ filePath: join(projectRoot, COMPANIONS_PATH),
482
+ }));
483
+ }
484
+ return errors;
485
+ }
@@ -3,6 +3,7 @@
3
3
  export { applyBlueprint } from './apply.js';
4
4
  export { listBlueprints, enrichRowsWithCategories, groupRowsByCategory } from './list.js';
5
5
  export { removeBlueprint } from './remove.js';
6
+ export { removeResolution } from './remove-resolution.js';
6
7
  export { loadBlueprint } from './loader.js';
7
8
  export { detectGlobalAdrConflicts, detectCrossBlueprintClaims, renderConflictReport, conflictReportJson } from './conflicts.js';
8
9
  export { stampId, parseIdParts, namespaceStyleFor, isNamespacedFor } from './namespace.js';
@@ -12,6 +13,22 @@ export { supersedeBlueprintTopic } from './supersede.js';
12
13
  export { diffBlueprintTopic, renderDiff } from './diff.js';
13
14
  export { resolveBlueprintSource, knownShelfSlugs, packagedShelfPath } from './shelf-resolver.js';
14
15
  export { loadLibrary } from './library-loader.js';
16
+ export {
17
+ COMPANIONS_PATH,
18
+ COMPANIONS_SCHEMA_VERSION,
19
+ readCompanionsFile,
20
+ validateCompanionsFile,
21
+ setCompanionPin,
22
+ unsetCompanionPin,
23
+ enumerateShelfProviders,
24
+ enumerateLibraryProviders,
25
+ enumerateAppliedProviders,
26
+ resolveCompanionRole,
27
+ resolveCompanions,
28
+ renderCompanionLines,
29
+ renderAmbiguousLibraryRefusal,
30
+ validateCompanionPinsResolvable,
31
+ } from './companions.js';
15
32
  export {
16
33
  REGISTRY_PATH,
17
34
  REGISTRY_VERSION,