jui-tools-mcp-server 2.16.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.github/workflows/test.yml +2 -1
  2. package/README.md +16 -1
  3. package/dist/index.js +16 -1
  4. package/dist/tools/infra/infra_check.d.ts +10 -0
  5. package/dist/tools/infra/infra_check.js +43 -0
  6. package/dist/tools/infra/infra_common.d.ts +40 -0
  7. package/dist/tools/infra/infra_common.js +44 -0
  8. package/dist/tools/infra/infra_cost.d.ts +3 -0
  9. package/dist/tools/infra/infra_cost.js +181 -0
  10. package/dist/tools/infra/infra_load_validate.d.ts +8 -0
  11. package/dist/tools/infra/infra_load_validate.js +39 -0
  12. package/dist/tools/infra/infra_prices_validate.d.ts +7 -0
  13. package/dist/tools/infra/infra_prices_validate.js +34 -0
  14. package/dist/tools/infra/infra_seed.d.ts +27 -0
  15. package/dist/tools/infra/infra_seed.js +81 -0
  16. package/dist/tools/infra/infra_validate.d.ts +3 -0
  17. package/dist/tools/infra/infra_validate.js +74 -0
  18. package/package.json +1 -1
  19. package/scripts/record-infra-fixtures.sh +143 -0
  20. package/src/index.ts +17 -1
  21. package/src/tools/infra/infra_check.ts +49 -0
  22. package/src/tools/infra/infra_common.ts +69 -0
  23. package/src/tools/infra/infra_cost.ts +177 -0
  24. package/src/tools/infra/infra_load_validate.ts +46 -0
  25. package/src/tools/infra/infra_prices_validate.ts +41 -0
  26. package/src/tools/infra/infra_seed.ts +88 -0
  27. package/src/tools/infra/infra_validate.ts +79 -0
  28. package/tests/fixtures/infra/RECORDED_FROM +1 -0
  29. package/tests/fixtures/infra/answers-without-plan.json +9 -0
  30. package/tests/fixtures/infra/check-infra.json +103 -0
  31. package/tests/fixtures/infra/cost-infra.day.json +10080 -0
  32. package/tests/fixtures/infra/cost-infra.month-by-node.json +492 -0
  33. package/tests/fixtures/infra/cost-infra.month.json +398 -0
  34. package/tests/fixtures/infra/cost-infra.plan-bad.json +16 -0
  35. package/tests/fixtures/infra/cost-infra.plan-move-account.json +167 -0
  36. package/tests/fixtures/infra/cost-infra.plan-move.json +174 -0
  37. package/tests/fixtures/infra/cost-infra.plan-none.json +16 -0
  38. package/tests/fixtures/infra/cost-infra.plan-shrink.json +162 -0
  39. package/tests/fixtures/infra/cost-infra.refused.json +5 -0
  40. package/tests/fixtures/infra/seed-infra.draft.json +233 -0
  41. package/tests/fixtures/infra/seed-infra.report.json +46 -0
  42. package/tests/fixtures/infra/validate-infra-load.errors.json +40 -0
  43. package/tests/fixtures/infra/validate-infra-load.ok.json +29 -0
  44. package/tests/fixtures/infra/validate-infra-load.unreadable.json +7 -0
  45. package/tests/fixtures/infra/validate-infra-prices.ok.json +9 -0
  46. package/tests/fixtures/infra/validate-infra.errors.json +43 -0
  47. package/tests/fixtures/infra/validate-infra.ok.json +30 -0
  48. package/tests/fixtures/infra/validate-infra.plan-both.json +504 -0
  49. package/tests/fixtures/infra/validate-infra.plan-duplicate.json +539 -0
  50. package/tests/fixtures/infra/validate-infra.plan-edge-end.json +539 -0
  51. package/tests/fixtures/infra/validate-infra.plan-half.json +49 -0
  52. package/tests/fixtures/infra/validate-infra.plan-move.json +506 -0
  53. package/tests/fixtures/infra/validate-infra.plan-none.json +49 -0
  54. package/tests/fixtures/infra/validate-infra.plan-order-unknown.json +539 -0
  55. package/tests/fixtures/infra/validate-infra.plan-remove-missing.json +539 -0
  56. package/tests/fixtures/infra/validate-infra.plans.json +506 -0
  57. package/tests/fixtures/infra/validate-infra.unreadable.json +6 -0
  58. package/tests/tool_count.test.ts +1 -1
  59. package/tests/tools_infra.test.ts +540 -0
