@intentius/chant-lexicon-fly 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,9 +26,9 @@ It fits two kinds of user: teams running on Fly who want their infrastructure as
26
26
  ## Author infrastructure as typed code
27
27
 
28
28
  ```ts
29
- import { App, Machine, MachineConfig, MachineGuest } from "@intentius/chant-lexicon-fly";
29
+ import { App, Machine, MachineConfig, MachineGuest, Fly } from "@intentius/chant-lexicon-fly";
30
30
 
31
- export const app = new App({ name: "my-app" });
31
+ export const app = new App({ name: "my-app", org_slug: Fly.OrgSlug });
32
32
 
33
33
  export const web = new Machine({
34
34
  region: "iad",
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Coverage analysis for the fly lexicon (#804) — how much of the generated
3
+ * Machines API surface the lexicon's typed resources cover. Thin wrapper over
4
+ * the shared `computeCoverage`, mirroring the docker/gcp/github peers.
5
+ */
6
+ import { computeCoverage, overallPct, formatSummary, formatVerbose, checkThresholds, type CoverageReport, type CoverageThresholds } from "@intentius/chant/codegen/coverage";
7
+ export type { CoverageReport, CoverageThresholds };
8
+ export { computeCoverage, overallPct, formatSummary, formatVerbose, checkThresholds };
9
+ /** Run coverage analysis for the fly lexicon. */
10
+ export declare function analyzeFlyCoverage(opts?: {
11
+ basePath?: string;
12
+ verbose?: boolean;
13
+ minOverall?: number;
14
+ }): Promise<CoverageReport>;
15
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EACL,eAAe,EACf,UAAU,EACV,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC;AAEtF,iDAAiD;AACjD,wBAAsB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqB1B"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Read-only MCP tools that expose what `chant build` already computes about a
3
+ * Fly deploy — its apps and machines, the flaps create bodies, image pinning,
4
+ * and the lexicon's findings — so an agent can ask about a change *before it
5
+ * runs* (#804).
6
+ *
7
+ * Every tool builds from source and returns data. None touch a live Fly org,
8
+ * read run history, or write anything — the same context-producer boundary the
9
+ * AWS/GitLab context tools hold.
10
+ */
11
+ import type { McpToolContribution } from "@intentius/chant/mcp/types";
12
+ /** A container image reference is pinned when it names a digest, not a tag. */
13
+ export declare function imagePinned(image: string): boolean;
14
+ export declare function flyContextTools(): McpToolContribution[];
15
+ //# sourceMappingURL=context-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-tools.d.ts","sourceRoot":"","sources":["../../src/mcp/context-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AA+BtE,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wBAAgB,eAAe,IAAI,mBAAmB,EAAE,CAoEvD"}
@@ -3,7 +3,7 @@ export interface FlapsUpArgs {
3
3
  name?: string;
4
4
  /** Host port mapped to the emulator's `:4280`. Default: `4280`. */
5
5
  port?: number;
6
- /** Image. Default: `ghcr.io/intentius/mudflaps:0.3.0`. */
6
+ /** Image. Default: `ghcr.io/intentius/mudflaps:0.3.1`. */
7
7
  image?: string;
8
8
  /** Readiness timeout in ms. Default: `60000`. */
9
9
  timeoutMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAkD,MAAM,0BAA0B,CAAC;AAiB9G;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aAsQvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAkD,MAAM,0BAA0B,CAAC;AAmB9G;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aA8QvB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { ReferenceCatalog } from "@intentius/chant/lexicon";
2
+ /**
3
+ * Fly reference catalog (#804) — how observed Fly resources reference each other,
4
+ * so `chant graph --live` reconstructs the topology and draws the App as a
5
+ * boundary box (the reference resolver, chant#778; containment, chant#779).
6
+ *
7
+ * Fly's relationships are app-centric and simple. `describeResources`
8
+ * (./describe-resources.ts) already returns the rich per-resource shape — each
9
+ * app-scoped resource carries its owning `app`, and a machine carries its full
10
+ * `config` (with `mounts`) — so, unlike AWS (chant#784), Fly needs **no**
11
+ * `enrichLiveAttrs`: the references are already in the observed attributes.
12
+ *
13
+ * Keyed to the `describeResources` attribute shape:
14
+ * - App → `{ app }` (physicalId = app name)
15
+ * - Machine → `{ app, config }` (config.mounts[].volume references a Volume)
16
+ * - Volume → `{ app, volumeName }`
17
+ * - IPAddress → `{ app, ... }`
18
+ * - Certificate→ `{ app, ... }`
19
+ */
20
+ export declare const flyReferenceCatalog: ReferenceCatalog;
21
+ //# sourceMappingURL=reference-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-catalog.d.ts","sourceRoot":"","sources":["../src/reference-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,EAAE,gBAkBjC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-fly",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -53,7 +53,7 @@
53
53
  "typescript": "^5.9.3"
54
54
  },
55
55
  "peerDependencies": {
56
- "@intentius/chant": "^0.16.0"
56
+ "@intentius/chant": "^0.17.0"
57
57
  },
58
58
  "description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
59
59
  "license": "Apache-2.0",
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Coverage analysis for the fly lexicon (#804) — how much of the generated
3
+ * Machines API surface the lexicon's typed resources cover. Thin wrapper over
4
+ * the shared `computeCoverage`, mirroring the docker/gcp/github peers.
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import {
11
+ computeCoverage,
12
+ overallPct,
13
+ formatSummary,
14
+ formatVerbose,
15
+ checkThresholds,
16
+ type CoverageReport,
17
+ type CoverageThresholds,
18
+ } from "@intentius/chant/codegen/coverage";
19
+
20
+ export type { CoverageReport, CoverageThresholds };
21
+ export { computeCoverage, overallPct, formatSummary, formatVerbose, checkThresholds };
22
+
23
+ /** Run coverage analysis for the fly lexicon. */
24
+ export async function analyzeFlyCoverage(opts?: {
25
+ basePath?: string;
26
+ verbose?: boolean;
27
+ minOverall?: number;
28
+ }): Promise<CoverageReport> {
29
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
30
+ const lexiconPath = join(basePath, "src", "generated", "lexicon-fly.json");
31
+
32
+ if (!existsSync(lexiconPath)) {
33
+ throw new Error(`Generated lexicon not found at ${lexiconPath}. Run "chant dev generate" first.`);
34
+ }
35
+
36
+ const report = computeCoverage(readFileSync(lexiconPath, "utf-8"));
37
+
38
+ console.error(opts?.verbose ? formatVerbose(report) : formatSummary(report));
39
+
40
+ if (typeof opts?.minOverall === "number") {
41
+ const result = checkThresholds(report, { minOverallPct: opts.minOverall });
42
+ if (!result.ok) {
43
+ for (const v of result.violations) console.error(` FAIL: ${v}`);
44
+ throw new Error("Coverage below threshold");
45
+ }
46
+ }
47
+
48
+ return report;
49
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { flyContextTools, imagePinned } from "./context-tools";
3
+
4
+ describe("flyContextTools", () => {
5
+ const tools = flyContextTools();
6
+
7
+ it("exposes the read-only fly:* context tools", () => {
8
+ expect(tools.map((t) => t.name).sort()).toEqual(["fly:app", "fly:checks", "fly:plan", "fly:references"]);
9
+ });
10
+
11
+ it("every tool has a description, a path input, and a handler", () => {
12
+ for (const t of tools) {
13
+ expect(t.description.length).toBeGreaterThan(10);
14
+ expect(t.inputSchema.type).toBe("object");
15
+ expect(t.inputSchema.properties).toHaveProperty("path");
16
+ expect(typeof t.handler).toBe("function");
17
+ }
18
+ });
19
+ });
20
+
21
+ describe("imagePinned", () => {
22
+ it("is pinned only when the image names a digest", () => {
23
+ expect(imagePinned("flyio/hellofly@sha256:abc123")).toBe(true);
24
+ expect(imagePinned("flyio/hellofly:latest")).toBe(false);
25
+ expect(imagePinned("flyio/hellofly")).toBe(false);
26
+ });
27
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Read-only MCP tools that expose what `chant build` already computes about a
3
+ * Fly deploy — its apps and machines, the flaps create bodies, image pinning,
4
+ * and the lexicon's findings — so an agent can ask about a change *before it
5
+ * runs* (#804).
6
+ *
7
+ * Every tool builds from source and returns data. None touch a live Fly org,
8
+ * read run history, or write anything — the same context-producer boundary the
9
+ * AWS/GitLab context tools hold.
10
+ */
11
+
12
+ import { build, type BuildResult } from "@intentius/chant/build";
13
+ import type { SerializerResult } from "@intentius/chant/serializer";
14
+ import { runPostSynthChecks, getPrimaryOutput } from "@intentius/chant/lint/post-synth";
15
+ import { postSynthChecks } from "../lint/post-synth";
16
+ import type { McpToolContribution } from "@intentius/chant/mcp/types";
17
+ import { flySerializer } from "../serializer";
18
+ import { parsePlan, isAppRequest, isMachineRequest, type FlyPlan, type FlapsRequest } from "../op/activities/fly-apply";
19
+
20
+ const PATH_INPUT = {
21
+ type: "object" as const,
22
+ properties: {
23
+ path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
24
+ },
25
+ };
26
+
27
+ /** Build the project and parse the flaps plan the serializer emits. */
28
+ async function buildFly(path: string): Promise<{ plan: FlyPlan; result: BuildResult; output?: string | SerializerResult }> {
29
+ const result = await build(path || ".", [flySerializer]);
30
+ const output = result.outputs.get("fly");
31
+ let plan: FlyPlan = {};
32
+ if (output) {
33
+ try {
34
+ plan = parsePlan(getPrimaryOutput(output));
35
+ } catch {
36
+ // not a parseable fly plan — leave empty
37
+ }
38
+ }
39
+ return { plan, result, output };
40
+ }
41
+
42
+ const machineImage = (req: FlapsRequest): string | undefined => {
43
+ const config = req.body.config as { image?: unknown } | undefined;
44
+ return typeof config?.image === "string" ? config.image : undefined;
45
+ };
46
+
47
+ /** A container image reference is pinned when it names a digest, not a tag. */
48
+ export function imagePinned(image: string): boolean {
49
+ return image.includes("@sha256:");
50
+ }
51
+
52
+ export function flyContextTools(): McpToolContribution[] {
53
+ return [
54
+ {
55
+ name: "fly:app",
56
+ description:
57
+ "Build the project and summarize the declared Fly apps and machines — regions, images, guest sizing — from source. Read-only; never touches a live Fly org.",
58
+ inputSchema: PATH_INPUT,
59
+ async handler(params: Record<string, unknown>): Promise<unknown> {
60
+ const { plan } = await buildFly(String(params.path ?? "."));
61
+ const apps: Array<{ name: string; org_slug: unknown }> = [];
62
+ const machines: Array<{ name: string; region: unknown; image: string | null; guest: unknown }> = [];
63
+ for (const [name, req] of Object.entries(plan)) {
64
+ if (isAppRequest(req)) {
65
+ apps.push({ name: String(req.body.app_name ?? name), org_slug: req.body.org_slug ?? null });
66
+ } else if (isMachineRequest(req)) {
67
+ machines.push({
68
+ name: String(req.body.name ?? name),
69
+ region: req.body.region ?? null,
70
+ image: machineImage(req) ?? null,
71
+ guest: (req.body.config as { guest?: unknown } | undefined)?.guest ?? null,
72
+ });
73
+ }
74
+ }
75
+ return { apps, machines };
76
+ },
77
+ },
78
+ {
79
+ name: "fly:checks",
80
+ description:
81
+ "Build the project and return the fly lexicon's post-synth findings (machine requires an image, a mount references a declared Volume) as JSON. Read-only.",
82
+ inputSchema: PATH_INPUT,
83
+ async handler(params: Record<string, unknown>): Promise<unknown> {
84
+ const { output, result } = await buildFly(String(params.path ?? "."));
85
+ if (!output) return { findings: [], note: "no Fly plan produced from this project" };
86
+ const scoped: BuildResult = { ...result, outputs: new Map([["fly", output]]) };
87
+ const diags = runPostSynthChecks(postSynthChecks, scoped);
88
+ return {
89
+ findings: diags.map((d) => ({ id: d.checkId, severity: d.severity, entity: d.entity ?? null, message: d.message })),
90
+ };
91
+ },
92
+ },
93
+ {
94
+ name: "fly:references",
95
+ description:
96
+ "Build the project and list each machine's container image and whether it is pinned to a digest (vs a floating tag). Read-only.",
97
+ inputSchema: PATH_INPUT,
98
+ async handler(params: Record<string, unknown>): Promise<unknown> {
99
+ const { plan } = await buildFly(String(params.path ?? "."));
100
+ const images: Array<{ machine: string; image: string; pinned: boolean }> = [];
101
+ for (const [name, req] of Object.entries(plan)) {
102
+ if (!isMachineRequest(req)) continue;
103
+ const image = machineImage(req);
104
+ if (image) images.push({ machine: String(req.body.name ?? name), image, pinned: imagePinned(image) });
105
+ }
106
+ return images;
107
+ },
108
+ },
109
+ {
110
+ name: "fly:plan",
111
+ description:
112
+ "Build the project and return the flaps create bodies the serializer emits — exactly what flyApply would POST to the Machines API. Read-only.",
113
+ inputSchema: PATH_INPUT,
114
+ async handler(params: Record<string, unknown>): Promise<unknown> {
115
+ const { plan } = await buildFly(String(params.path ?? "."));
116
+ return plan;
117
+ },
118
+ },
119
+ ];
120
+ }
@@ -10,7 +10,7 @@ import {
10
10
  describe("flaps (mudflaps) lifecycle commands", () => {
11
11
  test("run command uses defaults and maps the port", () => {
12
12
  expect(flapsRunCommand({})).toBe(
13
- "docker run -d --rm --name chant-mudflaps -p 4280:4280 ghcr.io/intentius/mudflaps:0.3.0",
13
+ "docker run -d --rm --name chant-mudflaps -p 4280:4280 ghcr.io/intentius/mudflaps:0.3.1",
14
14
  );
15
15
  });
16
16
 
@@ -5,7 +5,7 @@ export interface FlapsUpArgs {
5
5
  name?: string;
6
6
  /** Host port mapped to the emulator's `:4280`. Default: `4280`. */
7
7
  port?: number;
8
- /** Image. Default: `ghcr.io/intentius/mudflaps:0.3.0`. */
8
+ /** Image. Default: `ghcr.io/intentius/mudflaps:0.3.1`. */
9
9
  image?: string;
10
10
  /** Readiness timeout in ms. Default: `60000`. */
11
11
  timeoutMs?: number;
@@ -23,7 +23,7 @@ export interface FlapsDownArgs {
23
23
  // with FLY_FLAPS_BASE_URL. Shared lifecycle: emulatorLifecycle (#746).
24
24
  const flaps = emulatorLifecycle({
25
25
  name: "chant-mudflaps",
26
- image: "ghcr.io/intentius/mudflaps:0.3.0",
26
+ image: "ghcr.io/intentius/mudflaps:0.3.1",
27
27
  containerPort: 4280,
28
28
  healthPath: "/_mudflaps/health",
29
29
  });
package/src/plugin.ts CHANGED
@@ -14,6 +14,8 @@ import { hover } from "./lsp/hover";
14
14
  import { detectTemplate as detectFlyTemplate } from "./detect";
15
15
  import { FlyParser } from "./import/parser";
16
16
  import { FlyGenerator } from "./import/generator";
17
+ import { flyReferenceCatalog } from "./reference-catalog";
18
+ import { flyContextTools } from "./mcp/context-tools";
17
19
 
18
20
  /**
19
21
  * fly lexicon plugin.
@@ -39,8 +41,8 @@ export const flyPlugin: LexiconPlugin = {
39
41
  },
40
42
 
41
43
  async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
42
- // TODO: Implement coverage analysis
43
- console.error("Coverage analysis not yet implemented");
44
+ const { analyzeFlyCoverage } = await import("./coverage");
45
+ await analyzeFlyCoverage({ verbose: options?.verbose, minOverall: options?.minOverall });
44
46
  },
45
47
 
46
48
  async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
@@ -86,7 +88,7 @@ export const flyPlugin: LexiconPlugin = {
86
88
  examples: [
87
89
  {
88
90
  title: "Author an App and a Machine",
89
- output: "new App({ name: \"my-app\" });\nnew Machine({ region: \"iad\", config: new MachineConfig({ image: \"flyio/hellofly:latest\" }) })",
91
+ output: "new App({ name: \"my-app\", org_slug: Fly.OrgSlug });\nnew Machine({ region: \"iad\", config: new MachineConfig({ image: \"flyio/hellofly:latest\" }) })",
90
92
  },
91
93
  {
92
94
  title: "Deploy against mudflaps offline",
@@ -134,11 +136,14 @@ export const flyPlugin: LexiconPlugin = {
134
136
  ]),
135
137
 
136
138
  mcpTools() {
137
- return []; // TODO: Implement MCP tools
139
+ return flyContextTools();
138
140
  },
139
141
 
140
142
  mcpResources() {
141
- return []; // TODO: Implement MCP resources
143
+ // No standing MCP resources: Fly's read-only context is the fly:* tools above
144
+ // (build-from-source) plus the chant-fly skills. There's no static catalog or
145
+ // per-project artifact to expose as a resource.
146
+ return [];
142
147
  },
143
148
 
144
149
  detectTemplate(data: unknown) {
@@ -282,4 +287,9 @@ export { app, web };
282
287
  const { exportResources } = await import("./export-resources");
283
288
  return exportResources(options);
284
289
  },
290
+
291
+ // Live graph edges (#804): how observed Fly resources reference each other, so
292
+ // `chant graph --live` reconstructs the topology (App boundary + mount edges).
293
+ // No `enrichLiveAttrs` needed — describeResources already returns rich attrs.
294
+ referenceCatalog: flyReferenceCatalog,
285
295
  };
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { reconstructEdges, containmentGroups } from "@intentius/chant/graph-refs";
3
+ import type { IRNode } from "@intentius/chant/graph-ir";
4
+ import { flyReferenceCatalog } from "./reference-catalog";
5
+
6
+ const n = (id: string, kind: string, physicalId: string, attrs: Record<string, unknown>): IRNode => ({
7
+ id,
8
+ kind,
9
+ lexicon: "fly",
10
+ physicalId,
11
+ attrs,
12
+ });
13
+
14
+ // The shape describeResources() returns: an App boundary, machines (one mounting
15
+ // a volume), a volume, an IP — all app-scoped.
16
+ const nodes: IRNode[] = [
17
+ n("app", "Fly::Machines::App", "my-app", { app: "my-app" }),
18
+ n("web", "Fly::Machines::Machine", "m1", { app: "my-app", machineName: "web", config: { image: "flyio/hellofly:latest", mounts: [{ volume: "vol_1", path: "/data" }] } }),
19
+ n("worker", "Fly::Machines::Machine", "m2", { app: "my-app", machineName: "worker", config: { image: "flyio/hellofly:latest" } }),
20
+ n("data", "Fly::Machines::Volume", "vol_1", { app: "my-app", volumeName: "data" }),
21
+ n("ip", "Fly::Machines::IPAddress", "1.2.3.4", { app: "my-app", family: "v4" }),
22
+ ];
23
+
24
+ describe("flyReferenceCatalog — live graph edges", () => {
25
+ const { edges, containment, dangling } = reconstructEdges(nodes, flyReferenceCatalog);
26
+
27
+ it("contains every app-scoped resource in its App (boundary box)", () => {
28
+ for (const child of ["web", "worker", "data", "ip"]) {
29
+ expect(containment).toContainEqual({ child, parent: "app", label: "in app" });
30
+ }
31
+ // containment is never an edge
32
+ expect(edges.some((e) => e.to === "app")).toBe(false);
33
+ });
34
+
35
+ it("reconstructs the machine → volume mount edge", () => {
36
+ // mount.volume "vol_1" matches the Volume's physicalId
37
+ expect(edges).toContainEqual({ from: "web", to: "data", kind: "ref", viaAttr: "mounts" });
38
+ // the machine with no mounts has no volume edge
39
+ expect(edges.some((e) => e.from === "worker" && e.to === "data")).toBe(false);
40
+ });
41
+
42
+ it("has no dangling references", () => {
43
+ expect(dangling).toEqual([]);
44
+ });
45
+
46
+ it("containment groups nest App → resources (for #779 boundaries)", () => {
47
+ const g = containmentGroups(containment);
48
+ expect(g.app).toEqual(expect.arrayContaining(["web", "worker", "data", "ip"]));
49
+ });
50
+ });
@@ -0,0 +1,39 @@
1
+ import type { ReferenceCatalog } from "@intentius/chant/lexicon";
2
+
3
+ /**
4
+ * Fly reference catalog (#804) — how observed Fly resources reference each other,
5
+ * so `chant graph --live` reconstructs the topology and draws the App as a
6
+ * boundary box (the reference resolver, chant#778; containment, chant#779).
7
+ *
8
+ * Fly's relationships are app-centric and simple. `describeResources`
9
+ * (./describe-resources.ts) already returns the rich per-resource shape — each
10
+ * app-scoped resource carries its owning `app`, and a machine carries its full
11
+ * `config` (with `mounts`) — so, unlike AWS (chant#784), Fly needs **no**
12
+ * `enrichLiveAttrs`: the references are already in the observed attributes.
13
+ *
14
+ * Keyed to the `describeResources` attribute shape:
15
+ * - App → `{ app }` (physicalId = app name)
16
+ * - Machine → `{ app, config }` (config.mounts[].volume references a Volume)
17
+ * - Volume → `{ app, volumeName }`
18
+ * - IPAddress → `{ app, ... }`
19
+ * - Certificate→ `{ app, ... }`
20
+ */
21
+ export const flyReferenceCatalog: ReferenceCatalog = {
22
+ identities: [
23
+ // The App is identified by its name (also its physicalId), which every
24
+ // app-scoped resource carries in `app`.
25
+ { kind: "Fly::Machines::App", ids: ["app"] },
26
+ // A Volume is referenced by name; its id is already indexed via physicalId,
27
+ // so a mount that names either resolves.
28
+ { kind: "Fly::Machines::Volume", ids: ["volumeName"] },
29
+ ],
30
+ refs: [
31
+ // Containment: everything app-scoped lives inside its App → a boundary box.
32
+ { from: "Fly::Machines::Machine", path: "app", targetKind: "Fly::Machines::App", relation: "containment", label: "in app" },
33
+ { from: "Fly::Machines::Volume", path: "app", targetKind: "Fly::Machines::App", relation: "containment", label: "in app" },
34
+ { from: "Fly::Machines::IPAddress", path: "app", targetKind: "Fly::Machines::App", relation: "containment", label: "in app" },
35
+ { from: "Fly::Machines::Certificate", path: "app", targetKind: "Fly::Machines::App", relation: "containment", label: "in app" },
36
+ // Reference edge: a machine mount → the Volume it mounts.
37
+ { from: "Fly::Machines::Machine", path: "config.mounts[].volume", targetKind: "Fly::Machines::Volume", relation: "reference", label: "mounts" },
38
+ ],
39
+ };
@@ -15,9 +15,9 @@ Beyond the App and Machine covered in `chant-fly`, the lexicon models `Volume`,
15
15
  The FLY011 build check enforces the link statically: every machine mount must reference a `Volume` declared in the stack, checked across files. A mount that points at an undeclared volume fails `chant build` before anything reaches the API.
16
16
 
17
17
  ```ts
