@topogram/cli 0.3.45 → 0.3.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.45",
3
+ "version": "0.3.47",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -172,6 +172,7 @@ export function buildBundleAdoptionPriorities(report, confidenceRank) {
172
172
  enums: bundle.enums.length,
173
173
  capabilities: bundle.capabilities.length,
174
174
  shapes: bundle.shapes.length,
175
+ components: bundle.components?.length || 0,
175
176
  screens: bundle.screens.length,
176
177
  workflows: bundle.workflows.length,
177
178
  docs: bundle.docs.length
package/src/cli.js CHANGED
@@ -137,6 +137,20 @@ const KNOWN_CLI_CONSUMER_REPOS = [
137
137
  "topogram-demo-todo",
138
138
  "topogram-hello"
139
139
  ];
140
+ const KNOWN_CLI_CONSUMER_WORKFLOWS = {
141
+ "topogram-generator-express-api": "Generator Verification",
142
+ "topogram-generator-hono-api": "Generator Verification",
143
+ "topogram-generator-postgres-db": "Generator Verification",
144
+ "topogram-generator-react-web": "Generator Verification",
145
+ "topogram-generator-sqlite-db": "Generator Verification",
146
+ "topogram-generator-sveltekit-web": "Generator Verification",
147
+ "topogram-generator-swiftui-native": "Generator Verification",
148
+ "topogram-generator-vanilla-web": "Generator Verification",
149
+ "topogram-starters": "Starter Verification",
150
+ "topogram-template-todo": "Template Verification",
151
+ "topogram-demo-todo": "Demo Verification",
152
+ "topogram-hello": "Topogram Package Verification"
153
+ };
140
154
  const PACKAGE_UPDATE_CLI_CHECK_SCRIPTS = [
141
155
  "cli:surface",
142
156
  "doctor",
@@ -178,6 +192,7 @@ function printUsage(options = {}) {
178
192
  console.log("Usage: topogram doctor [--json] [--catalog <path-or-source>]");
179
193
  console.log("Usage: topogram setup package-auth|catalog-auth");
180
194
  console.log("Usage: topogram release status [--json] [--strict]");
195
+ console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push]");
181
196
  console.log("Usage: topogram check [path] [--json]");
182
197
  console.log(" or: topogram component check [path] [--projection <id>] [--component <id>] [--json]");
183
198
  console.log(" or: topogram component behavior [path] [--projection <id>] [--component <id>] [--json]");
@@ -231,6 +246,7 @@ function printUsage(options = {}) {
231
246
  console.log(" topogram doctor");
232
247
  console.log(" topogram setup package-auth");
233
248
  console.log(" topogram release status");
249
+ console.log(" topogram release roll-consumers --latest");
234
250
  console.log(" topogram new ./my-app");
235
251
  console.log(" topogram new --list-templates");
236
252
  console.log(" topogram new ./my-app --template todo");
@@ -744,13 +760,17 @@ function printPackageHelp() {
744
760
 
745
761
  function printReleaseHelp() {
746
762
  console.log("Usage: topogram release status [--json] [--strict]");
763
+ console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push]");
747
764
  console.log("");
748
- console.log("Checks the local CLI version, latest published package version, release tag, and first-party consumer pins.");
765
+ console.log("Checks the local CLI version, latest published package version, release tag, first-party consumer pins, and strict consumer CI state.");
766
+ console.log("Rolls first-party consumers to a published CLI version, runs their checks, commits, pushes, and prints latest workflow URLs.");
749
767
  console.log("");
750
768
  console.log("Examples:");
751
769
  console.log(" topogram release status");
752
770
  console.log(" topogram release status --json");
753
771
  console.log(" topogram release status --strict");
772
+ console.log(" topogram release roll-consumers 0.3.46");
773
+ console.log(" topogram release roll-consumers --latest");
754
774
  console.log("");
755
775
  console.log("Release preparation and publishing are repo-level tasks in the Topogram source checkout:");
756
776
  console.log(" npm run release:prepare -- <version>");
@@ -2493,8 +2513,208 @@ function printPackageUpdateCli(payload) {
2493
2513
  }
2494
2514
 
2495
2515
  /**
2496
- * @param {{ cwd?: string }} [options]
2497
- * @returns {{ ok: boolean, packageName: string, localVersion: string, latestVersion: string|null, currentPublished: boolean|null, git: { tag: string, local: boolean|null, remote: boolean|null, diagnostics: Array<Record<string, any>> }, consumerPins: ReturnType<typeof summarizeConsumerPins>, consumers: Array<{ name: string, path: string, version: string|null, found: boolean, matchesLocal: boolean|null }>, diagnostics: Array<Record<string, any>>, errors: string[] }}
2516
+ * @param {string} requested
2517
+ * @param {{ cwd?: string, push?: boolean }} [options]
2518
+ * @returns {{ ok: boolean, packageName: string, requestedVersion: string, requestedLatest: boolean, pushed: boolean, consumers: Array<Record<string, any>>, diagnostics: Array<Record<string, any>>, errors: string[] }}
2519
+ */
2520
+ function buildReleaseRollConsumersPayload(requested, options = {}) {
2521
+ const cwd = options.cwd || process.cwd();
2522
+ const push = options.push !== false;
2523
+ const requestedLatest = requested === "latest" || requested === "--latest";
2524
+ const diagnostics = [];
2525
+ const version = requestedLatest
2526
+ ? resolveLatestTopogramCliVersionForPackageUpdate(cwd, diagnostics)
2527
+ : requested;
2528
+ if (!isPackageVersion(version)) {
2529
+ throw new Error("topogram release roll-consumers requires <version> or --latest.");
2530
+ }
2531
+ const consumers = [];
2532
+ for (const consumer of discoverTopogramCliVersionConsumers(cwd)) {
2533
+ const workflow = expectedConsumerWorkflowName(consumer.name);
2534
+ const item = {
2535
+ name: consumer.name,
2536
+ root: consumer.root,
2537
+ workflow,
2538
+ updated: false,
2539
+ committed: false,
2540
+ pushed: false,
2541
+ commit: null,
2542
+ update: null,
2543
+ ci: null,
2544
+ diagnostics: []
2545
+ };
2546
+ consumers.push(item);
2547
+ if (!consumer.root || !fs.existsSync(consumer.root)) {
2548
+ item.diagnostics.push({
2549
+ code: "release_consumer_repo_missing",
2550
+ severity: "error",
2551
+ message: `First-party consumer repo ${consumer.name} was not found.`,
2552
+ path: consumer.path,
2553
+ suggestedFix: `Clone ${consumer.name} beside the topogram repo, then rerun roll-consumers.`
2554
+ });
2555
+ diagnostics.push(...item.diagnostics);
2556
+ continue;
2557
+ }
2558
+ const packagePath = path.join(consumer.root, "package.json");
2559
+ if (!fs.existsSync(packagePath)) {
2560
+ item.diagnostics.push({
2561
+ code: "release_consumer_package_missing",
2562
+ severity: "error",
2563
+ message: `First-party consumer repo ${consumer.name} does not contain package.json.`,
2564
+ path: packagePath,
2565
+ suggestedFix: "Only package-backed first-party consumers can be rolled by this command."
2566
+ });
2567
+ diagnostics.push(...item.diagnostics);
2568
+ continue;
2569
+ }
2570
+ const clean = inspectGitWorktreeClean(consumer.root);
2571
+ if (clean.ok !== true) {
2572
+ item.diagnostics.push({
2573
+ code: "release_consumer_worktree_dirty",
2574
+ severity: "error",
2575
+ message: clean.error || `First-party consumer repo ${consumer.name} has uncommitted changes.`,
2576
+ path: consumer.root,
2577
+ suggestedFix: "Commit, stash, or discard unrelated consumer changes before rolling the CLI version."
2578
+ });
2579
+ diagnostics.push(...item.diagnostics);
2580
+ continue;
2581
+ }
2582
+ try {
2583
+ item.update = buildPackageUpdateCliPayload(version, { cwd: consumer.root });
2584
+ item.updated = true;
2585
+ } catch (error) {
2586
+ item.diagnostics.push({
2587
+ code: "release_consumer_update_failed",
2588
+ severity: "error",
2589
+ message: `Failed to update ${consumer.name}: ${messageFromError(error)}`,
2590
+ path: consumer.root,
2591
+ suggestedFix: "Fix the consumer update/check failure, then rerun roll-consumers."
2592
+ });
2593
+ diagnostics.push(...item.diagnostics);
2594
+ continue;
2595
+ }
2596
+ const filesToStage = ["package.json", "package-lock.json", "topogram-cli.version"]
2597
+ .filter((file) => fs.existsSync(path.join(consumer.root, file)));
2598
+ const addResult = runGit(["add", ...filesToStage], consumer.root);
2599
+ if (addResult.status !== 0) {
2600
+ item.diagnostics.push(commandDiagnostic({
2601
+ code: "release_consumer_git_add_failed",
2602
+ severity: "error",
2603
+ message: `Failed to stage ${consumer.name} CLI update.`,
2604
+ path: consumer.root,
2605
+ suggestedFix: "Inspect git output, stage the changed files manually, then commit and push.",
2606
+ result: addResult
2607
+ }));
2608
+ diagnostics.push(...item.diagnostics);
2609
+ continue;
2610
+ }
2611
+ const staged = hasStagedGitChanges(consumer.root);
2612
+ if (!staged.ok) {
2613
+ item.diagnostics.push(commandDiagnostic({
2614
+ code: "release_consumer_git_diff_failed",
2615
+ severity: "error",
2616
+ message: `Could not inspect staged changes for ${consumer.name}.`,
2617
+ path: consumer.root,
2618
+ suggestedFix: "Inspect git status manually before committing.",
2619
+ result: staged.result
2620
+ }));
2621
+ diagnostics.push(...item.diagnostics);
2622
+ continue;
2623
+ }
2624
+ if (!staged.changed) {
2625
+ item.ci = inspectConsumerCi(consumer, { strict: false });
2626
+ item.diagnostics.push(...item.ci.diagnostics);
2627
+ diagnostics.push(...item.ci.diagnostics);
2628
+ continue;
2629
+ }
2630
+ const commitResult = runGit(["commit", "-m", `Update Topogram CLI to ${version}`], consumer.root);
2631
+ if (commitResult.status !== 0) {
2632
+ item.diagnostics.push(commandDiagnostic({
2633
+ code: "release_consumer_git_commit_failed",
2634
+ severity: "error",
2635
+ message: `Failed to commit ${consumer.name} CLI update.`,
2636
+ path: consumer.root,
2637
+ suggestedFix: "Inspect git output, commit the consumer update manually, then push.",
2638
+ result: commitResult
2639
+ }));
2640
+ diagnostics.push(...item.diagnostics);
2641
+ continue;
2642
+ }
2643
+ item.committed = true;
2644
+ item.commit = currentGitHead(consumer.root);
2645
+ if (push) {
2646
+ const pushResult = runGit(["push", "origin", "main"], consumer.root);
2647
+ if (pushResult.status !== 0) {
2648
+ item.diagnostics.push(commandDiagnostic({
2649
+ code: "release_consumer_git_push_failed",
2650
+ severity: "error",
2651
+ message: `Failed to push ${consumer.name} CLI update.`,
2652
+ path: consumer.root,
2653
+ suggestedFix: "Push the consumer update manually, then confirm its verification workflow passes.",
2654
+ result: pushResult
2655
+ }));
2656
+ diagnostics.push(...item.diagnostics);
2657
+ continue;
2658
+ }
2659
+ item.pushed = true;
2660
+ }
2661
+ item.ci = inspectConsumerCi(consumer, { strict: false });
2662
+ item.diagnostics.push(...item.ci.diagnostics);
2663
+ diagnostics.push(...item.ci.diagnostics);
2664
+ }
2665
+ const errors = diagnostics
2666
+ .filter((diagnostic) => diagnostic.severity === "error")
2667
+ .map((diagnostic) => diagnostic.message);
2668
+ return {
2669
+ ok: errors.length === 0,
2670
+ packageName: CLI_PACKAGE_NAME,
2671
+ requestedVersion: version,
2672
+ requestedLatest,
2673
+ pushed: push,
2674
+ consumers,
2675
+ diagnostics,
2676
+ errors
2677
+ };
2678
+ }
2679
+
2680
+ /**
2681
+ * @param {ReturnType<typeof buildReleaseRollConsumersPayload>} payload
2682
+ * @returns {void}
2683
+ */
2684
+ function printReleaseRollConsumers(payload) {
2685
+ console.log(payload.ok ? "Topogram consumer rollout completed." : "Topogram consumer rollout found issues.");
2686
+ if (payload.requestedLatest) {
2687
+ console.log(`Resolved latest version: ${payload.requestedVersion}`);
2688
+ }
2689
+ console.log(`Package: ${payload.packageName}@${payload.requestedVersion}`);
2690
+ console.log(`Push: ${payload.pushed ? "enabled" : "disabled"}`);
2691
+ for (const consumer of payload.consumers) {
2692
+ const state = consumer.committed
2693
+ ? consumer.pushed ? "pushed" : "committed"
2694
+ : consumer.updated ? "updated" : "skipped";
2695
+ console.log(`- ${consumer.name}: ${state}`);
2696
+ if (consumer.update) {
2697
+ console.log(` Checks run: ${consumer.update.scriptsRun.join(", ") || "none"}`);
2698
+ }
2699
+ if (consumer.commit) {
2700
+ console.log(` Commit: ${consumer.commit}`);
2701
+ }
2702
+ if (consumer.ci?.run?.url) {
2703
+ const run = consumer.ci.run;
2704
+ console.log(` CI: ${run.workflowName || consumer.workflow} ${run.status || "unknown"}/${run.conclusion || "unknown"} ${run.url}`);
2705
+ } else if (consumer.workflow) {
2706
+ console.log(` CI: ${consumer.workflow} not found`);
2707
+ }
2708
+ for (const diagnostic of consumer.diagnostics || []) {
2709
+ const label = diagnostic.severity === "error" ? "Error" : diagnostic.severity === "warning" ? "Warning" : "Note";
2710
+ console.log(` ${label}: ${diagnostic.message}`);
2711
+ }
2712
+ }
2713
+ }
2714
+
2715
+ /**
2716
+ * @param {{ cwd?: string, strict?: boolean }} [options]
2717
+ * @returns {{ ok: boolean, packageName: string, localVersion: string, latestVersion: string|null, currentPublished: boolean|null, git: { tag: string, local: boolean|null, remote: boolean|null, diagnostics: Array<Record<string, any>> }, consumerPins: ReturnType<typeof summarizeConsumerPins>, consumerCi: ReturnType<typeof summarizeConsumerCi>, consumers: Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean, matchesLocal: boolean|null, workflow: string|null, ci: ReturnType<typeof inspectConsumerCi>|null }>, diagnostics: Array<Record<string, any>>, errors: string[] }}
2498
2718
  */
2499
2719
  function buildReleaseStatusPayload(options = {}) {
2500
2720
  const cwd = options.cwd || process.cwd();
@@ -2517,9 +2737,20 @@ function buildReleaseStatusPayload(options = {}) {
2517
2737
  diagnostics.push(...git.diagnostics);
2518
2738
  const consumers = discoverTopogramCliVersionConsumers(cwd).map((consumer) => ({
2519
2739
  ...consumer,
2520
- matchesLocal: consumer.version ? consumer.version === localVersion : null
2740
+ matchesLocal: consumer.version ? consumer.version === localVersion : null,
2741
+ workflow: expectedConsumerWorkflowName(consumer.name),
2742
+ ci: null
2521
2743
  }));
2744
+ if (strict) {
2745
+ for (const consumer of consumers) {
2746
+ if (consumer.matchesLocal === true) {
2747
+ consumer.ci = inspectConsumerCi(consumer, { strict: true });
2748
+ diagnostics.push(...consumer.ci.diagnostics);
2749
+ }
2750
+ }
2751
+ }
2522
2752
  const consumerPins = summarizeConsumerPins(consumers);
2753
+ const consumerCi = summarizeConsumerCi(consumers);
2523
2754
  const currentPublished = latestVersion ? latestVersion === localVersion : null;
2524
2755
  if (strict) {
2525
2756
  diagnostics.push(...releaseStatusStrictDiagnostics({
@@ -2527,7 +2758,8 @@ function buildReleaseStatusPayload(options = {}) {
2527
2758
  latestVersion,
2528
2759
  currentPublished,
2529
2760
  git,
2530
- consumerPins
2761
+ consumerPins,
2762
+ consumerCi
2531
2763
  }));
2532
2764
  }
2533
2765
  const errors = diagnostics
@@ -2542,6 +2774,7 @@ function buildReleaseStatusPayload(options = {}) {
2542
2774
  currentPublished,
2543
2775
  git,
2544
2776
  consumerPins,
2777
+ consumerCi,
2545
2778
  consumers,
2546
2779
  diagnostics,
2547
2780
  errors
@@ -2554,7 +2787,8 @@ function buildReleaseStatusPayload(options = {}) {
2554
2787
  * latestVersion: string|null,
2555
2788
  * currentPublished: boolean|null,
2556
2789
  * git: ReturnType<typeof inspectReleaseGitTag>,
2557
- * consumerPins: ReturnType<typeof summarizeConsumerPins>
2790
+ * consumerPins: ReturnType<typeof summarizeConsumerPins>,
2791
+ * consumerCi: ReturnType<typeof summarizeConsumerCi>
2558
2792
  * }} release
2559
2793
  * @returns {Array<{ code: string, severity: "error", message: string, path: string, suggestedFix: string }>}
2560
2794
  */
@@ -2598,6 +2832,15 @@ function releaseStatusStrictDiagnostics(release) {
2598
2832
  suggestedFix: "Roll first-party consumer repositories to the current CLI version before treating this release as complete."
2599
2833
  });
2600
2834
  }
2835
+ if (release.consumerCi.allCheckedAndPassing !== true) {
2836
+ diagnostics.push({
2837
+ code: "release_consumer_ci_not_current",
2838
+ severity: "error",
2839
+ message: "First-party consumer verification workflows are not all passing on the checked-out consumer commits.",
2840
+ path: "GitHub Actions",
2841
+ suggestedFix: "Wait for or fix the consumer verification workflows, then rerun `topogram release status --strict`."
2842
+ });
2843
+ }
2601
2844
  return diagnostics;
2602
2845
  }
2603
2846
 
@@ -2618,13 +2861,27 @@ function printReleaseStatus(payload) {
2618
2861
  `Consumer pins: ${payload.consumerPins.pinned}/${payload.consumerPins.known} pinned, ` +
2619
2862
  `${payload.consumerPins.matching} matching, ${payload.consumerPins.differing} differing, ${payload.consumerPins.missing} missing`
2620
2863
  );
2864
+ if (payload.strict) {
2865
+ console.log(
2866
+ `Consumer CI: ${payload.consumerCi.passing}/${payload.consumerCi.checked} passing, ` +
2867
+ `${payload.consumerCi.failing} failing, ${payload.consumerCi.unavailable} unavailable, ${payload.consumerCi.skipped} skipped`
2868
+ );
2869
+ }
2621
2870
  for (const consumer of payload.consumers) {
2622
2871
  const status = consumer.matchesLocal === true
2623
2872
  ? "matches"
2624
2873
  : consumer.matchesLocal === false
2625
2874
  ? "differs"
2626
2875
  : "missing";
2627
- console.log(`- ${consumer.name}: ${consumer.version || "missing"} (${status})`);
2876
+ const ciStatus = consumer.ci?.run
2877
+ ? `; ${consumer.ci.run.workflowName || consumer.workflow}: ${consumer.ci.run.status || "unknown"}/${consumer.ci.run.conclusion || "unknown"}`
2878
+ : consumer.ci?.checked
2879
+ ? `; ${consumer.workflow || "workflow"} unavailable`
2880
+ : "";
2881
+ console.log(`- ${consumer.name}: ${consumer.version || "missing"} (${status})${ciStatus}`);
2882
+ if (consumer.ci?.run?.url) {
2883
+ console.log(` CI: ${consumer.ci.run.url}`);
2884
+ }
2628
2885
  }
2629
2886
  if (payload.diagnostics.length > 0) {
2630
2887
  console.log("Diagnostics:");
@@ -2697,9 +2954,236 @@ function inspectReleaseGitTag(version, cwd) {
2697
2954
  return { tag, local, remote, diagnostics };
2698
2955
  }
2699
2956
 
2957
+ /**
2958
+ * @param {string} name
2959
+ * @returns {string|null}
2960
+ */
2961
+ function expectedConsumerWorkflowName(name) {
2962
+ return KNOWN_CLI_CONSUMER_WORKFLOWS[name] || null;
2963
+ }
2964
+
2965
+ /**
2966
+ * @param {string[]} args
2967
+ * @param {string} cwd
2968
+ * @returns {ReturnType<typeof childProcess.spawnSync>}
2969
+ */
2970
+ function runGit(args, cwd) {
2971
+ return childProcess.spawnSync("git", args, {
2972
+ cwd,
2973
+ encoding: "utf8",
2974
+ env: { ...process.env, PATH: process.env.PATH || "" }
2975
+ });
2976
+ }
2977
+
2700
2978
  /**
2701
2979
  * @param {string} cwd
2702
- * @returns {Array<{ name: string, path: string, version: string|null, found: boolean }>}
2980
+ * @returns {{ ok: boolean, dirty: boolean|null, error: string|null }}
2981
+ */
2982
+ function inspectGitWorktreeClean(cwd) {
2983
+ const result = runGit(["status", "--porcelain"], cwd);
2984
+ if (result.status !== 0) {
2985
+ return {
2986
+ ok: false,
2987
+ dirty: null,
2988
+ error: `Could not inspect git status: ${commandOutput(result) || "unknown error"}`
2989
+ };
2990
+ }
2991
+ const dirty = String(result.stdout || "").trim().length > 0;
2992
+ return {
2993
+ ok: !dirty,
2994
+ dirty,
2995
+ error: dirty ? "Consumer repo has uncommitted changes." : null
2996
+ };
2997
+ }
2998
+
2999
+ /**
3000
+ * @param {string} cwd
3001
+ * @returns {{ ok: boolean, changed: boolean, result: ReturnType<typeof childProcess.spawnSync> }}
3002
+ */
3003
+ function hasStagedGitChanges(cwd) {
3004
+ const result = runGit(["diff", "--cached", "--quiet"], cwd);
3005
+ return {
3006
+ ok: result.status === 0 || result.status === 1,
3007
+ changed: result.status === 1,
3008
+ result
3009
+ };
3010
+ }
3011
+
3012
+ /**
3013
+ * @param {string} cwd
3014
+ * @returns {string|null}
3015
+ */
3016
+ function currentGitHead(cwd) {
3017
+ const result = runGit(["rev-parse", "HEAD"], cwd);
3018
+ return result.status === 0 ? String(result.stdout || "").trim() || null : null;
3019
+ }
3020
+
3021
+ /**
3022
+ * @param {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string, result: ReturnType<typeof childProcess.spawnSync> }} input
3023
+ * @returns {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string }}
3024
+ */
3025
+ function commandDiagnostic(input) {
3026
+ const output = commandOutput(input.result);
3027
+ return {
3028
+ code: input.code,
3029
+ severity: input.severity,
3030
+ message: output ? `${input.message}\n${output}` : input.message,
3031
+ path: input.path,
3032
+ suggestedFix: input.suggestedFix
3033
+ };
3034
+ }
3035
+
3036
+ /**
3037
+ * @param {ReturnType<typeof childProcess.spawnSync>} result
3038
+ * @returns {string}
3039
+ */
3040
+ function commandOutput(result) {
3041
+ return [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
3042
+ }
3043
+
3044
+ /**
3045
+ * @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
3046
+ * @param {{ strict?: boolean }} [options]
3047
+ * @returns {{ checked: boolean, ok: boolean|null, expectedWorkflow: string|null, headSha: string|null, run: { databaseId?: number, workflowName?: string, status?: string, conclusion?: string, headSha?: string, url?: string }|null, diagnostics: Array<Record<string, any>> }}
3048
+ */
3049
+ function inspectConsumerCi(consumer, options = {}) {
3050
+ const diagnostics = [];
3051
+ const expectedWorkflow = consumer.workflow || expectedConsumerWorkflowName(consumer.name);
3052
+ if (!consumer.root || !fs.existsSync(consumer.root)) {
3053
+ return {
3054
+ checked: false,
3055
+ ok: null,
3056
+ expectedWorkflow,
3057
+ headSha: null,
3058
+ run: null,
3059
+ diagnostics: []
3060
+ };
3061
+ }
3062
+ const headSha = currentGitHead(consumer.root);
3063
+ if (!headSha) {
3064
+ diagnostics.push({
3065
+ code: "release_consumer_head_unavailable",
3066
+ severity: options.strict ? "error" : "warning",
3067
+ message: `Could not inspect local HEAD for ${consumer.name}.`,
3068
+ path: consumer.root,
3069
+ suggestedFix: "Run from a checked-out consumer git repository."
3070
+ });
3071
+ }
3072
+ if (!expectedWorkflow) {
3073
+ diagnostics.push({
3074
+ code: "release_consumer_workflow_unknown",
3075
+ severity: options.strict ? "error" : "warning",
3076
+ message: `No expected verification workflow is configured for ${consumer.name}.`,
3077
+ path: consumer.name,
3078
+ suggestedFix: "Add the consumer repo to KNOWN_CLI_CONSUMER_WORKFLOWS."
3079
+ });
3080
+ return {
3081
+ checked: true,
3082
+ ok: false,
3083
+ expectedWorkflow,
3084
+ headSha,
3085
+ run: null,
3086
+ diagnostics
3087
+ };
3088
+ }
3089
+ const result = childProcess.spawnSync("gh", [
3090
+ "run",
3091
+ "list",
3092
+ "--repo",
3093
+ `attebury/${consumer.name}`,
3094
+ "--branch",
3095
+ "main",
3096
+ "--workflow",
3097
+ expectedWorkflow,
3098
+ "--limit",
3099
+ "1",
3100
+ "--json",
3101
+ "databaseId,workflowName,status,conclusion,headSha,url"
3102
+ ], {
3103
+ cwd: consumer.root,
3104
+ encoding: "utf8",
3105
+ env: { ...process.env, PATH: process.env.PATH || "" }
3106
+ });
3107
+ if (result.status !== 0) {
3108
+ diagnostics.push(commandDiagnostic({
3109
+ code: "release_consumer_ci_unavailable",
3110
+ severity: options.strict ? "error" : "warning",
3111
+ message: `Could not inspect ${expectedWorkflow} for ${consumer.name}.`,
3112
+ path: `attebury/${consumer.name}`,
3113
+ suggestedFix: "Check GitHub CLI auth/network access, then rerun release status.",
3114
+ result
3115
+ }));
3116
+ return {
3117
+ checked: true,
3118
+ ok: false,
3119
+ expectedWorkflow,
3120
+ headSha,
3121
+ run: null,
3122
+ diagnostics
3123
+ };
3124
+ }
3125
+ let runs = [];
3126
+ try {
3127
+ runs = JSON.parse(String(result.stdout || "[]"));
3128
+ } catch (error) {
3129
+ diagnostics.push({
3130
+ code: "release_consumer_ci_unreadable",
3131
+ severity: options.strict ? "error" : "warning",
3132
+ message: `Could not parse ${consumer.name} workflow status: ${messageFromError(error)}`,
3133
+ path: `attebury/${consumer.name}`,
3134
+ suggestedFix: "Rerun release status after GitHub CLI output is valid JSON."
3135
+ });
3136
+ }
3137
+ const run = Array.isArray(runs) && runs.length > 0 ? runs[0] : null;
3138
+ if (!run) {
3139
+ diagnostics.push({
3140
+ code: "release_consumer_ci_missing",
3141
+ severity: options.strict ? "error" : "warning",
3142
+ message: `${consumer.name} has no ${expectedWorkflow} run on main.`,
3143
+ path: `attebury/${consumer.name}`,
3144
+ suggestedFix: "Push the consumer repo and wait for its verification workflow."
3145
+ });
3146
+ return {
3147
+ checked: true,
3148
+ ok: false,
3149
+ expectedWorkflow,
3150
+ headSha,
3151
+ run: null,
3152
+ diagnostics
3153
+ };
3154
+ }
3155
+ if (headSha && run.headSha && run.headSha !== headSha) {
3156
+ diagnostics.push({
3157
+ code: "release_consumer_ci_head_mismatch",
3158
+ severity: options.strict ? "error" : "warning",
3159
+ message: `${consumer.name} latest ${expectedWorkflow} run is for ${run.headSha}, not checked-out HEAD ${headSha}.`,
3160
+ path: run.url || `attebury/${consumer.name}`,
3161
+ suggestedFix: "Wait for the verification workflow on the current consumer commit, then rerun release status."
3162
+ });
3163
+ }
3164
+ if (run.status !== "completed" || run.conclusion !== "success") {
3165
+ diagnostics.push({
3166
+ code: "release_consumer_ci_not_successful",
3167
+ severity: options.strict ? "error" : "warning",
3168
+ message: `${consumer.name} ${expectedWorkflow} is ${run.status || "unknown"}/${run.conclusion || "unknown"}.`,
3169
+ path: run.url || `attebury/${consumer.name}`,
3170
+ suggestedFix: "Wait for or fix the consumer verification workflow, then rerun release status."
3171
+ });
3172
+ }
3173
+ return {
3174
+ checked: true,
3175
+ ok: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length === 0 &&
3176
+ (!options.strict || (run.status === "completed" && run.conclusion === "success" && (!headSha || !run.headSha || run.headSha === headSha))),
3177
+ expectedWorkflow,
3178
+ headSha,
3179
+ run,
3180
+ diagnostics
3181
+ };
3182
+ }
3183
+
3184
+ /**
3185
+ * @param {string} cwd
3186
+ * @returns {Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean }>}
2703
3187
  */
2704
3188
  function discoverTopogramCliVersionConsumers(cwd) {
2705
3189
  const roots = [];
@@ -2718,6 +3202,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
2718
3202
  if (fs.existsSync(consumerRoot) && !fs.existsSync(versionPath)) {
2719
3203
  found = {
2720
3204
  name,
3205
+ root: consumerRoot,
2721
3206
  path: versionPath,
2722
3207
  version: null,
2723
3208
  found: false
@@ -2729,6 +3214,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
2729
3214
  }
2730
3215
  found = {
2731
3216
  name,
3217
+ root: consumerRoot,
2732
3218
  path: versionPath,
2733
3219
  version: fs.readFileSync(versionPath, "utf8").trim() || null,
2734
3220
  found: true
@@ -2737,6 +3223,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
2737
3223
  }
2738
3224
  consumers.push(found || {
2739
3225
  name,
3226
+ root: null,
2740
3227
  path: path.join(roots[0], name, "topogram-cli.version"),
2741
3228
  version: null,
2742
3229
  found: false
@@ -2766,6 +3253,30 @@ function summarizeConsumerPins(consumers) {
2766
3253
  };
2767
3254
  }
2768
3255
 
3256
+ /**
3257
+ * @param {Array<{ name: string, matchesLocal?: boolean|null, ci?: ReturnType<typeof inspectConsumerCi>|null }>} consumers
3258
+ * @returns {{ checked: number, passing: number, failing: number, unavailable: number, skipped: number, allCheckedAndPassing: boolean, passingNames: string[], failingNames: string[], unavailableNames: string[], skippedNames: string[] }}
3259
+ */
3260
+ function summarizeConsumerCi(consumers) {
3261
+ const checked = consumers.filter((consumer) => consumer.ci?.checked);
3262
+ const passingNames = checked.filter((consumer) => consumer.ci?.ok === true).map((consumer) => consumer.name);
3263
+ const failingNames = checked.filter((consumer) => consumer.ci?.ok === false && consumer.ci?.run).map((consumer) => consumer.name);
3264
+ const unavailableNames = checked.filter((consumer) => consumer.ci?.ok === false && !consumer.ci?.run).map((consumer) => consumer.name);
3265
+ const skippedNames = consumers.filter((consumer) => !consumer.ci?.checked).map((consumer) => consumer.name);
3266
+ return {
3267
+ checked: checked.length,
3268
+ passing: passingNames.length,
3269
+ failing: failingNames.length,
3270
+ unavailable: unavailableNames.length,
3271
+ skipped: skippedNames.length,
3272
+ allCheckedAndPassing: consumers.length > 0 && checked.length === consumers.length && failingNames.length === 0 && unavailableNames.length === 0,
3273
+ passingNames,
3274
+ failingNames,
3275
+ unavailableNames,
3276
+ skippedNames
3277
+ };
3278
+ }
3279
+
2769
3280
  /**
2770
3281
  * @param {string|null} source
2771
3282
  * @returns {{ ok: boolean, node: { version: string, minimum: string, ok: boolean, diagnostics: any[] }, npm: { available: boolean, version: string|null, diagnostics: any[] }, packageRegistry: { required: boolean, reason: string|null, registry: string, configuredRegistry: string|null, registryConfigured: boolean, nodeAuthTokenEnv: boolean, packageName: string, packageSpec: string|null, packageAccess: { ok: boolean, checkedVersion: string|null, diagnostics: any[] } }, lockfile: ReturnType<typeof inspectTopogramCliLockfile>, catalog: ReturnType<typeof buildCatalogDoctorPayload>, diagnostics: any[], errors: string[] }}
@@ -5565,6 +6076,32 @@ function importAdoptCommand(projectRoot, selector, write = false) {
5565
6076
  return `topogram import adopt ${selector} ${importProjectCommandPath(projectRoot)} ${write ? "--write" : "--dry-run"}`;
5566
6077
  }
5567
6078
 
6079
+ const BROWNFIELD_BROAD_ADOPT_SELECTORS = [
6080
+ {
6081
+ selector: "from-plan",
6082
+ kind: "plan",
6083
+ label: "approved or pending plan items",
6084
+ matches: (item) => item.current_state === "stage" || item.current_state === "accept"
6085
+ },
6086
+ { selector: "actors", kind: "kind", label: "actors", matches: (item) => item.kind === "actor" },
6087
+ { selector: "roles", kind: "kind", label: "roles", matches: (item) => item.kind === "role" },
6088
+ { selector: "enums", kind: "kind", label: "enums", matches: (item) => item.kind === "enum" },
6089
+ { selector: "shapes", kind: "kind", label: "shapes", matches: (item) => item.kind === "shape" },
6090
+ { selector: "entities", kind: "kind", label: "entities", matches: (item) => item.kind === "entity" },
6091
+ { selector: "capabilities", kind: "kind", label: "capabilities", matches: (item) => item.kind === "capability" },
6092
+ { selector: "components", kind: "kind", label: "components", matches: (item) => item.kind === "component" },
6093
+ { selector: "docs", kind: "track", label: "docs", matches: (item) => item.track === "docs" },
6094
+ {
6095
+ selector: "journeys",
6096
+ kind: "track",
6097
+ label: "journey docs",
6098
+ matches: (item) => item.track === "docs" && String(item.canonical_rel_path || "").startsWith("docs/journeys/")
6099
+ },
6100
+ { selector: "workflows", kind: "track", label: "workflows", matches: (item) => item.track === "workflows" || item.kind === "decision" },
6101
+ { selector: "verification", kind: "kind", label: "verification", matches: (item) => item.kind === "verification" },
6102
+ { selector: "ui", kind: "track", label: "UI reports and components", matches: (item) => item.track === "ui" }
6103
+ ];
6104
+
5568
6105
  function readImportAdoptionArtifacts(inputPath) {
5569
6106
  const projectRoot = normalizeProjectRoot(inputPath);
5570
6107
  const topogramRoot = normalizeTopogramPath(inputPath);
@@ -5589,6 +6126,27 @@ function readImportAdoptionArtifacts(inputPath) {
5589
6126
  };
5590
6127
  }
5591
6128
 
6129
+ function buildBrownfieldBroadAdoptSelectors(projectRoot, adoptionPlan) {
6130
+ const surfaces = adoptionPlan.imported_proposal_surfaces || [];
6131
+ return BROWNFIELD_BROAD_ADOPT_SELECTORS.map((definition) => {
6132
+ const items = surfaces.filter(definition.matches);
6133
+ const pendingItems = items.filter((item) => !["accept", "accepted", "applied"].includes(item.current_state));
6134
+ const appliedItems = items.filter((item) => ["accept", "accepted", "applied"].includes(item.current_state));
6135
+ const blockedItems = items.filter((item) => item.human_review_required);
6136
+ return {
6137
+ selector: definition.selector,
6138
+ kind: definition.kind,
6139
+ label: definition.label,
6140
+ itemCount: items.length,
6141
+ pendingItemCount: pendingItems.length,
6142
+ appliedItemCount: appliedItems.length,
6143
+ blockedItemCount: blockedItems.length,
6144
+ previewCommand: importAdoptCommand(projectRoot, definition.selector, false),
6145
+ writeCommand: importAdoptCommand(projectRoot, definition.selector, true)
6146
+ };
6147
+ }).filter((selector) => selector.itemCount > 0);
6148
+ }
6149
+
5592
6150
  function summarizeImportAdoption(adoptionPlan, adoptionStatus, projectRoot) {
5593
6151
  const surfaces = adoptionPlan.imported_proposal_surfaces || [];
5594
6152
  const slugs = [];
@@ -5700,6 +6258,7 @@ function printBrownfieldImportPlan(payload) {
5700
6258
  }
5701
6259
 
5702
6260
  function buildBrownfieldImportAdoptListPayload(inputPath) {
6261
+ const artifacts = readImportAdoptionArtifacts(inputPath);
5703
6262
  const plan = buildBrownfieldImportPlanPayload(inputPath);
5704
6263
  const selectors = plan.bundles.map((bundle) => ({
5705
6264
  selector: `bundle:${bundle.bundle}`,
@@ -5713,12 +6272,15 @@ function buildBrownfieldImportAdoptListPayload(inputPath) {
5713
6272
  previewCommand: importAdoptCommand(plan.projectRoot, `bundle:${bundle.bundle}`, false),
5714
6273
  writeCommand: importAdoptCommand(plan.projectRoot, `bundle:${bundle.bundle}`, true)
5715
6274
  }));
6275
+ const broadSelectors = buildBrownfieldBroadAdoptSelectors(plan.projectRoot, artifacts.adoptionPlan);
5716
6276
  return {
5717
6277
  ok: true,
5718
6278
  projectRoot: plan.projectRoot,
5719
6279
  topogramRoot: plan.topogramRoot,
5720
6280
  selectorCount: selectors.length,
5721
6281
  selectors,
6282
+ broadSelectorCount: broadSelectors.length,
6283
+ broadSelectors,
5722
6284
  nextCommand: selectors.find((selector) => !selector.complete)?.previewCommand || plan.commands.status
5723
6285
  };
5724
6286
  }
@@ -5734,6 +6296,15 @@ function printBrownfieldImportAdoptList(payload) {
5734
6296
  console.log(` Preview: ${selector.previewCommand}`);
5735
6297
  console.log(` Write: ${selector.writeCommand}`);
5736
6298
  }
6299
+ if (payload.broadSelectors.length > 0) {
6300
+ console.log("");
6301
+ console.log("Broad selectors:");
6302
+ for (const selector of payload.broadSelectors) {
6303
+ console.log(`- ${selector.selector}: ${selector.itemCount} ${selector.label}`);
6304
+ console.log(` Preview: ${selector.previewCommand}`);
6305
+ console.log(` Write: ${selector.writeCommand}`);
6306
+ }
6307
+ }
5737
6308
  console.log("");
5738
6309
  console.log(`Next: ${payload.nextCommand}`);
5739
6310
  }
@@ -7547,6 +8118,8 @@ if (args[0] === "version" || args[0] === "--version") {
7547
8118
  commandArgs = { doctor: true, inputPath: args[1] && !args[1].startsWith("-") ? args[1] : null };
7548
8119
  } else if (args[0] === "release" && args[1] === "status") {
7549
8120
  commandArgs = { releaseStatus: true, inputPath: null };
8121
+ } else if (args[0] === "release" && args[1] === "roll-consumers") {
8122
+ commandArgs = { releaseRollConsumers: true, releaseRollVersion: args[2], inputPath: null };
7550
8123
  } else if (args[0] === "new" || args[0] === "create") {
7551
8124
  commandArgs = args.includes("--list-templates")
7552
8125
  ? { templateList: true, inputPath: null }
@@ -7763,9 +8336,11 @@ if (commandArgs && Object.prototype.hasOwnProperty.call(commandArgs, "inputPath"
7763
8336
  }
7764
8337
  const emitJson = args.includes("--json");
7765
8338
  const strictReleaseStatus = args.includes("--strict");
8339
+ const shouldPushReleaseConsumers = !args.includes("--no-push");
7766
8340
  const shouldVersion = Boolean(commandArgs?.version);
7767
8341
  const shouldDoctor = Boolean(commandArgs?.doctor);
7768
8342
  const shouldReleaseStatus = Boolean(commandArgs?.releaseStatus);
8343
+ const shouldReleaseRollConsumers = Boolean(commandArgs?.releaseRollConsumers);
7769
8344
  const shouldCheck = Boolean(commandArgs?.check);
7770
8345
  const shouldComponentCheck = Boolean(commandArgs?.componentCheck);
7771
8346
  const shouldComponentBehavior = Boolean(commandArgs?.componentBehavior);
@@ -7999,6 +8574,18 @@ try {
7999
8574
  process.exit(payload.ok ? 0 : 1);
8000
8575
  }
8001
8576
 
8577
+ if (shouldReleaseRollConsumers) {
8578
+ const payload = buildReleaseRollConsumersPayload(commandArgs.releaseRollVersion, {
8579
+ push: shouldPushReleaseConsumers
8580
+ });
8581
+ if (emitJson) {
8582
+ console.log(stableStringify(payload));
8583
+ } else {
8584
+ printReleaseRollConsumers(payload);
8585
+ }
8586
+ process.exit(payload.ok ? 0 : 1);
8587
+ }
8588
+
8002
8589
  if (shouldQueryList) {
8003
8590
  const payload = buildQueryListPayload();
8004
8591
  if (emitJson) {
package/src/workflows.js CHANGED
@@ -1679,7 +1679,8 @@ function summarizeBundleParticipants(bundle) {
1679
1679
  }
1680
1680
 
1681
1681
  function summarizeBundleSurface(bundle, values, empty = "_none_") {
1682
- return values.length ? values.map((item) => `\`${item}\``).join(", ") : empty;
1682
+ const list = Array.isArray(values) ? values : [];
1683
+ return list.length ? list.map((item) => `\`${item}\``).join(", ") : empty;
1683
1684
  }
1684
1685
 
1685
1686
  function buildBundleOperatorSummary(bundle) {
@@ -1693,6 +1694,7 @@ function buildBundleOperatorSummary(bundle) {
1693
1694
  bundle.id;
1694
1695
  const participants = summarizeBundleParticipants(bundle);
1695
1696
  const capabilityIds = [...new Set((bundle.capabilities || []).map((entry) => entry.id_hint))].slice(0, 4);
1697
+ const componentIds = [...new Set((bundle.components || []).map((entry) => entry.id_hint))].slice(0, 4);
1696
1698
  const screenIds = [...new Set((bundle.screens || []).map((entry) => entry.id_hint))].slice(0, 4);
1697
1699
  const routePaths = [...new Set((bundle.uiRoutes || []).map((entry) => entry.path).filter(Boolean))].slice(0, 4);
1698
1700
  const workflowIds = [...new Set((bundle.workflows || []).map((entry) => entry.id_hint))].slice(0, 4);
@@ -1708,6 +1710,7 @@ function buildBundleOperatorSummary(bundle) {
1708
1710
  const evidenceKinds = [
1709
1711
  (bundle.entities || []).length > 0 ? "entity evidence" : null,
1710
1712
  (bundle.capabilities || []).length > 0 ? "API capability evidence" : null,
1713
+ (bundle.components || []).length > 0 ? "UI component evidence" : null,
1711
1714
  (bundle.screens || []).length > 0 || (bundle.uiRoutes || []).length > 0 ? "UI screen/route evidence" : null,
1712
1715
  (bundle.workflows || []).length > 0 ? "workflow evidence" : null,
1713
1716
  (bundle.docs || []).length > 0 ? "doc evidence" : null,
@@ -1723,6 +1726,7 @@ function buildBundleOperatorSummary(bundle) {
1723
1726
  primaryEntityId,
1724
1727
  participants,
1725
1728
  capabilityIds,
1729
+ componentIds,
1726
1730
  screenIds,
1727
1731
  routePaths,
1728
1732
  workflowIds,
@@ -2711,6 +2715,7 @@ function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
2711
2715
  `Enums: ${bundle.enums.length}`,
2712
2716
  `Capabilities: ${bundle.capabilities.length}`,
2713
2717
  `Shapes: ${bundle.shapes.length}`,
2718
+ `Components: ${bundle.components.length}`,
2714
2719
  `Screens: ${bundle.screens.length}`,
2715
2720
  `UI routes: ${bundle.uiRoutes.length}`,
2716
2721
  `UI actions: ${bundle.uiActions.length}`,
@@ -2728,6 +2733,7 @@ function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
2728
2733
  `- Primary entity: ${summary.primaryEntityId ? `\`${summary.primaryEntityId}\`` : "_none_"}`,
2729
2734
  `- Participants: ${summary.participants.label}`,
2730
2735
  `- Main capabilities: ${summarizeBundleSurface(bundle, summary.capabilityIds)}`,
2736
+ `- Main components: ${summarizeBundleSurface(bundle, summary.componentIds)}`,
2731
2737
  `- Main screens: ${summarizeBundleSurface(bundle, summary.screenIds)}`,
2732
2738
  `- Main routes: ${summarizeBundleSurface(bundle, summary.routePaths)}`,
2733
2739
  `- Main workflows: ${summarizeBundleSurface(bundle, summary.workflowIds)}`,
@@ -7581,6 +7587,7 @@ function reconcileWorkflow(inputPath, options = {}) {
7581
7587
  enums: bundle.enums.map((entry) => entry.id_hint),
7582
7588
  capabilities: bundle.capabilities.map((entry) => entry.id_hint),
7583
7589
  shapes: bundle.shapes.map((entry) => entry.id),
7590
+ components: bundle.components.map((entry) => entry.id_hint),
7584
7591
  screens: bundle.screens.map((entry) => entry.id_hint),
7585
7592
  workflows: bundle.workflows.map((entry) => entry.id_hint),
7586
7593
  docs: bundle.docs.map((entry) => entry.id),
@@ -7604,10 +7611,11 @@ function reconcileWorkflow(inputPath, options = {}) {
7604
7611
  : "## Promoted Canonical Items";
7605
7612
  files["candidates/reconcile/report.json"] = `${stableStringify(report)}\n`;
7606
7613
  const candidateModelBundlesMarkdown = report.candidate_model_bundles.length
7607
- ? report.candidate_model_bundles.map((bundle) => `- \`${bundle.slug}\` (${bundle.actors.length} actors, ${bundle.roles.length} roles, ${bundle.entities.length} entities, ${bundle.enums.length} enums, ${bundle.capabilities.length} capabilities, ${bundle.shapes.length} shapes, ${bundle.screens.length} screens, ${bundle.workflows.length} workflows, ${bundle.docs.length} docs)
7614
+ ? report.candidate_model_bundles.map((bundle) => `- \`${bundle.slug}\` (${bundle.actors.length} actors, ${bundle.roles.length} roles, ${bundle.entities.length} entities, ${bundle.enums.length} enums, ${bundle.capabilities.length} capabilities, ${bundle.shapes.length} shapes, ${bundle.components.length} components, ${bundle.screens.length} screens, ${bundle.workflows.length} workflows, ${bundle.docs.length} docs)
7608
7615
  - primary concept \`${bundle.operator_summary.primaryConcept}\`${bundle.operator_summary.primaryEntityId ? `, primary entity \`${bundle.operator_summary.primaryEntityId}\`` : ""}
7609
7616
  - participants ${bundle.operator_summary.participants.label}
7610
7617
  - main capabilities ${summarizeBundleSurface(bundle, bundle.operator_summary.capabilityIds)}
7618
+ - main components ${summarizeBundleSurface(bundle, bundle.operator_summary.componentIds)}
7611
7619
  - main routes ${summarizeBundleSurface(bundle, bundle.operator_summary.routePaths)}
7612
7620
  - candidate maintained seam mappings ${renderMaintainedSeamCandidatesInline(bundle)}
7613
7621
  - permission hints ${bundle.auth_permission_hints?.length ? bundle.auth_permission_hints.map((entry) => formatAuthPermissionHintInline(entry)).join(", ") : "_none_"}