@@ -0,0 +1,41 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { failure, need, problems, runDocJson, text } from "./infra_common.js";
5
+
6
+ /**
7
+ * `jsonui-doc validate infra-prices <project> --json`: the room's own price files (<infra.dir>/prices, or
8
+ * infra.prices_dir). Exit 0: no error; 1: errors; 2: the folder cannot be read (reason).
9
+ */
10
+ export function register(server: McpServer, config: ServerConfig) {
11
+ server.tool(
12
+ "infra_prices_validate",
13
+ "Validate the server room's own price files (docs/infra/prices/*.json — only for what the bundled prices lack) with `jsonui-doc validate infra-prices --json`. Returns the files used, how many prices, and the problems by severity and code with the first 20. Needs jsonui-cli 2.3.15+.",
14
+ {
15
+ project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
16
+ },
17
+ async ({ project_dir }) => {
18
+ try {
19
+ const projectDir = config.resolveProjectDir(project_dir);
20
+ const { exitCode, data } = await runDocJson(["validate", "infra-prices", projectDir, "--json"], projectDir);
21
+ need(data, ["ok"], "validate infra-prices");
22
+ if (!data.ok) {
23
+ need(data, ["reason", "message"], "validate infra-prices");
24
+ return text({ ok: false, exit: exitCode, readable: false, reason: data.reason, message: data.message });
25
+ }
26
+ need(data, ["dir", "files", "problems", "count"], "validate infra-prices");
27
+ return text({
28
+ ok: exitCode === 0,
29
+ exit: exitCode,
30
+ readable: true,
31
+ dir: data.dir,
32
+ files: data.files,
33
+ count: data.count,
34
+ problems: problems(data.problems, "validate infra-prices"),
35
+ });
36
+ } catch (e: any) {
37
+ return failure(e);
38
+ }
39
+ }
40
+ );
41
+ }
@@ -0,0 +1,88 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
6
+ import { ServerConfig } from "../../config.js";
7
+ import { ContractError, FIRST, failure, need, text } from "./infra_common.js";
8
+ import { runCli } from "../../cli_runner.js";
9
+
10
+ /**
11
+ * `jsonui-doc seed infra <project> -o <dir>`: a draft model from what the project already says (Compose, Terraform /
12
+ * OpenTofu, Kubernetes, CloudFormation / SAM / CDK, wrangler, PaaS files, SDK imports and env names; never state,
13
+ * tfvars or .env values), and its report (what it read, what it could not place). By default the draft is written to
14
+ * a temp folder and read back — nothing in the project changes; write: true writes <infra.dir>/seed (the draft is a
15
+ * starting point, never read as the model). Exit 0: drafted; 2: refused (bad config, the draft fails its own check,
16
+ * or a draft is already there without force).
17
+ */
18
+
19
+ function infraDir(projectDir: string): string {
20
+ try {
21
+ const cfg = JSON.parse(readFileSync(join(projectDir, "jui.config.json"), "utf-8"));
22
+ if (typeof cfg?.infra?.dir === "string") return cfg.infra.dir;
23
+ } catch {
24
+ // no config, or not JSON: the CLI says so itself
25
+ }
26
+ return "docs/infra";
27
+ }
28
+
29
+ function readJson(path: string, what: string): any {
30
+ try {
31
+ return JSON.parse(readFileSync(path, "utf-8"));
32
+ } catch {
33
+ throw new ContractError(`seed infra wrote no readable ${what} at ${path}`);
34
+ }
35
+ }
36
+
37
+ export function summarize(draft: any, report: any) {
38
+ need(draft, ["nodes", "groups", "edges"], "seed infra draft");
39
+ need(report, ["inputs", "unmatched", "skipped", "skippedCount", "notes", "withheld"], "seed infra report");
40
+ return {
41
+ counts: { nodes: draft.nodes.length, groups: draft.groups.length, edges: draft.edges.length,
42
+ flows: Array.isArray(draft.flows) ? draft.flows.length : 0 },
43
+ environments: (draft.environments ?? []).map((e: any) => e.id),
44
+ nodes: draft.nodes.slice(0, 50).map((n: any) => ({
45
+ id: n.id, category: n.category, provider: n.provider, service: n.service, iac: n.iac, environments: n.environments,
46
+ })),
47
+ inputs: report.inputs,
48
+ unmatched: Object.fromEntries(Object.entries(report.unmatched).map(([provider, u]: [string, any]) =>
49
+ [provider, { count: u.count, items: (u.items ?? []).slice(0, FIRST) }])),
50
+ notDrawn: report.notDrawn ?? {},
51
+ skipped: { count: report.skippedCount, first: report.skipped.slice(0, FIRST) },
52
+ notes: report.notes.slice(0, FIRST),
53
+ withheld: report.withheld,
54
+ };
55
+ }
56
+
57
+ export function register(server: McpServer, config: ServerConfig) {
58
+ server.tool(
59
+ "infra_seed",
60
+ "Draft the server room's model from the project's IaC, config files and code with `jsonui-doc seed infra` (Compose, Terraform/OpenTofu, Kubernetes, CloudFormation/SAM/CDK, wrangler, Firebase/PaaS files, SDK imports and env var names — never Terraform state, tfvars or .env values). Returns the draft's counts and nodes (id, category, provider, service, iac), what was read, what could not be placed (unmatched, by provider) or was skipped, and notes. By default nothing in the project is written; write: true writes <infra.dir>/seed/draft.json and seed-report.json (a starting point to copy from — the model is docs/infra/*.json).",
61
+ {
62
+ project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
63
+ write: z.boolean().optional().describe("Write the draft to <infra.dir>/seed in the project (default false: read only)"),
64
+ force: z.boolean().optional().describe("With write: replace a draft already there"),
65
+ },
66
+ async ({ project_dir, write, force }) => {
67
+ const temp = write ? null : mkdtempSync(join(tmpdir(), "jui-mcp-seed-"));
68
+ try {
69
+ const projectDir = config.resolveProjectDir(project_dir);
70
+ const out = temp ?? join(projectDir, infraDir(projectDir), "seed");
71
+ const args = ["seed", "infra", projectDir, "-o", out];
72
+ if (write && force) args.push("--force");
73
+ const result = await runCli("jsonui-doc", args, { cwd: projectDir, timeout: 180_000 });
74
+ if (result.exitCode !== 0 || !existsSync(join(out, "draft.json"))) {
75
+ return text({ ok: false, exit: result.exitCode,
76
+ message: (result.stderr || result.stdout).trim().split("\n").slice(-5).join("\n") });
77
+ }
78
+ const summary = summarize(readJson(join(out, "draft.json"), "draft.json"),
79
+ readJson(join(out, "seed-report.json"), "seed-report.json"));
80
+ return text({ ok: true, exit: 0, written: write ? out : null, ...summary });
81
+ } catch (e: any) {
82
+ return failure(e);
83
+ } finally {
84
+ if (temp) rmSync(temp, { recursive: true, force: true });
85
+ }
86
+ }
87
+ );
88
+ }
@@ -0,0 +1,79 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { ContractError, FIRST, failure, need, problems, runDocJson, text } from "./infra_common.js";
5
+
6
+ /**
7
+ * `jsonui-doc validate infra <project> --json`: the room's model (docs/infra/*.json, or jui.config.json's infra.dir)
8
+ * read as jui viz reads it — each file against the schema and the shapes of secrets, then merged, and its flows
9
+ * walked. Exit 0: no error (warnings alone are 0); 1: errors; 2: nothing could be read (reason).
10
+ *
11
+ * Migration plans (jsonui-cli 2.3.20+, docs/infra/migrations/<name>/changes.json + removals.json): the CLI checks every
12
+ * plan with the model, or one with `--migration <name>`, and lists them under "migrations". A plan's error makes the
13
+ * exit 1 while the model's own problems are empty, so a room that has plans gets their summary here — without it the
14
+ * exit would name an error the answer does not show. A room with none answers exactly as before.
15
+ */
16
+
17
+ function migrationSummary(m: any) {
18
+ need(m, ["name", "dir", "ok", "problems", "steps", "counts"], "validate infra migration");
19
+ return {
20
+ name: m.name,
21
+ dir: m.dir,
22
+ ok: m.ok,
23
+ counts: m.counts,
24
+ problems: problems(m.problems, "validate infra migration"),
25
+ steps: m.steps.length,
26
+ firstSteps: m.steps.slice(0, FIRST).map((s: any) => {
27
+ need(s, ["index", "kind", "node"], "validate infra migration step");
28
+ return { index: s.index, kind: s.kind, node: s.node,
29
+ ...(s.edgesRemoved?.length ? { edgesRemoved: s.edgesRemoved } : {}),
30
+ ...(s.edgesAdded?.length ? { edgesAdded: s.edgesAdded } : {}) };
31
+ }),
32
+ };
33
+ }
34
+ function migrations(data: any, plan: string | undefined) {
35
+ if (plan) need(data, ["migrations"], "validate infra --migration");
36
+ const list = data.migrations;
37
+ if (list === undefined) return {}; // a room with no plan: the CLI prints no key, and nor does this
38
+ if (!Array.isArray(list)) throw new ContractError('validate infra: "migrations" is not a list');
39
+ return { migrations: list.map(migrationSummary) };
40
+ }
41
+
42
+ export function register(server: McpServer, config: ServerConfig) {
43
+ server.tool(
44
+ "infra_validate",
45
+ "Validate the server room's infra model (docs/infra/*.json: environments, groups, nodes, edges, flows) with `jsonui-doc validate infra --json`. Returns whether it could be read (and why not), the files used, the counts, and the problems by severity and code with the first 20 (file, path, code, message). Flow problems (flow.*) name the rule a step most likely missed. When the room has migration plans (docs/infra/migrations/<name>/changes.json + removals.json), each plan's check too: its problems (migration.remove_missing / duplicate / both / order_unknown / edge_end, and the infra checks on the merged new model), its counts (added, replaced, removed, edges added / removed) and its steps in order; `plan` checks that one plan only. Needs jsonui-cli 2.3.15+ (plans: 2.3.20+).",
46
+ {
47
+ project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
48
+ plan: z.string().optional().describe("One migration plan's name (its folder under docs/infra/migrations); default: every plan"),
49
+ },
50
+ async ({ project_dir, plan }) => {
51
+ try {
52
+ const projectDir = config.resolveProjectDir(project_dir);
53
+ const args = ["validate", "infra", projectDir, "--json"];
54
+ if (plan) args.push("--migration", plan);
55
+ const { exitCode, data } = await runDocJson(args, projectDir);
56
+ need(data, ["ok"], "validate infra");
57
+ if (!data.ok && data.reason !== "invalid") {
58
+ need(data, ["reason", "message"], "validate infra");
59
+ return text({ ok: false, exit: exitCode, readable: false, reason: data.reason, message: data.message });
60
+ }
61
+ need(data, ["dir", "files", "problems"], "validate infra");
62
+ return text({
63
+ ok: exitCode === 0,
64
+ exit: exitCode,
65
+ readable: data.ok,
66
+ dir: data.dir,
67
+ files: data.files,
68
+ environments: (data.environments ?? []).map((e: any) => e.id),
69
+ counts: data.counts ?? null,
70
+ flows: Array.isArray(data.flows) ? data.flows.length : null,
71
+ problems: problems(data.problems, "validate infra"),
72
+ ...migrations(data, plan),
73
+ });
74
+ } catch (e: any) {
75
+ return failure(e);
76
+ }
77
+ }
78
+ );
79
+ }
@@ -0,0 +1 @@
1
+ 290ce9df migration plans test: an edge whose two ends go at different steps goes at the first of them
@@ -0,0 +1,9 @@
1
+ {
2
+ "validate ok": "{\n \"ok\": true,\n \"exit\": 0,\n \"readable\": true,\n \"dir\": \"docs/infra\",\n \"files\": [\n \"external.json\",\n \"main.json\"\n ],\n \"environments\": [\n \"production\",\n \"staging\"\n ],\n \"counts\": {\n \"groups\": 8,\n \"nodes\": 12,\n \"edges\": 10\n },\n \"flows\": 0,\n \"problems\": {\n \"count\": 0,\n \"errors\": 0,\n \"warnings\": 0,\n \"byCode\": {},\n \"first\": []\n }\n}",
3
+ "validate errors": "{\n \"ok\": false,\n \"exit\": 1,\n \"readable\": true,\n \"dir\": \"docs/infra\",\n \"files\": [\n \"external.json\",\n \"main.json\"\n ],\n \"environments\": [\n \"production\",\n \"staging\"\n ],\n \"counts\": {\n \"groups\": 8,\n \"nodes\": 12,\n \"edges\": 10\n },\n \"flows\": 0,\n \"problems\": {\n \"count\": 1,\n \"errors\": 1,\n \"warnings\": 0,\n \"byCode\": {\n \"schema\": 1\n },\n \"first\": [\n {\n \"severity\": \"error\",\n \"code\": \"schema\",\n \"file\": \"bad.json\",\n \"path\": \"/nodes/0/category\",\n \"message\": \"must be one of client, compute.vm, compute.container, compute.function, compute.k8s, compute.paas, edge.cdn, edge.dns, edge.waf, load_balancer, gateway.api, gateway.nat, network.ip, database.relational, database.document, database.kv, database.search, cache, storage.object, storage.file, storage.block, queue, stream, scheduler, mail, auth, secrets, monitoring, ci, external.saas, security.iam, security.policy, monitoring.alarm\"\n }\n ]\n }\n}",
4
+ "validate unreadable": "{\n \"ok\": false,\n \"exit\": 2,\n \"readable\": false,\n \"reason\": \"no_infra\",\n \"message\": \"no model files in docs/infra (*.json)\"\n}",
5
+ "cost month": "{\n \"ok\": true,\n \"exit\": 0,\n \"dir\": \"docs/infra/load\",\n \"scenario\": \"ordinary\",\n \"scenarios\": [\n {\n \"id\": \"ordinary\",\n \"label\": \"ふだんの月\",\n \"file\": \"ordinary.json\",\n \"month\": \"2026-11\",\n \"environment\": \"production\"\n },\n {\n \"id\": \"sale\",\n \"label\": \"セールの月\",\n \"file\": \"sale.json\",\n \"month\": \"2026-11\",\n \"environment\": \"production\"\n }\n ],\n \"loadProblems\": {\n \"count\": 0,\n \"errors\": 0,\n \"first\": []\n },\n \"priceWarnings\": [],\n \"priceWarningCount\": 0,\n \"month\": {\n \"scenario\": \"ordinary\",\n \"month\": \"2026-11\",\n \"usd\": 1018.5974779999989,\n \"by\": \"provider\",\n \"totals\": [\n {\n \"key\": \"aws\",\n \"label\": \"aws\",\n \"usd\": 1018.5974779999997\n }\n ],\n \"days\": 30,\n \"dearestDay\": \"2026-11-30\",\n \"noPrice\": [\n {\n \"node\": \"mailer\",\n \"meter\": \"emails\",\n \"sku\": \"emails\",\n \"unit\": \"email\"\n },\n {\n \"node\": \"api-5xx\",\n \"meter\": \"alarm\",\n \"sku\": \"alarm\",\n \"unit\": \"month\"\n }\n ],\n \"noRule\": [\n \"api-role\"\n ],\n \"loadUndefined\": [\n \"stripe\"\n ],\n \"overflowHours\": {},\n \"notes\": [\n \"no price for (no service) emails (email) in ap-northeast-1\",\n \"no price for cloudwatch.alarm alarm (month) in no region\"\n ],\n \"noteCount\": 2\n },\n \"topNodes\": [\n {\n \"node\": \"cdn\",\n \"usd\": 455.4774000000002\n },\n {\n \"node\": \"db-prod\",\n \"usd\": 422.3999999999987\n },\n {\n \"node\": \"api-prod\",\n \"usd\": 64.80000000000005\n },\n {\n \"node\": \"assets\",\n \"usd\": 44.55927800000017\n },\n {\n \"node\": \"alb-prod\",\n \"usd\": 29.015999999999977\n },\n {\n \"node\": \"api-prod>db-prod\",\n \"usd\": 2.3447999999999984\n }\n ]\n}",
6
+ "cost month by node": "{\n \"ok\": true,\n \"exit\": 0,\n \"dir\": \"docs/infra/load\",\n \"scenario\": \"sale\",\n \"scenarios\": [\n {\n \"id\": \"ordinary\",\n \"label\": \"ふだんの月\",\n \"file\": \"ordinary.json\",\n \"month\": \"2026-11\",\n \"environment\": \"production\"\n },\n {\n \"id\": \"sale\",\n \"label\": \"セールの月\",\n \"file\": \"sale.json\",\n \"month\": \"2026-11\",\n \"environment\": \"production\"\n }\n ],\n \"loadProblems\": {\n \"count\": 0,\n \"errors\": 0,\n \"first\": []\n },\n \"priceWarnings\": [],\n \"priceWarningCount\": 0,\n \"month\": {\n \"scenario\": \"sale\",\n \"month\": \"2026-11\",\n \"usd\": 2679.3521999999975,\n \"by\": \"node\",\n \"totals\": [\n {\n \"key\": \"cdn\",\n \"label\": \"CloudFront\",\n \"usd\": 1975.4412000000011\n },\n {\n \"key\": \"db-prod\",\n \"label\": \"db-prod\",\n \"usd\": 598.2749999999992\n },\n {\n \"key\": \"api-prod\",\n \"label\": \"api-prod\",\n \"usd\": 88.13999999999997\n },\n {\n \"key\": \"alb-prod\",\n \"label\": \"alb-prod\",\n \"usd\": 17.496\n }\n ],\n \"days\": 30,\n \"dearestDay\": \"2026-11-27\",\n \"noPrice\": [\n {\n \"node\": \"api-5xx\",\n \"meter\": \"alarm\",\n \"sku\": \"alarm\",\n \"unit\": \"month\"\n }\n ],\n \"noRule\": [\n \"api-role\"\n ],\n \"loadUndefined\": [\n \"stripe\",\n \"mailer\",\n \"assets\"\n ],\n \"overflowHours\": {\n \"api-prod\": 17,\n \"db-prod\": 3\n },\n \"notes\": [\n \"no price for cloudwatch.alarm alarm (month) in no region\"\n ],\n \"noteCount\": 1\n },\n \"topNodes\": [\n {\n \"node\": \"cdn\",\n \"usd\": 1975.4412000000011\n },\n {\n \"node\": \"db-prod\",\n \"usd\": 598.2749999999991\n },\n {\n \"node\": \"api-prod\",\n \"usd\": 88.13999999999996\n },\n {\n \"node\": \"alb-prod\",\n \"usd\": 17.496\n }\n ]\n}",
7
+ "cost day": "{\n \"ok\": true,\n \"exit\": 0,\n \"dir\": \"docs/infra/load\",\n \"scenario\": \"ordinary\",\n \"scenarios\": [\n {\n \"id\": \"ordinary\",\n \"label\": \"ふだんの月\",\n \"file\": \"ordinary.json\",\n \"month\": \"2026-11\",\n \"environment\": \"production\"\n },\n {\n \"id\": \"sale\",\n \"label\": \"セールの月\",\n \"file\": \"sale.json\",\n \"month\": \"2026-11\",\n \"environment\": \"production\"\n }\n ],\n \"loadProblems\": {\n \"count\": 0,\n \"errors\": 0,\n \"first\": []\n },\n \"priceWarnings\": [],\n \"priceWarningCount\": 0,\n \"day\": {\n \"scenario\": \"ordinary\",\n \"date\": \"2026-11-03\",\n \"usd\": 34.254097666666674,\n \"nodes\": [\n {\n \"node\": \"cdn\",\n \"label\": \"CloudFront\",\n \"provider\": \"aws\",\n \"rule\": \"aws/cloudfront\",\n \"usd\": 15.4623,\n \"peakHour\": 19,\n \"meters\": {\n \"egress-asia\": {\n \"usd\": 1.1940000000000008,\n \"amount\": 9.95,\n \"unit\": \"GB\",\n \"sku\": \"egress-asia\",\n \"layer\": \"bundled\"\n },\n \"egress-japan\": {\n \"usd\": 5.6715,\n \"amount\": 49.75,\n \"unit\": \"GB\",\n \"sku\": \"egress-japan\",\n \"layer\": \"bundled\"\n },\n \"requests-asia\": {\n \"usd\": 1.4328,\n \"amount\": 1194000,\n \"unit\": \"request\",\n \"sku\": \"requests-asia\",\n \"layer\": \"bundled\"\n },\n \"requests-japan\": {\n \"usd\": 7.164,\n \"amount\": 5970000,\n \"unit\": \"request\",\n \"sku\": \"requests-japan\",\n \"layer\": \"bundled\"\n }\n }\n },\n {\n \"node\": \"db-prod\",\n \"label\": \"db-prod\",\n \"provider\": \"aws\",\n \"rule\": \"aws/rds\",\n \"usd\": 14.080000000000005,\n \"peakHour\": 0,\n \"meters\": {\n \"instance\": {\n \"usd\": 12.239999999999997,\n \"amount\": 24,\n \"unit\": \"instance-hour\",\n \"sku\": \"db.r6g.large:multi-az\",\n \"layer\": \"bundled\"\n },\n \"storage\": {\n \"usd\": 1.8400000000000054,\n \"amount\": 6.666666666666665,\n \"unit\": \"GB-month\",\n \"sku\": \"storage:multi-az\",\n \"layer\": \"bundled\"\n }\n }\n },\n {\n \"node\": \"api-prod\",\n \"label\": \"api-prod\",\n \"provider\": \"aws\",\n \"rule\": \"aws/fargate\",\n \"usd\": 2.16,\n \"peakHour\": 0,\n \"meters\": {\n \"vcpu\": {\n \"usd\": 1.7999999999999994,\n \"amount\": 36,\n \"unit\": \"vCPU-hour\",\n \"sku\": \"vcpu\",\n \"layer\": \"user\"\n },\n \"memory\": {\n \"usd\": 0.3600000000000002,\n \"amount\": 72,\n \"unit\": \"GB-hour\",\n \"sku\": \"memory\",\n \"layer\": \"user\"\n }\n }\n },\n {\n \"node\": \"assets\",\n \"label\": \"アセットのバケツ\",\n \"provider\": \"aws\",\n \"rule\": \"aws/s3\",\n \"usd\": 1.5049976666666658,\n \"peakHour\": 19,\n \"meters\": {\n \"storage\": {\n \"usd\": 0.4166666666666656,\n \"amount\": 16.666666666666668,\n \"unit\": \"GB-month\",\n \"sku\": \"storage\",\n \"layer\": \"bundled\"\n },\n \"get\": {\n \"usd\": 1.060272,\n \"amount\": 2865600,\n \"unit\": \"request\",\n \"sku\": \"get\",\n \"layer\": \"user\"\n },\n \"put\": {\n \"usd\": 0.028059,\n \"amount\": 5970,\n \"unit\": \"request\",\n \"sku\": \"put\",\n \"layer\": \"bundled\"\n }\n }\n },\n {\n \"node\": \"alb-prod\",\n \"label\": \"alb-prod\",\n \"provider\": \"aws\",\n \"rule\": \"aws/alb\",\n \"usd\": 0.9672000000000001,\n \"peakHour\": 0,\n \"meters\": {\n \"hour\": {\n \"usd\": 0.5831999999999998,\n \"amount\": 24,\n \"unit\": \"instance-hour\",\n \"sku\": \"hour\",\n \"layer\": \"bundled\"\n },\n \"lcu\": {\n \"usd\": 0.38400000000000023,\n \"amount\": 48,\n \"unit\": \"LCU-hour\",\n \"sku\": \"lcu\",\n \"layer\": \"bundled\"\n }\n }\n },\n {\n \"node\": \"api-prod>db-prod\",\n \"label\": \"api-prod>db-prod\",\n \"provider\": \"aws\",\n \"rule\": \"transfer\",\n \"usd\": 0.07959999999999993,\n \"peakHour\": 19,\n \"meters\": {\n \"transfer-regional\": {\n \"usd\": 0.07959999999999996,\n \"amount\": 3.9800000000000004,\n \"unit\": \"GB\",\n \"sku\": \"transfer-regional\",\n \"layer\": \"bundled\"\n }\n }\n },\n {\n \"node\": \"stripe\",\n \"label\": \"Stripe\",\n \"provider\": \"stripe\",\n \"rule\": \"generic/external.saas\",\n \"note\": \"load undefined\",\n \"usd\": 0,\n \"peakHour\": 0,\n \"meters\": {}\n },\n {\n \"node\": \"mailer\",\n \"label\": \"メール配信\",\n \"provider\": \"sendgrid\",\n \"rule\": \"generic/mail\",\n \"usd\": 0,\n \"peakHour\": 0,\n \"meters\": {\n \"emails\": {\n \"usd\": 0,\n \"amount\": 1658.3333333333335,\n \"unit\": \"email\",\n \"sku\": \"emails\",\n \"layer\": null\n }\n }\n },\n {\n \"node\": \"api-role\",\n \"label\": \"API の実行ロール\",\n \"provider\": \"aws\",\n \"rule\": null,\n \"note\": \"no rule\",\n \"usd\": 0,\n \"peakHour\": 0,\n \"meters\": {}\n },\n {\n \"node\": \"api-5xx\",\n \"label\": \"API の 5xx\",\n \"provider\": \"aws\",\n \"rule\": \"aws/cloudwatch-alarm\",\n \"usd\": 0,\n \"peakHour\": 0,\n \"meters\": {\n \"alarm\": {\n \"usd\": 0,\n \"amount\": 0.03333333333333332,\n \"unit\": \"month\",\n \"sku\": \"alarm\",\n \"layer\": null\n }\n }\n }\n ],\n \"others\": {\n \"count\": 0,\n \"usd\": 0\n }\n }\n}",
8
+ "cost refused": "{\n \"ok\": false,\n \"exit\": 2,\n \"reason\": \"no_scenario\",\n \"message\": \"no scenario 'nope'; there are: ordinary, sale\"\n}"
9
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "ok": true,
3
+ "exit": 0,
4
+ "checks": [
5
+ {
6
+ "name": "infra-load",
7
+ "type": "builtin:infra-load",
8
+ "status": "ok",
9
+ "summary": {
10
+ "ok": 10,
11
+ "mismatch": 0,
12
+ "missing_in_impl": 0,
13
+ "missing_in_doc": 0,
14
+ "skipped": 4,
15
+ "warning": 4,
16
+ "unit": "node"
17
+ },
18
+ "results": [
19
+ {
20
+ "target": "load/ordinary/stripe",
21
+ "status": "warning",
22
+ "confidence": "metadata",
23
+ "expected": "requests, input_tokens, output_tokens",
24
+ "message": "billed by use, but this load definition gives it no load (undefined, not 0)"
25
+ },
26
+ {
27
+ "target": "load/ordinary/api-role",
28
+ "status": "skipped",
29
+ "confidence": "metadata",
30
+ "message": "its kind reads no load"
31
+ },
32
+ {
33
+ "target": "load/ordinary/api-5xx",
34
+ "status": "skipped",
35
+ "confidence": "metadata",
36
+ "message": "its kind reads no load"
37
+ },
38
+ {
39
+ "target": "load/sale/stripe",
40
+ "status": "warning",
41
+ "confidence": "metadata",
42
+ "expected": "requests, input_tokens, output_tokens",
43
+ "message": "billed by use, but this load definition gives it no load (undefined, not 0)"
44
+ },
45
+ {
46
+ "target": "load/sale/mailer",
47
+ "status": "warning",
48
+ "confidence": "metadata",
49
+ "expected": "emails",
50
+ "message": "billed by use, but this load definition gives it no load (undefined, not 0)"
51
+ },
52
+ {
53
+ "target": "load/sale/assets",
54
+ "status": "warning",
55
+ "confidence": "metadata",
56
+ "expected": "get_requests, put_requests, storage_gb, egress_gb",
57
+ "message": "billed by use, but this load definition gives it no load (undefined, not 0)"
58
+ },
59
+ {
60
+ "target": "load/sale/api-role",
61
+ "status": "skipped",
62
+ "confidence": "metadata",
63
+ "message": "its kind reads no load"
64
+ },
65
+ {
66
+ "target": "load/sale/api-5xx",
67
+ "status": "skipped",
68
+ "confidence": "metadata",
69
+ "message": "its kind reads no load"
70
+ }
71
+ ],
72
+ "warnings": [],
73
+ "report": "/private<ROOT>/room/docs/infra/load/.check-report.infra-load.json"
74
+ },
75
+ {
76
+ "name": "infra-code",
77
+ "type": "builtin:infra-code",
78
+ "status": "ok",
79
+ "summary": {
80
+ "ok": 0,
81
+ "mismatch": 0,
82
+ "missing_in_impl": 0,
83
+ "missing_in_doc": 0,
84
+ "skipped": 0,
85
+ "warning": 1,
86
+ "unit": "reference"
87
+ },
88
+ "results": [
89
+ {
90
+ "target": "stripe/stripe#evidence/0",
91
+ "status": "warning",
92
+ "confidence": "proof",
93
+ "expected": "backend/app/config.py",
94
+ "message": "the evidence file is not there"
95
+ }
96
+ ],
97
+ "warnings": [
98
+ "[unverified] *: only Python and JavaScript / TypeScript imports, AWS SDK clients and environment variable names are read (not values, not calls): a service reached another way is not seen"
99
+ ],
100
+ "report": "/private<ROOT>/room/docs/infra/.check-report.infra-code.json"
101
+ }
102
+ ]
103
+ }