@intentius/behold 0.8.0 → 0.9.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 (56) hide show
  1. package/AGENTS.md +85 -0
  2. package/README.md +123 -2
  3. package/demos.json +17 -1
  4. package/dist/cli.js +2522 -294
  5. package/example-argo-estate/README.md +43 -18
  6. package/example-argo-estate/app-a/chant.config.ts +10 -2
  7. package/example-argo-estate/app-a/package.json +2 -2
  8. package/example-argo-estate/app-b/chant.config.ts +11 -2
  9. package/example-argo-estate/app-b/package.json +2 -2
  10. package/example-argo-estate/control-plane/chant.config.ts +20 -5
  11. package/example-argo-estate/control-plane/package.json +2 -2
  12. package/example-argo-estate/package-lock.json +17 -17
  13. package/example-carve/README.md +194 -0
  14. package/example-carve/app/chant.config.ts +6 -0
  15. package/example-carve/app/package-lock.json +1075 -0
  16. package/example-carve/app/package.json +13 -0
  17. package/example-carve/app/src/carved.ts +30 -0
  18. package/example-carve/app/tsconfig.json +1 -0
  19. package/example-carve/carve-report.json +872 -0
  20. package/example-carve/legacy-tf/cdn.tf +23 -0
  21. package/example-carve/legacy-tf/compute.tf +63 -0
  22. package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
  23. package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
  24. package/example-carve/legacy-tf/naming.tf +10 -0
  25. package/example-carve/legacy-tf/network.tf +119 -0
  26. package/example-carve/legacy-tf/observability.tf +18 -0
  27. package/example-carve/legacy-tf/outputs.tf +16 -0
  28. package/example-carve/legacy-tf/storage.tf +33 -0
  29. package/example-carve/legacy-tf/terraform.tfstate +602 -0
  30. package/example-carve/legacy-tf/versions.tf +40 -0
  31. package/example-flux-estate/README.md +9 -4
  32. package/example-flux-estate/app-a/package.json +2 -2
  33. package/example-flux-estate/app-a/src/app.ts +2 -1
  34. package/example-flux-estate/app-b/chant.config.ts +4 -3
  35. package/example-flux-estate/app-b/package.json +2 -2
  36. package/example-flux-estate/app-b/src/app.ts +5 -3
  37. package/example-flux-estate/control-plane/package.json +2 -2
  38. package/example-flux-estate/control-plane/src/flux.ts +4 -2
  39. package/example-flux-estate/package-lock.json +17 -17
  40. package/example-k8s/package-lock.json +18 -18
  41. package/example-k8s/package.json +3 -3
  42. package/example-writes/package-lock.json +14 -14
  43. package/example-writes/package.json +3 -3
  44. package/package.json +8 -6
  45. package/web/app.js +714 -57
  46. package/web/carve-steps.js +610 -0
  47. package/web/carve-steps.test.js +233 -0
  48. package/web/demos.js +71 -0
  49. package/web/demos.test.js +83 -0
  50. package/web/index.html +93 -1
  51. package/web/json-view.js +334 -0
  52. package/web/json-view.test.js +218 -0
  53. package/web/layout-store.js +164 -4
  54. package/web/layout-store.test.js +226 -1
  55. package/web/panel.js +28 -0
  56. package/web/theme.js +57 -1
@@ -14,25 +14,31 @@ Three chant projects served as one composed estate, the Argo mirror of
14
14
  npx @intentius/behold demo argo-estate