18
- import { App, Machine, MachineConfig, MachineGuest, Volume } from "@intentius/chant-lexicon-fly";
18
+ import { App, Machine, MachineConfig, MachineGuest, Volume, Fly } from "@intentius/chant-lexicon-fly";
19
19
 
20
- export const app = new App({ name: "my-app" });
20
+ export const app = new App({ name: "my-app", org_slug: Fly.OrgSlug });
21
21
 
22
22
  export const data = new Volume({ name: "data", region: "iad", size_gb: 10 });
23
23
 
@@ -40,9 +40,9 @@ That Op runs the phases boot, build, apply, verify, and teardown against a local
40
40
  Import resource types from `@intentius/chant-lexicon-fly`. They are generated from Fly's Machines API OpenAPI spec, so `MachineConfig` is typed all the way down through guest, services, mounts, and checks.
41
41
 
42
42
  ```ts
43
- import { App, Machine, MachineConfig, MachineGuest } from "@intentius/chant-lexicon-fly";
43
+ import { App, Machine, MachineConfig, MachineGuest, Fly } from "@intentius/chant-lexicon-fly";
44
44
 
45
- export const app = new App({ name: "my-app" });
45
+ export const app = new App({ name: "my-app", org_slug: Fly.OrgSlug });
46
46
 
47
47
  export const web = new Machine({
48
48
  name: "web",
File without changes