@intentius/chant 0.124.44 → 0.124.46

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 (249) hide show
  1. package/dist/capabilities/aws.paths +148 -0
  2. package/dist/capabilities/base-mappings.json +122 -0
  3. package/dist/capabilities/catalog.d.ts +89 -0
  4. package/dist/capabilities/catalog.d.ts.map +1 -0
  5. package/dist/capabilities/catalog.js +204 -0
  6. package/dist/capabilities/language.d.ts +103 -0
  7. package/dist/capabilities/language.d.ts.map +1 -0
  8. package/dist/capabilities/language.js +335 -0
  9. package/dist/capabilities/matcher.d.ts +65 -0
  10. package/dist/capabilities/matcher.d.ts.map +1 -0
  11. package/dist/capabilities/matcher.js +299 -0
  12. package/dist/capabilities/view.d.ts +207 -0
  13. package/dist/capabilities/view.d.ts.map +1 -0
  14. package/dist/capabilities/view.js +484 -0
  15. package/dist/ci-identity.d.ts +183 -12
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +362 -53
  18. package/dist/cli/commands/lint.d.ts.map +1 -1
  19. package/dist/cli/commands/lint.js +1 -0
  20. package/dist/cli/handlers/build.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.js +17 -1
  22. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  23. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  24. package/dist/cli/handlers/declared-graph.js +87 -0
  25. package/dist/cli/handlers/graph.d.ts +25 -0
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +93 -87
  28. package/dist/cli/handlers/pr-env.d.ts +8 -2
  29. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  30. package/dist/cli/handlers/pr-env.js +127 -225
  31. package/dist/cli/handlers/pr.d.ts +3 -0
  32. package/dist/cli/handlers/pr.d.ts.map +1 -1
  33. package/dist/cli/handlers/pr.js +1 -0
  34. package/dist/cli/handlers/search-count.d.ts +89 -0
  35. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  36. package/dist/cli/handlers/search-count.js +132 -0
  37. package/dist/cli/handlers/search.d.ts +358 -6
  38. package/dist/cli/handlers/search.d.ts.map +1 -1
  39. package/dist/cli/handlers/search.js +1065 -155
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +23 -3
  42. package/dist/cli/mcp/server.d.ts.map +1 -1
  43. package/dist/cli/mcp/server.js +3 -0
  44. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  45. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  46. package/dist/cli/mcp/tools/capabilities.js +27 -0
  47. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  48. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  49. package/dist/cli/mcp/workspace-tools.js +47 -2
  50. package/dist/cli/parse-args.d.ts.map +1 -1
  51. package/dist/cli/parse-args.js +64 -17
  52. package/dist/cli/registry.d.ts +22 -0
  53. package/dist/cli/registry.d.ts.map +1 -1
  54. package/dist/components/cli-support.d.ts +2 -0
  55. package/dist/components/cli-support.d.ts.map +1 -1
  56. package/dist/components/cli-support.js +4 -2
  57. package/dist/components/fan-out-support.d.ts +3 -0
  58. package/dist/components/fan-out-support.d.ts.map +1 -1
  59. package/dist/components/fan-out-support.js +1 -1
  60. package/dist/components/pr-env-ports.d.ts +29 -0
  61. package/dist/components/pr-env-ports.d.ts.map +1 -0
  62. package/dist/components/pr-env-ports.js +77 -0
  63. package/dist/components/pr-env-run.d.ts +254 -0
  64. package/dist/components/pr-env-run.d.ts.map +1 -0
  65. package/dist/components/pr-env-run.js +376 -0
  66. package/dist/components/pr-environment.d.ts +91 -15
  67. package/dist/components/pr-environment.d.ts.map +1 -1
  68. package/dist/components/pr-environment.js +95 -9
  69. package/dist/components/pr-pipeline.d.ts +10 -1
  70. package/dist/components/pr-pipeline.d.ts.map +1 -1
  71. package/dist/config-environments.d.ts +92 -0
  72. package/dist/config-environments.d.ts.map +1 -0
  73. package/dist/config-environments.js +102 -0
  74. package/dist/config.d.ts +24 -84
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +9 -96
  77. package/dist/graph-detail.d.ts.map +1 -1
  78. package/dist/graph-detail.js +15 -3
  79. package/dist/graph-effective.d.ts +33 -2
  80. package/dist/graph-effective.d.ts.map +1 -1
  81. package/dist/graph-effective.js +215 -68
  82. package/dist/graph-ir.d.ts +23 -0
  83. package/dist/graph-ir.d.ts.map +1 -1
  84. package/dist/graph-ir.js +130 -0
  85. package/dist/graph-sub-template.d.ts +11 -0
  86. package/dist/graph-sub-template.d.ts.map +1 -0
  87. package/dist/graph-sub-template.js +19 -0
  88. package/dist/lexicon.d.ts +9 -0
  89. package/dist/lexicon.d.ts.map +1 -1
  90. package/dist/lifecycle/observe.d.ts +28 -0
  91. package/dist/lifecycle/observe.d.ts.map +1 -1
  92. package/dist/lifecycle/observe.js +23 -1
  93. package/dist/lint/rule.d.ts +6 -0
  94. package/dist/lint/rule.d.ts.map +1 -1
  95. package/dist/live-endpoint.d.ts +1 -1
  96. package/dist/live-endpoint.d.ts.map +1 -1
  97. package/dist/live-endpoint.js +1 -1
  98. package/dist/observation.d.ts +48 -1
  99. package/dist/observation.d.ts.map +1 -1
  100. package/dist/observation.js +21 -5
  101. package/dist/op/activities/activity-contracts.d.ts +4 -4
  102. package/dist/op/op-waves.d.ts +17 -0
  103. package/dist/op/op-waves.d.ts.map +1 -1
  104. package/dist/op/op-waves.js +23 -1
  105. package/dist/pr-forge.d.ts +15 -1
  106. package/dist/pr-forge.d.ts.map +1 -1
  107. package/dist/pr-forge.js +28 -16
  108. package/dist/pr-requires.d.ts +5 -1
  109. package/dist/pr-requires.d.ts.map +1 -1
  110. package/dist/wave-runtime.mjs +251 -250
  111. package/dist/workspace/capabilities-contract.d.ts +66 -0
  112. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  113. package/dist/workspace/capabilities-contract.js +107 -0
  114. package/dist/workspace/capabilities.schema.json +229 -0
  115. package/dist/workspace/ci-cloud-identity.d.ts +3 -1
  116. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  117. package/dist/workspace/ci-cloud-identity.js +16 -1
  118. package/dist/workspace/ci-run.d.ts +35 -8
  119. package/dist/workspace/ci-run.d.ts.map +1 -1
  120. package/dist/workspace/ci-run.js +95 -21
  121. package/dist/workspace/ci-secrets.d.ts.map +1 -1
  122. package/dist/workspace/ci-secrets.js +4 -3
  123. package/dist/workspace/compose-graph.d.ts +9 -0
  124. package/dist/workspace/compose-graph.d.ts.map +1 -1
  125. package/dist/workspace/compose-graph.js +3 -1
  126. package/dist/workspace/conformance/index.d.ts +1 -1
  127. package/dist/workspace/conformance/index.d.ts.map +1 -1
  128. package/dist/workspace/conformance/index.js +39 -2
  129. package/dist/workspace/declaration.d.ts +10 -0
  130. package/dist/workspace/declaration.d.ts.map +1 -1
  131. package/dist/workspace/declaration.js +17 -1
  132. package/dist/workspace/declaration.schema.json +36 -1
  133. package/dist/workspace/graph-cache.d.ts +8 -0
  134. package/dist/workspace/graph-cache.d.ts.map +1 -1
  135. package/dist/workspace/graph-cache.js +27 -3
  136. package/dist/workspace/graph-cli.d.ts +39 -2
  137. package/dist/workspace/graph-cli.d.ts.map +1 -1
  138. package/dist/workspace/graph-cli.js +66 -24
  139. package/dist/workspace/graph.schema.json +148 -0
  140. package/dist/workspace/kind-readers.d.ts +3 -0
  141. package/dist/workspace/kind-readers.d.ts.map +1 -1
  142. package/dist/workspace/kind-readers.js +7 -1
  143. package/dist/workspace/member-commands.d.ts +10 -1
  144. package/dist/workspace/member-commands.d.ts.map +1 -1
  145. package/dist/workspace/member-commands.js +17 -1
  146. package/dist/workspace/nested-graph.d.ts +2 -0
  147. package/dist/workspace/nested-graph.d.ts.map +1 -1
  148. package/dist/workspace/nested-graph.js +32 -1
  149. package/dist/workspace/reason-codes.d.ts +10 -0
  150. package/dist/workspace/reason-codes.d.ts.map +1 -1
  151. package/dist/workspace/reason-codes.js +12 -0
  152. package/dist/workspace/search-cli.d.ts +204 -0
  153. package/dist/workspace/search-cli.d.ts.map +1 -0
  154. package/dist/workspace/search-cli.js +466 -0
  155. package/dist/workspace/search-contract.d.ts +56 -0
  156. package/dist/workspace/search-contract.d.ts.map +1 -0
  157. package/dist/workspace/search-contract.js +70 -0
  158. package/dist/workspace/search.schema.json +822 -0
  159. package/package.json +1 -1
  160. package/src/__snapshots__/okf.test.ts.snap +10 -0
  161. package/src/capabilities/aws.paths +148 -0
  162. package/src/capabilities/base-mappings.json +122 -0
  163. package/src/capabilities/catalog.ts +230 -0
  164. package/src/capabilities/language.test.ts +191 -0
  165. package/src/capabilities/language.ts +373 -0
  166. package/src/capabilities/matcher.ts +315 -0
  167. package/src/capabilities/view-compose.test.ts +76 -0
  168. package/src/capabilities/view.ts +631 -0
  169. package/src/ci-identity-targets.test.ts +275 -0
  170. package/src/ci-identity.ts +394 -61
  171. package/src/cli/commands/lint.ts +1 -0
  172. package/src/cli/handlers/build.ts +15 -1
  173. package/src/cli/handlers/declared-graph.ts +99 -0
  174. package/src/cli/handlers/graph.test.ts +94 -3
  175. package/src/cli/handlers/graph.ts +123 -96
  176. package/src/cli/handlers/pr-env.ts +135 -227
  177. package/src/cli/handlers/pr.ts +4 -0
  178. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  179. package/src/cli/handlers/search-count.test.ts +191 -0
  180. package/src/cli/handlers/search-count.ts +165 -0
  181. package/src/cli/handlers/search-json.test.ts +333 -0
  182. package/src/cli/handlers/search-live.test.ts +387 -0
  183. package/src/cli/handlers/search-paths.test.ts +235 -0
  184. package/src/cli/handlers/search.ts +1312 -166
  185. package/src/cli/main.test.ts +28 -3
  186. package/src/cli/main.ts +23 -3
  187. package/src/cli/mcp/server.test.ts +3 -3
  188. package/src/cli/mcp/server.ts +3 -0
  189. package/src/cli/mcp/tools/capabilities.ts +32 -0
  190. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  191. package/src/cli/mcp/workspace-tools.ts +47 -2
  192. package/src/cli/parse-args.ts +52 -13
  193. package/src/cli/registry.ts +22 -0
  194. package/src/components/cli-support.ts +6 -1
  195. package/src/components/fan-out-support.ts +4 -1
  196. package/src/components/pr-env-ports.ts +89 -0
  197. package/src/components/pr-env-run-bundle.test.ts +54 -0
  198. package/src/components/pr-env-run.test.ts +290 -0
  199. package/src/components/pr-env-run.ts +563 -0
  200. package/src/components/pr-environment.ts +161 -13
  201. package/src/components/pr-pipeline.ts +10 -1
  202. package/src/config-environments.ts +133 -0
  203. package/src/config.ts +37 -124
  204. package/src/graph-detail.ts +15 -3
  205. package/src/graph-effective.ts +226 -75
  206. package/src/graph-ir.test.ts +163 -0
  207. package/src/graph-ir.ts +147 -0
  208. package/src/graph-sub-template.ts +22 -0
  209. package/src/lexicon.ts +10 -0
  210. package/src/lifecycle/observe.test.ts +27 -1
  211. package/src/lifecycle/observe.ts +43 -1
  212. package/src/lint/rule.ts +6 -0
  213. package/src/live-endpoint.ts +1 -1
  214. package/src/observation.test.ts +25 -0
  215. package/src/observation.ts +69 -5
  216. package/src/op/op-waves.ts +38 -1
  217. package/src/pr-forge-bundle.test.ts +1 -1
  218. package/src/pr-forge.ts +44 -17
  219. package/src/pr-requires.test.ts +43 -6
  220. package/src/pr-requires.ts +5 -1
  221. package/src/workspace/capabilities-contract.test.ts +88 -0
  222. package/src/workspace/capabilities-contract.ts +132 -0
  223. package/src/workspace/capabilities.schema.json +229 -0
  224. package/src/workspace/ci-cloud-identity.test.ts +20 -0
  225. package/src/workspace/ci-cloud-identity.ts +14 -1
  226. package/src/workspace/ci-run.test.ts +74 -1
  227. package/src/workspace/ci-run.ts +122 -29
  228. package/src/workspace/ci-secrets.test.ts +1 -1
  229. package/src/workspace/ci-secrets.ts +4 -3
  230. package/src/workspace/compose-graph.ts +10 -1
  231. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  232. package/src/workspace/conformance/index.ts +31 -2
  233. package/src/workspace/declaration.schema.json +36 -1
  234. package/src/workspace/declaration.test.ts +7 -0
  235. package/src/workspace/declaration.ts +24 -2
  236. package/src/workspace/graph-cache.ts +27 -3
  237. package/src/workspace/graph-cli.ts +96 -24
  238. package/src/workspace/graph.schema.json +148 -0
  239. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  240. package/src/workspace/kind-readers.test.ts +1 -1
  241. package/src/workspace/kind-readers.ts +7 -1
  242. package/src/workspace/member-commands.ts +22 -1
  243. package/src/workspace/nested-graph.ts +32 -1
  244. package/src/workspace/reason-codes.test.ts +7 -0
  245. package/src/workspace/reason-codes.ts +12 -0
  246. package/src/workspace/search-cli.e2e.test.ts +305 -0
  247. package/src/workspace/search-cli.ts +591 -0
  248. package/src/workspace/search-contract.ts +95 -0
  249. package/src/workspace/search.schema.json +822 -0