15
15
  ```
16
16
 
17
- Unlike flux-estate this one is **declared only** — no cluster, no Argo install,
18
- no Docker. It copies out, `npm install`s, and serves. What to look at:
17
+ The demo path is **declared only** — no cluster, no Argo install, no Docker. It
18
+ copies out, `npm install`s, and serves. (There is a live lane, but it is an
19
+ acceptance run rather than part of the demo: see "Live" below.) What to look
20
+ at:
19
21
 
20
22
  - The **estate**: per-project boundary boxes, and the `project` edges joining
21
23
  both Applications to the AppProject they name (behold#222) — the join chant
22
24
  lints as ARGO002, so an estate whose lint passes cannot be drawn wrong.
23
25
  The entity graph carries them at the attributes tier (`?detail=3`).
24
- - The **logical lens** on the control plane alone
25
- (`behold serve control-plane`, zoom: logical): `namespace app-a` and
26
- `namespace app-b` drawn as boxes even though the control plane declares no
27
- `Namespace` object anywhere. They come from each Application's
28
- `spec.destination.namespace` a namespace Argo will own objects in is a
29
- namespace the estate is committed to. Serve all three members and the apps'
30
- Deployments and Services sit inside those boxes.
31
-
32
- That last one is the Argo/Flux difference worth noticing: flux-estate's control
33
- plane must declare the app namespaces itself (a Kustomization's
34
- `targetNamespace` must already exist), while here `CreateNamespace=true` means
35
- Argo makes them so the box exists on the strength of the destination alone.
26
+ - The **logical lens** (zoom: logical): cluster `namespace app-a` and
27
+ `namespace app-b`, each holding that app's Deployment + Service, plus
28
+ `namespace argocd` holding both Applications and the AppProject drawn
29
+ even though the control plane declares no `Namespace` object anywhere. The
30
+ app namespaces come from each Application's `spec.destination.namespace`; a
31
+ namespace Argo will own objects in is a namespace the estate is committed
32
+ to, and behold#224 taught the composed estate to draw that projection.
33
+ Serve `control-plane` alone (`behold serve control-plane`, zoom: logical)
34
+ and the same boxes appear empty — the destination harvest still names them,
35
+ but there is nothing from app-a/app-b in the graph to fill them with.
36
+
37
+ That alone-vs-composed split is the Argo/Flux difference worth noticing:
38
+ flux-estate's control plane must declare the app namespaces itself (a
39
+ Kustomization's `targetNamespace` must already exist), while here
40
+ `CreateNamespace=true` means Argo makes them — so the box exists on the
41
+ strength of the destination alone.
36
42
 
37
43
  Argo's ordering is the `argocd.argoproj.io/sync-wave` annotation, so unlike
38
44
  Flux's `dependsOn` (behold#223) there is no edge to draw for it: app-a is wave
@@ -40,7 +46,26 @@ Flux's `dependsOn` (behold#223) there is no edge to draw for it: app-a is wave
40
46
 
41
47
  The app `manifests/` are committed `chant build` output — the path each
42
48
  Application syncs. Changing an app's source means a rebuild (`npm run build` in
43
- the app). `chant build` in the control plane warns ARGO005 on both
44
- Applications: `source.path` is repo-relative (that's what Argo resolves it
45
- against), not relative to the build root, so it doesn't resolve locally. Fine
46
- here, and the check's own message says so.
49
+ the app), and the rebuilt manifests have to be on `main` before Argo can sync
50
+ them. `chant build` in the control plane warns ARGO005 on both Applications:
51
+ `source.path` is repo-relative (that's what Argo resolves it against), not
52
+ relative to the build root, so it doesn't resolve locally. Fine here, and the
53
+ check's own message says so.
54
+
55
+ ## Live
56
+
57
+ Nothing above needs a cluster, but the estate as committed does reconcile
58
+ against a real one — the Applications point at this repo on `main` and the app
59
+ `manifests/` are already there, so an Argo CD install has everything it needs.
60
+ `just e2e-argo-estate` (behold#269) is that run: a scratch k3d cluster, Argo
61
+ from the pinned `core-install` manifest, the control plane applied once, and
62
+ then the composed estate served against the result — the destination-namespace
63
+ boxes filled with live cards, #238's health verdicts read off each
64
+ Application's health/sync pair, and the unhappy arm (an Application pointed at
65
+ a path that does not exist, then restored). It creates and deletes its own
66
+ cluster and touches no other context.
67
+
68
+ Each member's `chant.config.ts` therefore binds a `local` profile to the
69
+ context `k3d-behold-argo-demo`, which is what that run aliases its scratch
70
+ cluster to. Serving without `--env` — what `behold demo argo-estate` does —
71
+ reads none of it.
@@ -1,10 +1,18 @@
1
1
  import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
2
3
 
3
4
  // App project A. Argo syncs its committed manifests/ (chant build output) from
4
- // the repo; this project applies nothing itself. Declared-only demo, so no
5
- // kube context is bound see ../control-plane/chant.config.ts.
5
+ // the repo; this project applies nothing itself. behold serves it as part of
6
+ // the estate, and the live overlay reads the cluster through the same binding
7
+ // as every other member — see ../control-plane/chant.config.ts.
6
8
  export default {
7
9
  lexicons: ["k8s"],
8
10
  sourceDir: "src",
11
+ environments: ["local"],
9
12
  ownership: { stack: "argo-app-a" },
13
+ k8s: {
14
+ profiles: {
15
+ local: { context: "k3d-behold-argo-demo" },
16
+ },
17
+ } satisfies K8sChantConfig,
10
18
  } satisfies ChantConfig;
@@ -7,7 +7,7 @@
7
7
  "build": "chant build src -o manifests/app.yaml --format yaml"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.44.3",
11
- "@intentius/chant-lexicon-k8s": "^0.44.3"
10
+ "@intentius/chant": "^0.44.6",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.6"
12
12
  }
13
13
  }
@@ -1,9 +1,18 @@
1
1
  import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
2
3
 
3
- // App project B — same shape as app-a, second sync wave. Declared-only demo,
4
- // so no kube context is bound; see ../control-plane/chant.config.ts.
4
+ // App project B — same shape as app-a, second sync wave. Unlike flux-estate's
5
+ // app-b this one declares its own namespace on every object (Argo has no
6
+ // targetNamespace to stamp one at apply time), so a live read resolves it
7
+ // straight away. See ../control-plane/chant.config.ts for the binding.
5
8
  export default {
6
9
  lexicons: ["k8s"],
7
10
  sourceDir: "src",
11
+ environments: ["local"],
8
12
  ownership: { stack: "argo-app-b" },
13
+ k8s: {
14
+ profiles: {
15
+ local: { context: "k3d-behold-argo-demo" },
16
+ },
17
+ } satisfies K8sChantConfig,
9
18
  } satisfies ChantConfig;
@@ -7,7 +7,7 @@
7
7
  "build": "chant build src -o manifests/app.yaml --format yaml"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.44.3",
11
- "@intentius/chant-lexicon-k8s": "^0.44.3"
10
+ "@intentius/chant": "^0.44.6",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.6"
12
12
  }
13
13
  }
@@ -1,14 +1,29 @@
1
1
  import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
2
3
 
3
4
  // The estate's control plane: it declares the Argo machinery (an AppProject +
4
- // one Application per app), and Argo's controller deploys the app projects —
5
- // this project never applies a workload itself.
5
+ // one Application per app), and Argo's application controller deploys the app
6
+ // projects — this project never applies a workload itself.
6
7
  //
7
- // No k8s profile block, unlike the flux-estate demo's: argo-estate is the
8
- // DECLARED story only. Nothing here binds a kube context because nothing here
9
- // reads one — `behold demo argo-estate` needs no cluster and installs no Argo.
8
+ // `npx @intentius/behold demo argo-estate` still needs no cluster: it serves
9
+ // the source graph with no `--env`, and nothing below is read on that path.
10
+ // The `local` profile exists for the live lane (behold#269,
11
+ // `just e2e-argo-estate`), which stands up a scratch k3d cluster and aliases
12
+ // its context to the name pinned here — so a live read resolves the DECLARED
13
+ // binding and can never observe whatever kubectl context happens to be
14
+ // ambient (chant#1100).
15
+ //
16
+ // Deliberately no `ownership.env`: the app members' committed manifests/ are
17
+ // what Argo syncs from `main`, so a label that changes their build output
18
+ // would have to be merged before any live run could agree with the cluster.
10
19
  export default {
11
20
  lexicons: ["k8s"],
12
21
  sourceDir: "src",
22
+ environments: ["local"],
13
23
  ownership: { stack: "argo-control-plane" },
24
+ k8s: {
25
+ profiles: {
26
+ local: { context: "k3d-behold-argo-demo" },
27
+ },
28
+ } satisfies K8sChantConfig,
14
29
  } satisfies ChantConfig;
@@ -7,7 +7,7 @@
7
7
  "build": "chant build src -o dist/control-plane.yaml --format yaml"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.44.3",
11
- "@intentius/chant-lexicon-k8s": "^0.44.3"
10
+ "@intentius/chant": "^0.44.6",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.6"
12
12
  }
13
13
  }
@@ -14,22 +14,22 @@
14
14
  "app-a": {
15
15
  "name": "behold-example-argo-app-a",
16
16
  "dependencies": {
17
- "@intentius/chant": "^0.44.3",
18
- "@intentius/chant-lexicon-k8s": "^0.44.3"
17
+ "@intentius/chant": "^0.44.6",
18
+ "@intentius/chant-lexicon-k8s": "^0.44.6"
19
19
  }
20
20
  },
21
21
  "app-b": {
22
22
  "name": "behold-example-argo-app-b",
23
23
  "dependencies": {
24
- "@intentius/chant": "^0.44.3",
25
- "@intentius/chant-lexicon-k8s": "^0.44.3"
24
+ "@intentius/chant": "^0.44.6",
25
+ "@intentius/chant-lexicon-k8s": "^0.44.6"
26
26
  }
27
27
  },
28
28
  "control-plane": {
29
29
  "name": "behold-example-argo-control-plane",
30
30
  "dependencies": {
31
- "@intentius/chant": "^0.44.3",
32
- "@intentius/chant-lexicon-k8s": "^0.44.3"
31
+ "@intentius/chant": "^0.44.6",
32
+ "@intentius/chant-lexicon-k8s": "^0.44.6"
33
33
  }
34
34
  },
35
35
  "node_modules/@dagrejs/dagre": {
@@ -464,9 +464,9 @@
464
464
  }
465
465
  },
466
466
  "node_modules/@intentius/chant": {
467
- "version": "0.44.3",
468
- "resolved": "https://registry.npmjs.org/@intentius/chant/-/chant-0.44.3.tgz",
469
- "integrity": "sha512-OVqkUberFaCGn0yrHQn5afzs9i6kRbArZU/AA68pfhh2O8gEfVkVqRO2z4pcF/1W+Lolp1o5aaxj51vqa6yrXA==",
467
+ "version": "0.44.6",
468
+ "resolved": "https://registry.npmjs.org/@intentius/chant/-/chant-0.44.6.tgz",
469
+ "integrity": "sha512-VFqPKe2WsSAJoUvfyghVTlIcj+QRNn//JgSHToq6PDDuKPU7F34V/4f+EZpX4MF3gvwVkZc9d6FUwKio6t0uqg==",
470
470
  "license": "Apache-2.0",
471
471
  "dependencies": {
472
472
  "@dagrejs/dagre": "^3.0.0",
@@ -482,9 +482,9 @@
482
482
  }
483
483
  },
484
484
  "node_modules/@intentius/chant-k8s-client": {
485
- "version": "0.44.3",
486
- "resolved": "https://registry.npmjs.org/@intentius/chant-k8s-client/-/chant-k8s-client-0.44.3.tgz",
487
- "integrity": "sha512-BWNatONqGzPDnZIlBk/5Dze9ANk8NgynvLYcjPJMuBg5EfyUZETt17fq3PsKP2IiOR/fm+ZWHb2mZ3+qEEkfgA==",
485
+ "version": "0.44.6",
486
+ "resolved": "https://registry.npmjs.org/@intentius/chant-k8s-client/-/chant-k8s-client-0.44.6.tgz",
487
+ "integrity": "sha512-Do3zY0Bd2nIITDDrXYePwdOBdnNf59xn5J1aJA6gsg3dzXxR3dZ25YBAh+SQxKbRPuXIbu9rKd5lodaM8ZQDng==",
488
488
  "license": "Apache-2.0",
489
489
  "optional": true,
490
490
  "dependencies": {
@@ -492,19 +492,19 @@
492
492
  }
493
493
  },
494
494
  "node_modules/@intentius/chant-lexicon-k8s": {
495
- "version": "0.44.3",
496
- "resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-k8s/-/chant-lexicon-k8s-0.44.3.tgz",
497
- "integrity": "sha512-7JNg5Hlrqr3vl0iEhQcwUopandy1g4FCkGipFikbDp1KoXyp8QOJaovOOR0nUQ/wRhiRXYJX5Cg13cs0FsNbjQ==",
495
+ "version": "0.44.6",
496
+ "resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-k8s/-/chant-lexicon-k8s-0.44.6.tgz",
497
+ "integrity": "sha512-JP2Lo4fFV7FPbSTHapKuDgpvrsm3dbNBT9pMW9NQB2MoE/ZkMGf3z3/wsPnygLdjbYq29abVYmw7DzPqBmA1tw==",
498
498
  "license": "Apache-2.0",
499
499
  "dependencies": {
500
500
  "@types/js-yaml": "^4.0.9",
501
501
  "js-yaml": "^4.1.1"
502
502
  },
503
503
  "optionalDependencies": {
504
- "@intentius/chant-k8s-client": "^0.44.3"
504
+ "@intentius/chant-k8s-client": "^0.44.6"
505
505
  },
506
506
  "peerDependencies": {
507
- "@intentius/chant": "^0.44.3",
507
+ "@intentius/chant": "^0.44.6",
508
508
  "typescript": "^5.9.3",
509
509
  "zod": "^4.3.6"
510
510
  }
@@ -0,0 +1,194 @@
1
+ # example-carve — the mixed estate the peeling demo runs on
2
+
3
+ A migration caught half-done. `app/` is chant; `legacy-tf/` is Terraform; both
4
+ describe one AWS account, and the walkthrough carves one more resource across
5
+ the line while the camera is running.
6
+
7
+ Run it:
8
+
9
+ ```sh
10
+ behold demo carve # copies this directory, installs, advises, serves
11
+ ```
12
+
13
+ The boot does three things before a port opens: `npm install` in `app/` (whose
14
+ chant every step of the walkthrough shells), `@cdktf/hcl2json` into the copy's
15
+ root `node_modules` (chant lazy-loads the HCL parser from its own install
16
+ upward, so `<copy>/node_modules` is where it resolves — not beside the `.tf`
17
+ files), and `chant carve advise --report` over the copy's own Terraform. If any
18
+ of that fails, the committed `carve-report.json` is served instead and the
19
+ reason is on screen; a blank graph is the one outcome that's never allowed.
20
+
21
+ ```
22
+ example-carve/
23
+ app/ a small chant project — the pieces already carved
24
+ carveout/ where Emit and Bridge write, in a copy (gitignored)
25
+ legacy-tf/ the Terraform half, still Terraform-owned
26
+ terraform.tfstate synthetic state (fake account, fake ARNs)
27
+ floci-override.tf.disabled provider endpoints for the --live tier, inert
28
+ modules/cdn/ a local module, so module.cdn resolves offline
29
+ carve-report.json the committed `carve advise` output
30
+ ```
31
+
32
+ `carveout/` sits INSIDE `app/` rather than beside it, and that is load-bearing:
33
+ the emitted source imports `@intentius/chant-lexicon-aws`, and Node resolves
34
+ that from the file's own directory upward. From `app/carveout/src/assets.ts` it
35
+ reaches `app/node_modules`; from a sibling `carveout/` it would reach nothing,
36
+ and the Emit step's `chant lint` would fail on an install problem rather than
37
+ on the source.
38
+
39
+ ## The estate is mixed from the first frame
40
+
41
+ `app/src/carved.ts` holds a CloudWatch log group and an SSM parameter that came
42
+ out of `legacy-tf/` last month. The source says so, and `legacy-tf/cdn.tf` still
43
+ carries the other half of that carve: a `data "aws_ssm_parameter"` block reading
44
+ back the value chant now owns. That is the data-source rewrite `carve bridge`
45
+ generates, sitting in the estate a month later and working.
46
+
47
+ So the estate view opens on a chant box beside a Terraform box. The morph at the
48
+ end of the walkthrough has somewhere to land.
49
+
50
+ ## Bands
51
+
52
+ Real output, from `chant carve advise` at chant 0.44.6. Reproduce it with:
53
+
54
+ ```sh
55
+ npm install -D @cdktf/hcl2json # once, anywhere on your PATH resolution
56
+ chant carve advise --from example-carve/legacy-tf \
57
+ --state example-carve/legacy-tf/terraform.tfstate
58
+ ```
59
+
60
+ | score | address | band | why | role in the script |
61
+ |---|---|---|---|---|
62
+ | 100 | `aws_cloudwatch_log_group.worker` | clean leaf | clean 1:1 native map, no boundary edges | the free first move: named, not carved |
63
+ | 88 | `aws_s3_bucket.assets` | clean leaf | 1 inbound | **the star** — the resource we carve on camera |
64
+ | 71 | `module.cdn` | carvable w/ edits | 1 outbound, tier 2, data present | one module, so module scoring appears |
65
+ | 69 | `aws_iam_role.api` | carvable w/ edits | 1 inbound, 1 outbound, tier 2 | what an inbound edge costs |
66
+ | 69 | `aws_vpc_endpoint.ssm` | carvable w/ edits | 4 outbound, tier 2 | outbound edges are cheap, but they add up |
67
+ | 65 | `aws_lambda_function.api` | carvable w/ edits | 5 outbound, tier 2 | the honest yellow: next month, not today |
68
+ | 46 | `aws_subnet.private_a` | leave in Terraform | 3 inbound, 2 outbound, data present | |
69
+ | 46 | `aws_subnet.private_b` | leave in Terraform | 3 inbound, 2 outbound, data present | |
70
+ | 43 | `aws_security_group.lambda` | leave in Terraform | 2 inbound, 2 outbound, tier 2, data present | |
71
+ | 36 | `aws_vpc.main` | leave in Terraform | 5 inbound, 1 outbound | the grey anchor: this stays, and that's fine |
72
+ | 0 | `aws_network_acl.private` | leave in Terraform | no known native mapping | |
73
+ | 0 | `random_pet.suffix` | leave in Terraform | no known native mapping | the long tail, stated plainly |
74
+
75
+ Bands: 80-100 carve now, 50-79 carve with boundary edits, 0-49 leave in Terraform.
76
+
77
+ The scoring model is `100 - 12*inbound - 4*outbound - 15*(tier-1) - 10*dynamic
78
+ - 3*(instances-1)`, clamped, with unmapped types pinned at 0. Every number
79
+ above is that arithmetic on the edges in `legacy-tf/`, which is why the estate
80
+ is shaped the way it is:
81
+
82
+ - The bucket has exactly one inbound edge, `aws_lambda_function.api`'s
83
+ `ASSETS_BUCKET` environment variable. One edge, one patch, 88.
84
+ - `aws_s3_bucket_versioning.assets` and `aws_s3_bucket_public_access_block.assets`
85
+ share the bucket's name, so they fold into its carve set. They are not ranked
86
+ separately and their edges to the bucket are not counted against it.
87
+ - Five resources point at the VPC. Carving it would mean patching all five at
88
+ once, and 36 says so.
89
+ - The subnets and the security group each read a data source, which costs 10.
90
+ That is what pushes them under the line rather than sitting awkwardly in the
91
+ middle band next to the star.
92
+
93
+ No resource uses `count` or `for_each`, so the instance-count penalty is zero
94
+ and `--state` produces byte-identical scores to a `.tf`-only parse. The bands do
95
+ not move depending on which way the walkthrough invokes the advisor.
96
+
97
+ ### carve-report.json
98
+
99
+ `carve-report.json` is the `--report` output of exactly the command above,
100
+ committed so the walkthrough and #252's carve lens have an artifact to read
101
+ without shelling anything. JSON carries no comments, so this is its header:
102
+ regenerate it, unchanged, with
103
+
104
+ ```sh
105
+ chant carve advise --from example-carve/legacy-tf \
106
+ --state example-carve/legacy-tf/terraform.tfstate \
107
+ --report example-carve/carve-report.json
108
+ ```
109
+
110
+ from the repository root, so the `from` field stays a relative path.
111
+
112
+ `app/` pins chant ^0.44.6 and that floor is load-bearing, not housekeeping: the
113
+ advisor only publishes a top-level `version: 1` and per-resource `boundary` edge
114
+ lists from 0.44.6 (chant#1636). Run the same command on 0.44.4 and every score
115
+ is identical but no resource carries a `boundary` field at all, so the Pick step
116
+ falls back to naming inbound/outbound COUNTS instead of highlighting the one
117
+ `aws_lambda_function.api` edge the carve severs. The committed report above is
118
+ byte-identical to what 0.44.6 regenerates, which is why the demo's fallback path
119
+ and its fresh-run path show the same picture.
120
+
121
+ ## The six beats
122
+
123
+ These are the six steps on the panel's Carve tab. behold runs beats 4 and 5
124
+ itself, into `app/carveout/` in the copy; beat 6 is copy buttons, on purpose.
125
+
126
+ 1. **The green star.** The estate view, banded. Two resources are green; the
127
+ 100 is a log group nobody will miss, so the eye lands on the 88 next to it.
128
+ 2. **The arithmetic.** Open `aws_s3_bucket.assets`: 100 minus 12 for one
129
+ inbound edge. The score is not a vibe, it is a subtraction, and the inspect
130
+ pane shows the terms.
131
+ 3. **The cut edge.** Highlight the one dependency the carve severs:
132
+ `aws_lambda_function.api` reads the bucket name. One line of Terraform.
133
+ 4. **Emit.** `carve emit --from legacy-tf --state legacy-tf/terraform.tfstate
134
+ --select aws_s3_bucket.assets` adopts the bucket from state into typed chant
135
+ source, folds both sub-resources into the carve set, and writes a boundary
136
+ report. Nothing is applied, nothing is destroyed, and `chant lint` on the
137
+ result exits clean.
138
+ 5. **The bridge.** One `data "aws_s3_bucket" "assets"` block in the surviving
139
+ Terraform, and `aws_lambda_function.api` reads from that instead. The
140
+ surviving plan is whole again.
141
+ 6. **The handoff.** Two commands: `terraform state rm aws_s3_bucket.assets`,
142
+ then chant takes the observe position. `terraform plan` shows no destroy.
143
+
144
+ Then the box slides out of the Terraform half and into `app/`, beside the log
145
+ group and the SSM parameter that made the same trip last month. Around two
146
+ minutes end to end.
147
+
148
+ ## Two tiers
149
+
150
+ **Offline is the default.** The synthetic `terraform.tfstate` is committed, so
151
+ `carve advise` and `carve emit --state` run with no Docker, no terraform binary,
152
+ no AWS account and no network. The only dependency beyond this directory is
153
+ `@cdktf/hcl2json`, which chant lazy-loads and names in its error if absent. This
154
+ tier guarantees the first thirty seconds of the video.
155
+
156
+ **`--live` is the full video.** Boot a scratch Floci, arm
157
+ `legacy-tf/floci-override.tf.disabled` (see its header), and `terraform apply`
158
+ the estate into it, so the state is one terraform really wrote. Then the observe
159
+ beats become footage rather than caption: after Emit, `chant lifecycle diff
160
+ --live` reads the bucket out of Floci, clean, while Terraform still owns it; the
161
+ handoff runs a real `terraform plan` showing no destroy; and behold's overlay
162
+ flips the bucket green afterwards. The line it exists for is "Terraform forgot
163
+ it, chant adopted it, and it never blinked."
164
+
165
+ The live tier needs `docker` and `terraform` on PATH, boots its own throwaway
166
+ Floci and deletes it after, and never touches an existing `floci*` container.
167
+ Floci's emulation bounds it: S3 and CloudWatch Logs, the two types the
168
+ walkthrough actually carves, are covered (chant's `just carve-emit-e2e` is the
169
+ prior art). The VPC, lambda and CloudFront blocks are scenery for the advisor's
170
+ grey band, so expect `-target` rather than one apply of the whole estate.
171
+
172
+ ## Known rough edge in emit
173
+
174
+ `carve emit --state --select aws_s3_bucket.assets` reports "Folded in:
175
+ `aws_s3_bucket_public_access_block.assets`, `aws_s3_bucket_versioning.assets`"
176
+ and then emits a `Bucket` carrying neither. Only `bucket` and `tags` are in the
177
+ AWS carve-out table's field map for `aws_s3_bucket`; versioning, public-access
178
+ block and the state's server-side encryption block come out in the "unmapped
179
+ Terraform attributes" comment instead. `chant lint` on the emitted project
180
+ passes with warnings, but `chant build` fails on two AWS policy rules
181
+ (`PublicAccessBlockConfiguration` missing, no TLS-deny bucket policy) even
182
+ though the Terraform declared the first of those.
183
+
184
+ Verified against chant 0.44.6. The walkthrough's Emit beat shows `lint`, not
185
+ `build`, until the fold is applied as well as reported — the step's "why lint
186
+ and not build?" note says the same thing on screen, so nobody reads a lint pass
187
+ as a build pass. That is chant#1637.
188
+
189
+ ## Values
190
+
191
+ Every name here is a boring-realistic stand-in for a small production estate,
192
+ because the video pauses on the inspect pane. There is no `foo` and no `bar`.
193
+ The account id is `000000000000` and every ARN, id and hostname is fake; the
194
+ state file says so in its `_fixture_note` output.
@@ -0,0 +1,6 @@
1
+ export default {
2
+ lexicons: ["aws"],
3
+ sourceDir: "src",
4
+ environments: ["prod"],
5
+ ownership: { stack: "acme-platform-carved", env: "prod" },
6
+ };