@@ -335,6 +335,12 @@ export interface CiPrEnvironment {
335
335
  template: string;
336
336
  /** How long the environment lives after its last apply: `72h` when absent. */
337
337
  ttl: string;
338
+ /**
339
+ * A command that tears down what no lexicon does (#4330), such as a
340
+ * Terraform root's copy: run by `pr-env down` and `pr-env sweep` after the
341
+ * ownership check (../components/pr-env-run.ts, `commandTeardownAdapter`).
342
+ */
343
+ teardown?: { name?: string; command: string[]; lexicons?: string[] };
338
344
  }
339
345
 
340
346
  /** The default `ci.prEnvironment.ttl`. */
@@ -1260,7 +1266,7 @@ function cloudIdentityOf(raw: CloudIdentityDeclaration, at: (pointer: string, ke
1260
1266
 
1261
1267
  function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocation): CiDeclaration | null {
1262
1268
  if (raw === undefined) return null;
1263
- const block = raw as { reportUrl?: string; workflow?: CiWorkflow; hooks?: unknown; identity?: { cloud?: CloudIdentityDeclaration }; branches?: Record<string, string[]>; labels?: Record<string, string[]>; locks?: CiLocks; policy?: unknown; prEnvironment?: { template: string; ttl?: string }; green?: { branch: string; window?: string; phases: Record<string, string[] | { runs: string[]; skipped?: CiSkipped; build?: CiBuild }>; require: string[]; preMerge?: string[]; signedSince?: string; source?: CiSource } };
1269
+ const block = raw as { reportUrl?: string; workflow?: CiWorkflow; hooks?: unknown; identity?: { cloud?: CloudIdentityDeclaration }; branches?: Record<string, string[]>; labels?: Record<string, string[]>; locks?: CiLocks; policy?: unknown; prEnvironment?: { template: string; ttl?: string; teardown?: { name?: string; command: string[]; lexicons?: string[] } }; green?: { branch: string; window?: string; phases: Record<string, string[] | { runs: string[]; skipped?: CiSkipped; build?: CiBuild }>; require: string[]; preMerge?: string[]; signedSince?: string; source?: CiSource } };
1264
1270
  const hooks = ciHooksOf(block.hooks, "/ci/hooks", at);
1265
1271
  const policy = ciPolicyOf(block.policy, at);
1266
1272
  const workflow = {
@@ -1271,7 +1277,23 @@ function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocatio
1271
1277
  ...(block.locks !== undefined ? { locks: block.locks } : {}),
1272
1278
  ...(policy ? { policy } : {}),
1273
1279
  ...(block.identity?.cloud !== undefined ? { identity: { cloud: cloudIdentityOf(block.identity.cloud, at) } } : {}),
1274
- ...(block.prEnvironment !== undefined ? { prEnvironment: { template: block.prEnvironment.template, ttl: block.prEnvironment.ttl ?? DEFAULT_PR_ENVIRONMENT_TTL } } : {}),
1280
+ ...(block.prEnvironment !== undefined
1281
+ ? {
1282
+ prEnvironment: {
1283
+ template: block.prEnvironment.template,
1284
+ ttl: block.prEnvironment.ttl ?? DEFAULT_PR_ENVIRONMENT_TTL,
1285
+ ...(block.prEnvironment.teardown !== undefined
1286
+ ? {
1287
+ teardown: {
1288
+ ...(block.prEnvironment.teardown.name !== undefined ? { name: block.prEnvironment.teardown.name } : {}),
1289
+ command: [...block.prEnvironment.teardown.command],
1290
+ ...(block.prEnvironment.teardown.lexicons !== undefined ? { lexicons: [...block.prEnvironment.teardown.lexicons] } : {}),
1291
+ },
1292
+ }
1293
+ : {}),
1294
+ },
1295
+ }
1296
+ : {}),
1275
1297
  };
1276
1298
  let reportUrl: string | null = null;
1277
1299
  if (block.reportUrl !== undefined) {
@@ -368,9 +368,7 @@ export function splitCached(plan: MemberPlan, args: ParsedArgs, cache: GraphCach
368
368
  // A member read through a generated reader project (#2874) is also keyed on
369
369
  // the kind's graph block and the version of the package that reads it.
370
370
  const reader = unit.reader ? readerStamp(unit.reader) : "";
371
- const abs = unit.dir === "." ? plan.workspace.root : join(plan.workspace.root, ...unit.dir.split("/"));
372
- const revision = at ? { commit: at, dir: unit.dir } : undefined;
373
- const stamp = memberStamp(abs, unit.exclude, revision);
371
+ const { abs, revision, stamp } = unitStamp(plan, unit, at);
374
372
  if (!stamp) continue;
375
373
  stamps.set(unit.id, stamp.value);
376
374
  const key = cacheKey({ member: unit.member, dir: unit.dir, stamp: stamp.value, toolchain: `${tool}\0${reader}`, argv, env });
@@ -385,6 +383,32 @@ export function splitCached(plan: MemberPlan, args: ParsedArgs, cache: GraphCach
385
383
  return { hits, stamps, pending, started };
386
384
  }
387
385
 
386
+ /** One unit's stamp, and what it was taken over: the member's directory on disk, and the commit for `--at`. */
387
+ function unitStamp(plan: MemberPlan, unit: RunUnit, at: string | null): { abs: string; revision?: { commit: string; dir: string }; stamp: Stamp | undefined } {
388
+ const abs = unit.dir === "." ? plan.workspace.root : join(plan.workspace.root, ...unit.dir.split("/"));
389
+ const revision = at ? { commit: at, dir: unit.dir } : undefined;
390
+ return { abs, ...(revision ? { revision } : {}), stamp: memberStamp(abs, unit.exclude, revision) };
391
+ }
392
+
393
+ /**
394
+ * Every member unit's stamp, by unit id, by the rule {@link splitCached} keys
395
+ * the cache on (ws-059), with no cache read. A read that is never cached, such
396
+ * as `chant workspace search` (#4374), still stamps its members this way, so a
397
+ * reader compares stamps across documents with one meaning. A unit whose stamp
398
+ * can't be taken is left out.
399
+ */
400
+ export function planStamps(plan: MemberPlan, at: string | null): Map<string, string> {
401
+ const stamps = new Map<string, string>();
402
+ for (const group of plan.groups) {
403
+ for (const unit of group.units) {
404
+ if (unit.group) continue;
405
+ const { stamp } = unitStamp(plan, unit, at);
406
+ if (stamp) stamps.set(unit.id, stamp.value);
407
+ }
408
+ }
409
+ return stamps;
410
+ }
411
+
388
412
  /** The plan without the units in `ids`; a toolchain left with no units is dropped. */
389
413
  export function withoutUnits(plan: MemberPlan, ids: ReadonlySet<string>): MemberPlan {
390
414
  if (ids.size === 0) return plan;
@@ -31,11 +31,13 @@ import { dirname, join, resolve } from "node:path";
31
31
  import { formatError } from "../cli/format";
32
32
  import type { CommandContext, ParsedArgs } from "../cli/registry";
33
33
  import { graphBox, plantability, type Plantable } from "./box-factory";
34
- import { composeWorkspaceGraph, readMemberIr, type ComposedMember, type ComposeInput, type WorkspaceGraph } from "./compose-graph";
35
- import { openGraphCache, splitCached, storeReads, withoutUnits } from "./graph-cache";
36
- import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type ErrorLocation } from "./declaration";
37
- import { describePlan, emitDocument, executePlan, memberStatus, planJson, planMembers, type MemberPlan, type Toolchain, type UnitResult } from "./member-commands";
38
- import { loadKindRegistry } from "./kinds";
34
+ import { composedId, composeWorkspaceGraph, readMemberIr, type ComposedMember, type ComposeInput, type WorkspaceGraph } from "./compose-graph";
35
+ import { isViewDocument, prefixView } from "../capabilities/view";
36
+ import { refusedCapabilityView } from "./nested-graph";
37
+ import { openGraphCache, planStamps, splitCached, storeReads, withoutUnits } from "./graph-cache";
38
+ import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type Declaration, type ErrorLocation } from "./declaration";
39
+ import { describePlan, emitDocument, executePlan, memberStatus, planJson, planMembers, type MemberArgv, type MemberPlan, type Toolchain, type UnitResult } from "./member-commands";
40
+ import { loadKindRegistry, type KindRegistry } from "./kinds";
39
41
  import { readKindLinks } from "./kind-link-readers";
40
42
  import { prepareKindReaders, type PreparedReaders } from "./kind-readers";
41
43
  import { recordLinkRows } from "./record-assets";
@@ -168,6 +170,7 @@ function compose(
168
170
  only: string[] | undefined,
169
171
  declarationMembers: { name: string; dir: string; kind: string }[],
170
172
  live = false,
173
+ capabilityView = false,
171
174
  ): { inputs: ComposeInput[]; failed: boolean } {
172
175
  // A member that ran was read live when --live was given, and says when (#2875).
173
176
  const read = (member: ComposedMember, r: UnitResult): ComposedMember => {
@@ -203,6 +206,12 @@ function compose(
203
206
  }
204
207
  const member = read(memberStatus(m.name, m.dir, m.kind, "composed", null, r.chant), r);
205
208
  member.irVersion = ir.irVersion;
209
+ // The member's own capability view (#4378), with composed ids; null when its chant printed none.
210
+ if (capabilityView) {
211
+ const view = (ir.ir as { capabilityView?: unknown }).capabilityView;
212
+ member.capabilityView = isViewDocument(view) ? prefixView(view, composedId(m.name, "")) : null;
213
+ }
214
+ delete (ir.ir as { capabilityView?: unknown }).capabilityView;
206
215
  inputs.push({ member, ir: ir.ir });
207
216
  }
208
217
  return { inputs, failed };
@@ -212,51 +221,117 @@ function compose(
212
221
  export function planGraph(query: GraphQuery): MemberPlan {
213
222
  const located = locateWorkspace(query.cwd, query.at);
214
223
  const declaration = readDeclaration(located.tree, "", { rootChant: true });
215
- const kinds = loadKindRegistry(declaration.pins, located.rootOnDisk).registry;
216
- return planMembers("graph", located.rootOnDisk, { only: query.members, reader: query.reader, tree: located.tree, kinds });
224
+ const { registry: kinds, problems } = loadKindRegistry(declaration.pins, located.rootOnDisk);
225
+ return planMembers("graph", located.rootOnDisk, { only: query.members, reader: query.reader, tree: located.tree, kinds, kindProblems: problems.map((p) => p.message) });
217
226
  }
218
227
 
219
- /** Read and compose the graph, and build the document. Never throws a {@link WorkspaceReadError}. */
220
- export async function workspaceGraph(query: GraphQuery): Promise<GraphResult> {
221
- const head: Head = { $schema: GRAPH_OUTPUT_SCHEMA_ID, contract: GRAPH_CONTRACT_VERSION, chant: readerVersion() };
228
+ /** The members of a workspace read, each read under its own toolchain: what graph and search compose (#4374). */
229
+ export interface MemberReads {
230
+ located: LocatedWorkspace;
231
+ declaration: Declaration;
232
+ kinds: KindRegistry;
233
+ /** The plan the members ran from. */
234
+ plan: MemberPlan;
235
+ /** Each member's answer, the cache's included, in no particular order. */
236
+ results: UnitResult[];
237
+ /** The members the cache answered. */
238
+ served: ReadonlySet<string>;
239
+ /** Each member's stamp, by unit id (ws-059). */
240
+ stamps: ReadonlyMap<string, string>;
241
+ /** With {@link GraphQuery.components}: each member's component graph run. */
242
+ components?: UnitResult[];
243
+ }
244
+
245
+ export interface MemberReadOptions {
246
+ /** The command line each member runs; `chant graph --format ir` and the verb's flags by default. */
247
+ argvFor?: MemberArgv;
248
+ /**
249
+ * Answer members from the per-member cache and store what they print
250
+ * (ws-059). Only a read whose output is a graph IR may be cached. When
251
+ * off, every member is still stamped by the same rule.
252
+ */
253
+ cache: boolean;
254
+ }
255
+
256
+ /**
257
+ * Plan the members a workspace read covers, run each under its own toolchain
258
+ * (ws-021) at the revision asked for, and return what they printed. One
259
+ * fan-out, shared by `chant workspace graph` and `chant workspace search`
260
+ * (#4374): `--at` exports the tree once, a member of a package's kind runs in
261
+ * its reader project, and every member is stamped by the rule of ws-059. The
262
+ * exported tree and the reader projects are removed before this returns.
263
+ * Throws a {@link WorkspaceReadError} when the declaration can't be read.
264
+ */
265
+ export async function readMembers(query: GraphQuery, options: MemberReadOptions): Promise<MemberReads> {
222
266
  let exported: string | undefined;
223
267
  let readers: PreparedReaders | undefined;
224
- if (query.args?.live && query.at !== undefined) {
225
- return { doc: { ...head, error: { code: "live-at-revision", message: LIVE_AT_REVISION, location: null } }, failed: true };
226
- }
227
268
  try {
228
269
  const located = locateWorkspace(query.cwd, query.at);
229
270
  const declaration = readDeclaration(located.tree, "", { rootChant: true });
230
271
  // Kinds are read before planning: a package kind with a graph block runs (#2874).
231
- const kinds = loadKindRegistry(declaration.pins, located.rootOnDisk).registry;
232
- let plan = planMembers("graph", located.rootOnDisk, { only: query.members, reader: query.reader, tree: located.tree, kinds });
272
+ const { registry: kinds, problems } = loadKindRegistry(declaration.pins, located.rootOnDisk);
273
+ const kindProblems = problems.map((p) => p.message);
274
+ let plan = planMembers("graph", located.rootOnDisk, { only: query.members, reader: query.reader, tree: located.tree, kinds, kindProblems });
233
275
  const args = (query.args ?? {}) as ParsedArgs;
234
276
  // Members whose last source read still stands are answered from the cache (#2876).
235
277
  // A composites read runs every member's component graph as well, so it reads everything.
236
- const cache = query.noCache || query.components ? undefined : openGraphCache(located.rootOnDisk);
278
+ const cache = !options.cache || query.noCache || query.components ? undefined : openGraphCache(located.rootOnDisk);
237
279
  const split = cache ? splitCached(plan, args, cache, located.at) : undefined;
280
+ const stamps = split?.stamps ?? (options.cache ? new Map<string, string>() : planStamps(plan, located.at));
238
281
  const served = new Set(split?.hits.map((h) => h.id));
239
282
  if (located.at !== null && withoutUnits(plan, served).groups.length > 0) {
240
283
  exported = exportRevision(located, withoutUnits(plan, served).groups.flatMap((g) => g.units.map((u) => u.dir)));
241
- plan = planMembers("graph", exported, { only: query.members, reader: query.reader, tree: workingTree(exported), kinds });
284
+ plan = planMembers("graph", exported, { only: query.members, reader: query.reader, tree: workingTree(exported), kinds, kindProblems });
242
285
  }
243
286
  // Only the members that run get a reader project; a served member needs none.
244
287
  const toRun = withoutUnits(plan, served);
245
288
  readers = prepareKindReaders(toRun, exported ?? located.rootOnDisk);
246
- const [ran, components] = await Promise.all([
247
- executePlan(toRun, args),
289
+ const [first, components] = await Promise.all([
290
+ executePlan(toRun, args, options.argvFor),
248
291
  query.components ? executePlan(plan, args, () => componentGraphArgv(args)) : Promise.resolve(undefined),
249
292
  ]);
293
+ const ran = args.capabilityView && !options.argvFor ? await withoutViewWhereRefused(toRun, args, first) : first;
250
294
  if (cache && split) storeReads(split, ran, cache);
251
295
  const results = [...(split?.hits ?? []), ...ran];
252
296
  if (query.inTree) await query.inTree(exported ?? located.rootOnDisk, declaration.members.filter((m) => !query.members?.length || query.members.includes(m.name)));
253
297
  for (const r of components ?? []) if (r.stderr.trim() && query.onStderr) query.onStderr(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`);
254
298
  for (const r of results) if (r.stderr.trim() && query.onStderr) query.onStderr(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`);
255
- const { inputs, failed } = compose(plan, results, query.members, declaration.members, !!args.live);
299
+ return { located, declaration, kinds, plan, results, served, stamps, ...(components ? { components } : {}) };
300
+ } finally {
301
+ readers?.cleanup();
302
+ if (exported) rmSync(dirname(exported), { recursive: true, force: true });
303
+ }
304
+ }
305
+
306
+ /**
307
+ * A member's chant from before #4378 refuses `--capability-view` as a flag
308
+ * it doesn't know. Read those members again without it, so the graph still
309
+ * composes them; they carry `capabilityView: null`, which a capability search
310
+ * reports as `member-chant-too-old`.
311
+ */
312
+ async function withoutViewWhereRefused(plan: MemberPlan, args: ParsedArgs, results: UnitResult[]): Promise<UnitResult[]> {
313
+ const refused = new Set(results.filter((r) => r.exitCode !== 0 && refusedCapabilityView(r.stderr)).map((r) => r.id));
314
+ if (refused.size === 0) return results;
315
+ const others = new Set(results.filter((r) => !refused.has(r.id)).map((r) => r.id));
316
+ const again = await executePlan(withoutUnits(plan, others), { ...args, capabilityView: false });
317
+ const byId = new Map(again.map((r) => [r.id, r]));
318
+ return results.map((r) => byId.get(r.id) ?? r);
319
+ }
320
+
321
+ /** Read and compose the graph, and build the document. Never throws a {@link WorkspaceReadError}. */
322
+ export async function workspaceGraph(query: GraphQuery): Promise<GraphResult> {
323
+ const head: Head = { $schema: GRAPH_OUTPUT_SCHEMA_ID, contract: GRAPH_CONTRACT_VERSION, chant: readerVersion() };
324
+ if (query.args?.live && query.at !== undefined) {
325
+ return { doc: { ...head, error: { code: "live-at-revision", message: LIVE_AT_REVISION, location: null } }, failed: true };
326
+ }
327
+ try {
328
+ const { located, declaration, kinds, plan, results, served, stamps, components } = await readMembers(query, { cache: true });
329
+ const args = (query.args ?? {}) as ParsedArgs;
330
+ const { inputs, failed } = compose(plan, results, query.members, declaration.members, !!args.live, !!args.capabilityView);
256
331
  for (const { member } of inputs) {
257
332
  if (member.status !== "composed") continue;
258
333
  member.cached = served.has(member.name);
259
- member.stamp = split?.stamps.get(member.name) ?? null;
334
+ member.stamp = stamps.get(member.name) ?? null;
260
335
  }
261
336
  // Links (#2539) resolve against the declaration that was read, the revision's for --at, and the kinds installed now.
262
337
  // A kind's link reader (#3932) reads the member's files in the tree read, as check does.
@@ -285,9 +360,6 @@ export async function workspaceGraph(query: GraphQuery): Promise<GraphResult> {
285
360
  } catch (err) {
286
361
  if (!(err instanceof WorkspaceReadError)) throw err;
287
362
  return { doc: { ...head, error: { code: err.code, message: err.message, location: err.location ?? null } }, failed: true };
288
- } finally {
289
- readers?.cleanup();
290
- if (exported) rmSync(dirname(exported), { recursive: true, force: true });
291
363
  }
292
364
  }
293
365
 
@@ -437,6 +437,17 @@
437
437
  }
438
438
  }
439
439
  ]
440
+ },
441
+ "capabilityView": {
442
+ "description": "Added in contract 1 by #4378. Only with `--capability-view`, which `chant workspace search` asks for when a query has a capability term. The lexicon-neutral view capability rules read, built by the member's own chant through the mappings and adapters of the lexicons the member loads (its `chant graph --format ir --capability-view`), with `<member>/<id>` ids. null on a composed member whose chant printed no view (one from before #4378); a capability search reports that member as `member-chant-too-old`. Absent without the flag, and on a member that was not composed.",
443
+ "oneOf": [
444
+ {
445
+ "type": "null"
446
+ },
447
+ {
448
+ "$ref": "#/$defs/capabilityView"
449
+ }
450
+ ]
440
451
  }
441
452
  },
442
453
  "if": {
@@ -1438,6 +1449,143 @@
1438
1449
  ]
1439
1450
  }
1440
1451
  }
1452
+ },
1453
+ "capabilityView": {
1454
+ "type": "object",
1455
+ "description": "One member's capability view (#4378), as `capabilities/view.ts` serializes it. Rules read labels, attributes and edges; a node's attributes are the neutral ones its lexicon's mapping names.",
1456
+ "required": [
1457
+ "version",
1458
+ "lexicons",
1459
+ "nodes",
1460
+ "edges",
1461
+ "unresolved"
1462
+ ],
1463
+ "properties": {
1464
+ "version": {
1465
+ "const": 1,
1466
+ "description": "The view's own format version. A reader treats a view of a version it doesn't know as no view."
1467
+ },
1468
+ "lexicons": {
1469
+ "type": "array",
1470
+ "items": {
1471
+ "type": "string"
1472
+ },
1473
+ "description": "The lexicons whose nodes a mapping labelled."
1474
+ },
1475
+ "nodes": {
1476
+ "type": "array",
1477
+ "items": {
1478
+ "type": "object",
1479
+ "required": [
1480
+ "id",
1481
+ "label",
1482
+ "irId",
1483
+ "lexicon",
1484
+ "attrs"
1485
+ ],
1486
+ "properties": {
1487
+ "id": {
1488
+ "type": "string",
1489
+ "description": "A node id of the composed graph, or, for a policy statement or an inline block, that id with `#<attribute path>`."
1490
+ },
1491
+ "label": {
1492
+ "type": "string",
1493
+ "description": "The neutral label (`Role`, `Bucket`, `Statement`, `IngressRule`)."
1494
+ },
1495
+ "irId": {
1496
+ "type": "string",
1497
+ "description": "The composed graph node this view node is, or sits inside."
1498
+ },
1499
+ "lexicon": {
1500
+ "type": "string"
1501
+ },
1502
+ "attrs": {
1503
+ "type": "object"
1504
+ }
1505
+ }
1506
+ }
1507
+ },
1508
+ "edges": {
1509
+ "type": "array",
1510
+ "items": {
1511
+ "type": "object",
1512
+ "required": [
1513
+ "from",
1514
+ "to",
1515
+ "name",
1516
+ "hops"
1517
+ ],
1518
+ "properties": {
1519
+ "from": {
1520
+ "type": "string"
1521
+ },
1522
+ "to": {
1523
+ "type": "string"
1524
+ },
1525
+ "name": {
1526
+ "type": "string",
1527
+ "description": "The view edge's name (`subnet`, `statement`, `resource`, `attachedTo`)."
1528
+ },
1529
+ "hops": {
1530
+ "type": "array",
1531
+ "description": "The graph references the edge stands for, in walk order from `from`, each in the reference's own direction.",
1532
+ "items": {
1533
+ "type": "object",
1534
+ "required": [
1535
+ "from",
1536
+ "to",
1537
+ "edge"
1538
+ ],
1539
+ "properties": {
1540
+ "from": {
1541
+ "type": "string"
1542
+ },
1543
+ "fromAttr": {
1544
+ "type": "string"
1545
+ },
1546
+ "to": {
1547
+ "type": "string"
1548
+ },
1549
+ "toAttr": {
1550
+ "type": "string"
1551
+ },
1552
+ "edge": {
1553
+ "type": "string"
1554
+ }
1555
+ }
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+ },
1561
+ "unresolved": {
1562
+ "type": "array",
1563
+ "description": "ARN strings naming a role, bucket or table that the member declares nothing of. `chant workspace search` resolves them by name over every member's view, so a policy in one member reaches a bucket in another.",
1564
+ "items": {
1565
+ "type": "object",
1566
+ "required": [
1567
+ "from",
1568
+ "fromAttr",
1569
+ "edge",
1570
+ "value"
1571
+ ],
1572
+ "properties": {
1573
+ "from": {
1574
+ "type": "string"
1575
+ },
1576
+ "fromAttr": {
1577
+ "type": "string"
1578
+ },
1579
+ "edge": {
1580
+ "type": "string"
1581
+ },
1582
+ "value": {
1583
+ "type": "string"
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+ }
1441
1589
  }
1442
1590
  }
1443
1591
  }
@@ -65,4 +65,32 @@ describe("terraform and choudoufu members through the terraform lexicon (#2874)"
65
65
  },
66
66
  TIMEOUT,
67
67
  );
68
+
69
+ // behold#919: the reader project sets `graph.lint: "report"`, so a member
70
+ // whose HCL fails one of the lexicon's error rules is composed, with the
71
+ // finding on its meta.lint. Before, it failed with command-failed.
72
+ test(
73
+ "a member with a lint error is composed, the finding on its meta.lint",
74
+ async () => {
75
+ const root = repo({
76
+ "chant.workspace.json": declaration([{ name: "db", dir: "estates/db", kind: "terraform" }], { pins: [{ package: LEXICON, version: VERSION }] }),
77
+ "estates/db/main.tf":
78
+ 'resource "aws_db_instance" "orders" {\n identifier = "orders"\n engine = "postgres"\n instance_class = "db.t3.micro"\n allocated_storage = 20\n storage_encrypted = false\n}\n',
79
+ ".gitignore": "node_modules\n",
80
+ });
81
+ symlinkSync(join(REPO, "node_modules"), join(root, "node_modules"), "dir");
82
+
83
+ const { doc, failed } = await workspaceGraph({ cwd: root });
84
+ const g = result(doc);
85
+ expectValid(g);
86
+ expect(g.members.map((m) => [m.name, m.status, m.reason?.code ?? null])).toEqual([["db", "composed", null]]);
87
+ expect(failed).toBe(false);
88
+ expect(g.nodes.map((n) => n.id)).toContain("db/db/aws_db_instance.orders");
89
+ const lint = (g.members[0] as { meta?: { lint?: { errors: number; findings: { ruleId: string; severity: string; entity?: string }[] } } }).meta?.lint;
90
+ expect(lint?.errors).toBeGreaterThanOrEqual(1);
91
+ // The finding names its entity, the id the member IR gives the node (before composition prefixes it).
92
+ expect(lint?.findings).toContainEqual(expect.objectContaining({ ruleId: "TF033", severity: "error", entity: "db/aws_db_instance.orders" }));
93
+ },
94
+ TIMEOUT,
95
+ );
68
96
  });
@@ -80,7 +80,7 @@ describe("workspace graph reads a package kind with a graph block (#2874)", () =
80
80
  ]);
81
81
  expect(g.nodes.map((n) => n.id)).toEqual(["net/root/Thing"]);
82
82
  const attrs = g.nodes[0].attrs as { found: boolean; pwd: string; argv: string; config: Record<string, unknown> };
83
- expect(attrs.config).toEqual({ lexicons: ["fake"], fake: { moduleRoot: root, roots: { net: { dir: `${root}/estates/net` } } } });
83
+ expect(attrs.config).toEqual({ lexicons: ["fake"], fake: { moduleRoot: root, roots: { net: { dir: `${root}/estates/net` } } }, graph: { lint: "report" } });
84
84
  expect(attrs.found).toBe(true);
85
85
  // A reader project declares no environments, so --env stays with the chant members.
86
86
  expect(attrs.argv).toBe("graph . --format ir");
@@ -13,6 +13,9 @@
13
13
  * (`substituteGraphConfig`). The terraform lexicon's config names one root
14
14
  * after the member, at the member's directory, with `moduleRoot` at the
15
15
  * workspace root, so a member that calls `../modules/x` keeps its modules.
16
+ * - It also sets `graph.lint: "report"` (behold#919), so a member whose
17
+ * source fails the lexicon's lint is still composed, with the findings on
18
+ * its `meta.lint`, instead of failing with `command-failed`.
16
19
  * - `node_modules` links to the `node_modules` directory holding the lexicon
17
20
  * package, found from the workspace root as the kind's pin was. The lexicon
18
21
  * chant loads is then the one the declaration pins.
@@ -95,7 +98,10 @@ export function prepareKindReaders(plan: MemberPlan, root: string): PreparedRead
95
98
  const project = join(scratch, unit.id);
96
99
  mkdirSync(project, { recursive: true });
97
100
  const config = substituteGraphConfig(unit.reader.config, { member: unit.member, dir: unit.abs, workspace: root });
98
- writeFileSync(join(project, "chant.config.json"), `${JSON.stringify({ lexicons: [unit.reader.lexicon], [unit.reader.lexicon]: config }, null, 2)}\n`);
101
+ // `graph.lint: "report"` (behold#919): chant did not author a reader
102
+ // member's source, so a lint error there is a finding about it, carried
103
+ // on the member's `meta.lint`, not a reason to leave it out of the graph.
104
+ writeFileSync(join(project, "chant.config.json"), `${JSON.stringify({ lexicons: [unit.reader.lexicon], [unit.reader.lexicon]: config, graph: { lint: "report" } }, null, 2)}\n`);
99
105
  symlinkSync(found.modules, join(project, "node_modules"), "dir");
100
106
  unit.abs = project;
101
107
  return true;
@@ -12,7 +12,10 @@
12
12
  * - For `graph` only, members of a pinned package's kind whose kinds file
13
13
  * says how to read them (#2874, a `graph` block): each runs `chant graph`
14
14
  * in a reader project chant writes for it (`kind-readers.ts`). Other
15
- * package kinds are `kind-not-run`, as before.
15
+ * package kinds are `kind-not-run`, as before. A kind no built-in or
16
+ * pinned package supplies, such as `terraform` when the pinned lexicon is
17
+ * missing or at another version, is `unknown-kind` and fails the read,
18
+ * with the pin's problem in the message (#4380).
16
19
  * - For `build` and `lint` only, every project an example group matches
17
20
  * (ws-051): groups are built and linted, and have no ledger, audit or place
18
21
  * in the graph. On a large repository this is most of the run, so
@@ -155,6 +158,12 @@ export interface PlanOptions {
155
158
  reader?: Toolchain;
156
159
  /** The files to plan from, such as a revision's (`--at`); the working tree under `root` by default. */
157
160
  tree?: WorkspaceTree;
161
+ /**
162
+ * Why a pinned package's kinds couldn't be loaded (`loadKindRegistry`'s
163
+ * problems), such as a pin at one version with another installed. A graph
164
+ * read names them on a member whose kind is unknown (#4380).
165
+ */
166
+ kindProblems?: readonly string[];
158
167
  }
159
168
 
160
169
  /** Work out which projects run and under which chant. Throws a {@link WorkspaceReadError} for an unreadable declaration or an unknown `--member` name. */
@@ -218,6 +227,16 @@ export function planMembers(verb: WorkspaceVerb, root: string, options: PlanOpti
218
227
  });
219
228
  continue;
220
229
  }
230
+ // A kind no built-in or pinned package supplies is unknown, not a kind
231
+ // the read leaves out (#4380): a terraform member whose pinned lexicon is
232
+ // missing or at another version said kind-not-run before, as if the graph
233
+ // never read its kind. Say why, the pin's problem included.
234
+ if (verb === "graph" && !kinds.get(entry.kind)) {
235
+ const reason = memberReason(entry, tree, kinds) ?? { code: "unknown-kind", message: `no built-in kind or pinned package supplies kind ${entry.kind}` };
236
+ const problems = reason.code === "unknown-kind" ? (options.kindProblems ?? []) : [];
237
+ unreadable.push({ name: entry.name, dir: entry.dir, kind: entry.kind, reason: problems.length ? { ...reason, message: `${reason.message}; ${problems.join("; ")}` } : reason });
238
+ continue;
239
+ }
221
240
  if (entry.kind !== "chant") {
222
241
  const why =
223
242
  entry.kind === "workspace"
@@ -308,6 +327,8 @@ export function memberArgv(verb: WorkspaceVerb, unit: RunUnit, args: ParsedArgs)
308
327
  // The detail tier too, so `workspace graph --detail 1` composes collapsed
309
328
  // composites with their rolled-up status (#4228).
310
329
  if (args.detail !== undefined) argv.push("--detail", String(args.detail));
330
+ // Each member's chant maps its own capability view (#4378).
331
+ if (args.capabilityView) argv.push("--capability-view");
311
332
  }
312
333
  return argv;
313
334
  }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { spawn } from "node:child_process";
24
24
  import type { ParsedArgs } from "../cli/registry";
25
+ import { isViewDocument, prefixView, VIEW_DOCUMENT_VERSION } from "../capabilities/view";
25
26
  import { danglingKey, SCOPED_GROUPS, type ComposedMember, type ComposedNode, type WorkspaceGraph } from "./compose-graph";
26
27
 
27
28
  export type { NestedWorkspace } from "./compose-graph";
@@ -45,6 +46,7 @@ export function nestedArgv(dir: string, args: Partial<ParsedArgs>, at: string |
45
46
  ...(args.traffic !== undefined ? ["--traffic", String(args.traffic)] : []),
46
47
  ...(args.detail !== undefined ? ["--detail", String(args.detail)] : []),
47
48
  ...(args.noCache ? ["--no-cache"] : []),
49
+ ...(args.capabilityView ? ["--capability-view"] : []),
48
50
  ];
49
51
  }
50
52
 
@@ -97,10 +99,21 @@ export async function readNested(
97
99
  contract: number,
98
100
  toolchain: Toolchain | undefined = undefined,
99
101
  ): Promise<NestedRead & { stderr: string }> {
100
- const r = await spawnRead((toolchain ?? readerToolchain()).command, nestedArgv(abs, args, at), abs);
102
+ const command = (toolchain ?? readerToolchain()).command;
103
+ let r = await spawnRead(command, nestedArgv(abs, args, at), abs);
104
+ // A nested chant from before #4378 has no --capability-view: read it
105
+ // without, and its members carry no view.
106
+ if (args.capabilityView && r.exitCode !== 0 && refusedCapabilityView(r.stderr)) {
107
+ r = await spawnRead(command, nestedArgv(abs, { ...args, capabilityView: false }, at), abs);
108
+ }
101
109
  return { ...readNestedDocument(r.stdout, r.exitCode, r.stderr, schemaId, contract), stderr: r.stderr };
102
110
  }
103
111
 
112
+ /** Whether a chant refused `--capability-view` as a flag it doesn't know (#4378): one from before it. */
113
+ export function refusedCapabilityView(stderr: string): boolean {
114
+ return /Unknown flag:?\s*--capability-view/i.test(stderr);
115
+ }
116
+
104
117
  const prefixed = (outer: string, id: string): string => `${outer}/${id}`;
105
118
 
106
119
  function prefixRefs(value: unknown, outer: string): unknown {
@@ -141,6 +154,24 @@ export function foldNested(graph: WorkspaceGraph, member: ComposedMember, inner:
141
154
  for (const e of inner.exports ?? []) graph.exports.push({ ...e, ...(e.node ? { node: prefixed(outer, e.node) } : {}), member: outer });
142
155
  for (const i of inner.imports ?? []) graph.imports.push({ ...i, node: prefixed(outer, i.node), member: outer });
143
156
  graph.groups.byMember[outer] = ids.sort();
157
+ // The capability view (#4378): each nested member's, under the outer id. A
158
+ // nested member that read without one leaves the outer member without one.
159
+ const composedInner = inner.members.filter((m) => m.status === "composed");
160
+ if (composedInner.some((m) => m.capabilityView !== undefined)) {
161
+ const views = composedInner.map((m) => m.capabilityView);
162
+ if (views.every(isViewDocument)) {
163
+ const ps = views.map((v) => prefixView(v, `${outer}/`));
164
+ member.capabilityView = {
165
+ version: VIEW_DOCUMENT_VERSION,
166
+ lexicons: [...new Set(ps.flatMap((v) => v.lexicons))].sort(),
167
+ nodes: ps.flatMap((v) => v.nodes),
168
+ edges: ps.flatMap((v) => v.edges),
169
+ unresolved: ps.flatMap((v) => v.unresolved),
170
+ };
171
+ } else {
172
+ member.capabilityView = null;
173
+ }
174
+ }
144
175
  const groups = graph.groups as unknown as Record<string, Record<string, string[]> | undefined>;
145
176
  const innerGroups = inner.groups as unknown as Record<string, Record<string, string[]> | undefined>;
146
177
  for (const key of ["byLexicon", "byComposite"] as const) {
@@ -55,6 +55,8 @@ import { ROTATION_REFUSAL_CODES } from "./trust/rotation";
55
55
  import { SIGNERS_ERROR_CODES } from "./trust/signers-cli";
56
56
  import { EVIDENCE_ERROR_CODES } from "./trust/evidence";
57
57
  import { METRICS_ERROR_CODES, METRICS_REASON_CODES } from "./delivery-metrics";
58
+ import { SEARCH_ERROR_CODES, SEARCH_MEMBER_REASON_CODES, WORKSPACE_SEARCH_ERROR_CODES } from "./search-contract";
59
+ import { CAPABILITIES_ERROR_CODES, CAPABILITY_SUPPORT_CODES } from "./capabilities-contract";
58
60
 
59
61
  const HERE = import.meta.dirname;
60
62
 
@@ -132,6 +134,11 @@ const PER_COMMAND: Record<string, readonly string[]> = {
132
134
  EVIDENCE_ERROR_CODES,
133
135
  METRICS_ERROR_CODES,
134
136
  METRICS_REASON_CODES,
137
+ SEARCH_ERROR_CODES,
138
+ WORKSPACE_SEARCH_ERROR_CODES,
139
+ SEARCH_MEMBER_REASON_CODES,
140
+ CAPABILITIES_ERROR_CODES,
141
+ CAPABILITY_SUPPORT_CODES,
135
142
  };
136
143
 
137
144
  /** Every string in an `enum` under a property named `code`, anywhere in a schema. */