@intentius/behold 0.18.0 → 0.20.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 (68) hide show
  1. package/AGENTS.md +377 -76
  2. package/README.md +238 -190
  3. package/demos.json +17 -8
  4. package/dist/cli.js +13118 -11622
  5. package/example-argo-estate/README.md +8 -8
  6. package/example-argo-estate/app-a/package.json +2 -2
  7. package/example-argo-estate/app-b/package.json +2 -2
  8. package/example-argo-estate/chant.workspace.json +27 -0
  9. package/example-argo-estate/control-plane/package.json +2 -2
  10. package/example-argo-estate/package-lock.json +139 -62
  11. package/example-carve/README.md +14 -14
  12. package/example-carve/app/package-lock.json +75 -12
  13. package/example-carve/app/package.json +2 -2
  14. package/example-carve/legacy-tf/versions.tf +1 -1
  15. package/example-choudoufu-estate/README.md +4 -4
  16. package/example-choudoufu-estate/chant.workspace.json +32 -0
  17. package/example-choudoufu-estate/scripts/choudoufu-up.sh +21 -0
  18. package/example-flux-estate/README.md +5 -5
  19. package/example-flux-estate/app-a/package.json +2 -2
  20. package/example-flux-estate/app-b/package.json +2 -2
  21. package/example-flux-estate/chant.workspace.json +27 -0
  22. package/example-flux-estate/control-plane/package.json +2 -2
  23. package/example-flux-estate/package-lock.json +139 -62
  24. package/example-k8s/README.md +20 -20
  25. package/example-k8s/chant.config.ts +1 -1
  26. package/example-k8s/ops/k3d-apply.op.ts +1 -1
  27. package/example-k8s/package-lock.json +135 -217
  28. package/example-k8s/package.json +2 -3
  29. package/example-terraform-estate/README.md +109 -0
  30. package/example-terraform-estate/backends/backend.s3.tf +10 -0
  31. package/example-terraform-estate/chant.workspace.json +22 -0
  32. package/example-terraform-estate/envs/dev/README.md +5 -0
  33. package/example-terraform-estate/envs/prod/data.tf +18 -0
  34. package/example-terraform-estate/envs/prod/network.tf +80 -0
  35. package/example-terraform-estate/envs/prod/outputs.tf +13 -0
  36. package/example-terraform-estate/envs/prod/provider.tf +10 -0
  37. package/example-terraform-estate/envs/prod/services.tf +21 -0
  38. package/example-terraform-estate/envs/prod/storage.tf +31 -0
  39. package/example-terraform-estate/envs/prod/variables.tf +17 -0
  40. package/example-terraform-estate/envs/prod/versions.tf +10 -0
  41. package/example-terraform-estate/modules/service/main.tf +58 -0
  42. package/example-terraform-estate/modules/service/outputs.tf +7 -0
  43. package/example-terraform-estate/modules/service/variables.tf +16 -0
  44. package/example-terraform-estate/modules/service/versions.tf +10 -0
  45. package/example-terraform-estate/platform/iam.tf +40 -0
  46. package/example-terraform-estate/platform/kms.tf +16 -0
  47. package/example-terraform-estate/platform/outputs.tf +7 -0
  48. package/example-terraform-estate/platform/variables.tf +10 -0
  49. package/example-terraform-estate/platform/versions.tf +10 -0
  50. package/example-writes/README.md +5 -5
  51. package/example-writes/chant.config.ts +1 -1
  52. package/example-writes/ops/apply.op.ts +1 -1
  53. package/example-writes/ops/floci.op.ts +2 -2
  54. package/example-writes/ops/reconcile.op.ts +1 -1
  55. package/example-writes/package-lock.json +65 -178
  56. package/example-writes/package.json +2 -3
  57. package/package.json +7 -6
  58. package/web/app.js +439 -34
  59. package/web/behaviour-scale.js +32 -0
  60. package/web/behaviour-scale.test.js +39 -0
  61. package/web/index.html +23 -1
  62. package/web/pending.js +67 -0
  63. package/web/pending.test.js +62 -0
  64. package/web/read-cost.js +49 -0
  65. package/web/read-cost.test.js +76 -0
  66. package/web/refresh-queue.js +26 -0
  67. package/web/refresh-queue.test.js +54 -0
  68. package/example-choudoufu-estate/.behold.json +0 -8
@@ -0,0 +1,109 @@
1
+ # example-terraform-estate
2
+
3
+ A multi-root Terraform estate, declared only. No cloud account, no Docker, no
4
+ `terraform` binary — behold reads the HCL through chant's terraform lexicon and
5
+ draws what it finds.
6
+
7
+ ```sh
8
+ npx @intentius/behold demo terraform-estate
9
+ ```
10
+
11
+ ## What it declares
12
+
13
+ Two roots that are applied separately, plus the shared module both are built
14
+ on. 25 resources, drawing 37 cards and 31 edges at the `resources` zoom.
15
+
16
+ | directory | what it is |
17
+ |---|---|
18
+ | `envs/prod` | The environment root: a VPC with three subnets, a route table and an edge security group, an artifacts bucket with its versioning/public-access/encryption siblings, and two calls to the shared service module. |
19
+ | `platform` | The estate-wide root, applied separately: the permissions boundary every service role attaches, a deploy role, the KMS key and alias, an audit log group. It declares **no provider block** — it inherits one where it is used. |
20
+ | `modules/service` | The shared module `envs/prod` calls twice. An IAM role and inline policy, a log group, a work queue with its dead-letter queue, a task security group. |
21
+ | `backends` | A backend fragment, copied into a root when one is initialised. |
22
+ | `envs/dev` | Not built yet. A README and no `.tf`. |
23
+
24
+ ## What each directory is there to show
25
+
26
+ Discovery has to tell five things apart, and this estate is built to be each of
27
+ them. A root is a directory with a `.tf` declaring a line-start `terraform {` or
28
+ `provider "` block beside a `resource`, `data` or `module` block, a regex probe
29
+ at depth, never an HCL parse.
30
+
31
+ **`envs/prod` is the obvious root.** Provider block, resources, module calls.
32
+
33
+ **`platform` is the root that has no provider block.** The naive reading of "a
34
+ root configures a provider" would miss it. It is a root because it declares
35
+ resources beside a `terraform` block, and it is applied on its own.
36
+
37
+ **`modules/service` is the case that defeats the probe.** Its `versions.tf` is
38
+ byte-for-byte `envs/prod/versions.tf`, the same `required_version`, the same
39
+ `required_providers`. Nothing in the file's content says which of the two is a
40
+ root. What separates them is that this one sits under a `modules/` segment,
41
+ Terraform's own standard module structure, and the roots that call it draw its
42
+ blocks already. Drawing it again would double every card in it.
43
+
44
+ **`backends` has a `terraform` block and nothing to draw.** No resource, no
45
+ data source, no module call. It is reported as skipped rather than drawn as an
46
+ empty box, a box with nothing in it is a question, and the note answers it.
47
+
48
+ **`envs/dev` is neither drawn nor reported.** It holds no `.tf` at all, so
49
+ there is nothing here that looks like Terraform and nothing to say about it.
50
+ Silence is the right answer; reporting it would be noise about every directory
51
+ in every repository.
52
+
53
+ The graph's status line says all of this:
54
+
55
+ ```
56
+ 2 roots — prod, platform; skipped backends (no resource, data or module block —
57
+ nothing to draw), modules/service (called as a module, never applied on its own)
58
+ ```
59
+
60
+ ## The edge that is not drawn
61
+
62
+ `platform` declares the KMS key and its alias. `envs/prod` reads it back:
63
+
64
+ ```hcl
65
+ data "aws_kms_key" "estate" {
66
+ key_id = "alias/${var.name}-estate"
67
+ }
68
+ ```
69
+
70
+ That is a real cross-root reference, and it is how a multi-root estate is
71
+ actually wired — two roots applied separately, neither a module of the other,
72
+ joined by a name one publishes and the other looks up.
73
+
74
+ **behold draws no edge for it**, and that is deliberate. Both ends carry the
75
+ same unresolved `${var.name}` interpolation, so matching them would be a
76
+ coincidence of variable naming rather than a relationship either root states.
77
+ It was measured on a real estate and refused ([#381]). The data source card says
78
+ what it reads instead, which is a claim the estate does make.
79
+
80
+ Module calls *do* draw: each `module` block expands into its own contents under
81
+ `module.checkout/…`, with the reference edges chant's lexicon resolves from the
82
+ HCL.
83
+
84
+ ## It needs the lexicon
85
+
86
+ behold parses no HCL and ships no HCL parser. The reading goes through
87
+ `@intentius/chant-lexicon-terraform`, which loads `@cdktn/hcl2json` underneath —
88
+ a ~1.8 MB wasm blob. Both are declared as optional peers and behold does not
89
+ install them, so that every user who serves an ordinary chant project does not
90
+ carry an HCL parser they will never run:
91
+
92
+ ```sh
93
+ npm install --no-save @intentius/chant-lexicon-terraform @cdktn/hcl2json
94
+ ```
95
+
96
+ Without them the demo refuses with that exact line and says where it looked.
97
+
98
+ ## Not the carve demo
99
+
100
+ `behold demo carve` also involves Terraform and answers a different question.
101
+ This estate asks **what is in my Terraform** — draw it, box it by root, tell the
102
+ roots from the modules. The carve demo asks **what would it cost to leave**:
103
+ `chant carve advise` ranks each resource by peelability and the walkthrough
104
+ carves one across the line.
105
+
106
+ Nothing here is being migrated. There is no chant project beside it, no
107
+ peelability score on any card, and no stepper.
108
+
109
+ [#381]: https://github.com/INTENTIUS/behold/issues/381
@@ -0,0 +1,10 @@
1
+ # A backend fragment, copied into a root when one is initialised. It declares
2
+ # a `terraform` block and nothing to draw, which is why discovery reports it as
3
+ # skipped rather than drawing an empty box for it.
4
+ terraform {
5
+ backend "s3" {
6
+ bucket = "waterline-tfstate"
7
+ key = "prod/terraform.tfstate"
8
+ region = "us-east-1"
9
+ }
10
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "example-terraform-estate",
3
+ "schema": 1,
4
+ "pins": [
5
+ {
6
+ "package": "@intentius/chant-lexicon-terraform",
7
+ "version": "0.95.0"
8
+ }
9
+ ],
10
+ "members": [
11
+ {
12
+ "name": "prod",
13
+ "dir": "envs/prod",
14
+ "kind": "terraform"
15
+ },
16
+ {
17
+ "name": "platform",
18
+ "dir": "platform",
19
+ "kind": "terraform"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,5 @@
1
+ # envs/dev
2
+
3
+ Not built yet. This directory holds no `.tf` at all, which is why discovery
4
+ neither draws it nor reports it as skipped: there is nothing here that looks
5
+ like Terraform, so there is nothing to say about it.
@@ -0,0 +1,18 @@
1
+ data "aws_caller_identity" "current" {}
2
+
3
+ data "aws_region" "current" {}
4
+
5
+ # The cross-root read, and what behold does with it.
6
+ #
7
+ # `platform` declares this key and its alias; this root reads it back by alias
8
+ # rather than by a reference Terraform could resolve, because the two roots are
9
+ # applied separately and neither is a module of the other. That is how a real
10
+ # multi-root estate is wired, and it is also why NO EDGE is drawn between them:
11
+ # both ends carry the same unresolved `${var.name}` interpolation, so a match
12
+ # would be a coincidence of variable naming rather than a stated relationship.
13
+ # behold measured that and refused it (INTENTIUS/behold#381). What you get
14
+ # instead is this card, saying exactly what it reads — which is a claim the
15
+ # estate actually makes.
16
+ data "aws_kms_key" "estate" {
17
+ key_id = "alias/${var.name}-estate"
18
+ }
@@ -0,0 +1,80 @@
1
+ resource "aws_vpc" "main" {
2
+ cidr_block = var.vpc_cidr
3
+ enable_dns_hostnames = true
4
+ enable_dns_support = true
5
+
6
+ tags = { Name = "${var.name}-vpc" }
7
+ }
8
+
9
+ resource "aws_internet_gateway" "main" {
10
+ vpc_id = aws_vpc.main.id
11
+
12
+ tags = { Name = "${var.name}-igw" }
13
+ }
14
+
15
+ resource "aws_subnet" "public_a" {
16
+ vpc_id = aws_vpc.main.id
17
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, 0)
18
+ availability_zone = "${var.region}a"
19
+ map_public_ip_on_launch = true
20
+
21
+ tags = { Name = "${var.name}-public-a" }
22
+ }
23
+
24
+ resource "aws_subnet" "public_b" {
25
+ vpc_id = aws_vpc.main.id
26
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, 1)
27
+ availability_zone = "${var.region}b"
28
+ map_public_ip_on_launch = true
29
+
30
+ tags = { Name = "${var.name}-public-b" }
31
+ }
32
+
33
+ resource "aws_subnet" "private_a" {
34
+ vpc_id = aws_vpc.main.id
35
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, 10)
36
+ availability_zone = "${var.region}a"
37
+
38
+ tags = { Name = "${var.name}-private-a" }
39
+ }
40
+
41
+ resource "aws_route_table" "public" {
42
+ vpc_id = aws_vpc.main.id
43
+
44
+ route {
45
+ cidr_block = "0.0.0.0/0"
46
+ gateway_id = aws_internet_gateway.main.id
47
+ }
48
+
49
+ tags = { Name = "${var.name}-public" }
50
+ }
51
+
52
+ resource "aws_route_table_association" "public_a" {
53
+ subnet_id = aws_subnet.public_a.id
54
+ route_table_id = aws_route_table.public.id
55
+ }
56
+
57
+ resource "aws_route_table_association" "public_b" {
58
+ subnet_id = aws_subnet.public_b.id
59
+ route_table_id = aws_route_table.public.id
60
+ }
61
+
62
+ resource "aws_security_group" "edge" {
63
+ name = "${var.name}-edge"
64
+ description = "Ingress for the public tier."
65
+ vpc_id = aws_vpc.main.id
66
+
67
+ ingress {
68
+ from_port = 443
69
+ to_port = 443
70
+ protocol = "tcp"
71
+ cidr_blocks = ["0.0.0.0/0"]
72
+ }
73
+
74
+ egress {
75
+ from_port = 0
76
+ to_port = 0
77
+ protocol = "-1"
78
+ cidr_blocks = ["0.0.0.0/0"]
79
+ }
80
+ }
@@ -0,0 +1,13 @@
1
+ output "vpc_id" {
2
+ description = "The network everything in this environment sits in."
3
+ value = aws_vpc.main.id
4
+ }
5
+
6
+ output "artifacts_bucket" {
7
+ value = aws_s3_bucket.artifacts.bucket
8
+ }
9
+
10
+ output "service_role_arns" {
11
+ description = "One per service module, for the platform root to reference back."
12
+ value = [module.checkout.role_arn, module.ledger.role_arn]
13
+ }
@@ -0,0 +1,10 @@
1
+ provider "aws" {
2
+ region = var.region
3
+
4
+ default_tags {
5
+ tags = {
6
+ Estate = "waterline"
7
+ Environment = "prod"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,21 @@
1
+ module "checkout" {
2
+ source = "../../modules/service"
3
+
4
+ name = "${var.name}-checkout"
5
+ subnet_ids = [aws_subnet.private_a.id]
6
+ boundary_arn = data.aws_iam_policy.boundary.arn
7
+ log_kms_key_arn = data.aws_kms_key.estate.arn
8
+ }
9
+
10
+ module "ledger" {
11
+ source = "../../modules/service"
12
+
13
+ name = "${var.name}-ledger"
14
+ subnet_ids = [aws_subnet.private_a.id]
15
+ boundary_arn = data.aws_iam_policy.boundary.arn
16
+ log_kms_key_arn = data.aws_kms_key.estate.arn
17
+ }
18
+
19
+ data "aws_iam_policy" "boundary" {
20
+ name = "${var.name}-boundary"
21
+ }
@@ -0,0 +1,31 @@
1
+ resource "aws_s3_bucket" "artifacts" {
2
+ bucket = "${var.name}-artifacts"
3
+ }
4
+
5
+ resource "aws_s3_bucket_versioning" "artifacts" {
6
+ bucket = aws_s3_bucket.artifacts.id
7
+
8
+ versioning_configuration {
9
+ status = "Enabled"
10
+ }
11
+ }
12
+
13
+ resource "aws_s3_bucket_public_access_block" "artifacts" {
14
+ bucket = aws_s3_bucket.artifacts.id
15
+
16
+ block_public_acls = true
17
+ block_public_policy = true
18
+ ignore_public_acls = true
19
+ restrict_public_buckets = true
20
+ }
21
+
22
+ resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
23
+ bucket = aws_s3_bucket.artifacts.id
24
+
25
+ rule {
26
+ apply_server_side_encryption_by_default {
27
+ sse_algorithm = "aws:kms"
28
+ kms_master_key_id = data.aws_kms_key.estate.arn
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ variable "region" {
2
+ description = "The region every resource in this root is created in."
3
+ type = string
4
+ default = "us-east-1"
5
+ }
6
+
7
+ variable "name" {
8
+ description = "Prefix for every resource this root names."
9
+ type = string
10
+ default = "waterline-prod"
11
+ }
12
+
13
+ variable "vpc_cidr" {
14
+ description = "The network this environment sits in."
15
+ type = string
16
+ default = "10.40.0.0/16"
17
+ }
@@ -0,0 +1,10 @@
1
+ terraform {
2
+ required_version = ">= 1.9.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 6.0"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,58 @@
1
+ resource "aws_iam_role" "this" {
2
+ name = "${var.name}-role"
3
+ permissions_boundary = var.boundary_arn
4
+
5
+ assume_role_policy = jsonencode({
6
+ Version = "2012-10-17"
7
+ Statement = [{
8
+ Effect = "Allow"
9
+ Principal = { Service = "ecs-tasks.amazonaws.com" }
10
+ Action = "sts:AssumeRole"
11
+ }]
12
+ })
13
+ }
14
+
15
+ resource "aws_iam_role_policy" "this" {
16
+ name = "${var.name}-inline"
17
+ role = aws_iam_role.this.id
18
+
19
+ policy = jsonencode({
20
+ Version = "2012-10-17"
21
+ Statement = [{
22
+ Effect = "Allow"
23
+ Action = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
24
+ Resource = aws_sqs_queue.work.arn
25
+ }]
26
+ })
27
+ }
28
+
29
+ resource "aws_cloudwatch_log_group" "this" {
30
+ name = "/service/${var.name}"
31
+ retention_in_days = 14
32
+ kms_key_id = var.log_kms_key_arn
33
+ }
34
+
35
+ resource "aws_sqs_queue" "work" {
36
+ name = "${var.name}-work"
37
+ visibility_timeout_seconds = 60
38
+ redrive_policy = jsonencode({
39
+ deadLetterTargetArn = aws_sqs_queue.dead.arn
40
+ maxReceiveCount = 5
41
+ })
42
+ }
43
+
44
+ resource "aws_sqs_queue" "dead" {
45
+ name = "${var.name}-dead"
46
+ }
47
+
48
+ resource "aws_security_group" "task" {
49
+ name = "${var.name}-task"
50
+ description = "Egress only; the task reaches the queue and the log group."
51
+
52
+ egress {
53
+ from_port = 0
54
+ to_port = 0
55
+ protocol = "-1"
56
+ cidr_blocks = ["0.0.0.0/0"]
57
+ }
58
+ }
@@ -0,0 +1,7 @@
1
+ output "role_arn" {
2
+ value = aws_iam_role.this.arn
3
+ }
4
+
5
+ output "queue_url" {
6
+ value = aws_sqs_queue.work.url
7
+ }
@@ -0,0 +1,16 @@
1
+ variable "name" {
2
+ type = string
3
+ }
4
+
5
+ variable "subnet_ids" {
6
+ type = list(string)
7
+ }
8
+
9
+ variable "boundary_arn" {
10
+ description = "The platform root's permissions boundary."
11
+ type = string
12
+ }
13
+
14
+ variable "log_kms_key_arn" {
15
+ type = string
16
+ }
@@ -0,0 +1,10 @@
1
+ terraform {
2
+ required_version = ">= 1.9.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 6.0"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,40 @@
1
+ # The permissions boundary every service role in the estate attaches. Declared
2
+ # once here; each environment root reads it back by name as a data source.
3
+ resource "aws_iam_policy" "boundary" {
4
+ name = "${var.name}-boundary"
5
+ description = "Ceiling for every role the service module creates."
6
+
7
+ policy = jsonencode({
8
+ Version = "2012-10-17"
9
+ Statement = [
10
+ {
11
+ Effect = "Allow"
12
+ Action = ["s3:GetObject", "s3:PutObject", "logs:PutLogEvents", "sqs:*"]
13
+ Resource = "*"
14
+ },
15
+ {
16
+ Effect = "Deny"
17
+ Action = ["iam:*", "organizations:*"]
18
+ Resource = "*"
19
+ },
20
+ ]
21
+ })
22
+ }
23
+
24
+ resource "aws_iam_role" "deploy" {
25
+ name = "${var.name}-deploy"
26
+
27
+ assume_role_policy = jsonencode({
28
+ Version = "2012-10-17"
29
+ Statement = [{
30
+ Effect = "Allow"
31
+ Principal = { Service = "codebuild.amazonaws.com" }
32
+ Action = "sts:AssumeRole"
33
+ }]
34
+ })
35
+ }
36
+
37
+ resource "aws_iam_role_policy_attachment" "deploy_boundary" {
38
+ role = aws_iam_role.deploy.name
39
+ policy_arn = aws_iam_policy.boundary.arn
40
+ }
@@ -0,0 +1,16 @@
1
+ resource "aws_kms_key" "estate" {
2
+ description = "Estate-wide key: artifact encryption and log group encryption."
3
+ deletion_window_in_days = 14
4
+ enable_key_rotation = true
5
+ }
6
+
7
+ resource "aws_kms_alias" "estate" {
8
+ name = "alias/${var.name}-estate"
9
+ target_key_id = aws_kms_key.estate.key_id
10
+ }
11
+
12
+ resource "aws_cloudwatch_log_group" "audit" {
13
+ name = "/${var.name}/audit"
14
+ retention_in_days = var.log_retention_days
15
+ kms_key_id = aws_kms_key.estate.arn
16
+ }
@@ -0,0 +1,7 @@
1
+ output "boundary_arn" {
2
+ value = aws_iam_policy.boundary.arn
3
+ }
4
+
5
+ output "kms_key_arn" {
6
+ value = aws_kms_key.estate.arn
7
+ }
@@ -0,0 +1,10 @@
1
+ variable "name" {
2
+ description = "Prefix for every resource this root names."
3
+ type = string
4
+ default = "waterline-prod"
5
+ }
6
+
7
+ variable "log_retention_days" {
8
+ type = number
9
+ default = 30
10
+ }
@@ -0,0 +1,10 @@
1
+ terraform {
2
+ required_version = ">= 1.9.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 6.0"
8
+ }
9
+ }
10
+ }
@@ -10,7 +10,7 @@ src/bucket.ts one S3 Bucket + a TLS-only policy (the whole "infra")
10
10
  ops/apply.op.ts ApplyOp "prod-apply" — code → real AWS (aws cloudformation deploy)
11
11
  ops/floci.op.ts Op "floci-apply" — code → local Floci (CloudFormation API), no account
12
12
  ops/reconcile.op.ts ReconcileOp "prod-reconcile" — cloud → code PR (the Adopt button)
13
- chant.config.ts lexicons [aws, temporal], environments [prod]
13
+ chant.config.ts lexicons [aws], environments [prod]
14
14
  ```
15
15
 
16
16
  The **Sync** button appears because a project declares an `ApplyOp`. A project with
@@ -19,7 +19,7 @@ no `*.op.ts` (like `../example`) shows no Sync — that's expected, not a bug.
19
19
  ## Creds-free: deploy to a local emulator (`--local`)
20
20
 
21
21
  No AWS account? Serve with `--local` and behold boots a local Floci emulator
22
- (needs Docker), then the **floci-apply** Op deploys the bucket to it — the same
22
+ (needs Docker), then the **floci-apply** Op deploys the bucket to it, the same
23
23
  delegated-write path, zero cloud creds:
24
24
 
25
25
  ```sh
@@ -36,7 +36,7 @@ you Ctrl-C. Everything below uses real AWS instead.
36
36
  ## Prerequisites
37
37
 
38
38
  - Node 20+, and **AWS credentials** in your environment (`aws sts get-caller-identity`
39
- should work). The apply creates a real S3 bucket via CloudFormation — a few cents,
39
+ should work). The apply creates a real S3 bucket via CloudFormation, a few cents,
40
40
  torn down at the end. behold holds no creds; `chant` uses yours on this machine.
41
41
  - One edit: S3 bucket names are **globally unique**, so change `BucketName` in
42
42
  [`src/bucket.ts`](src/bucket.ts) to something of your own (e.g. include your AWS
@@ -59,10 +59,10 @@ npm run dev -- serve example-writes --env prod
59
59
  colour flip.)
60
60
  2. In the header, click **Sync**. The now-line streams the Op's phases:
61
61
  `▶ chant run prod-apply` → Build → Plan → **Apply** (`aws cloudformation deploy`).
62
- `prod-apply` is **ungated**, so it applies straight away — no approval step.
62
+ `prod-apply` is **ungated**, so it applies straight away, no approval step.
63
63
  3. When it finishes, the `store` node flips **blue → green (managed)**. Click it: the
64
64
  inspect panel's **live** section shows the bucket's observed **physical id** and
65
- status. That's the hydration — the cloud's view of your declared resource.
65
+ status. That's the hydration, the cloud's view of your declared resource.
66
66
  4. Click **Refresh** any time to re-check drift and drop a lanes frame.
67
67
 
68
68
  ## What "gated" looks like
@@ -1,7 +1,7 @@
1
1
  import type { TemporalChantConfig } from "@intentius/chant-lexicon-temporal";
2
2
 
3
3
  export default {
4
- lexicons: ["aws", "temporal"],
4
+ lexicons: ["aws"],
5
5
  sourceDir: "src",
6
6
  environments: ["prod"],
7
7
  ownership: { stack: "behold-writes", env: "prod" },
@@ -1,4 +1,4 @@
1
- import { ApplyOp } from "@intentius/chant-lexicon-temporal";
1
+ import { ApplyOp } from "@intentius/chant/op";
2
2
 
3
3
  // code → cloud. Additive apply (no gate) runs one-shot on the local executor —
4
4
  // behold's Sync button shells `chant run prod-apply`. Target cloudformation;
@@ -1,4 +1,5 @@
1
- import { Op, phase, build, awsApply, httpCheck } from "@intentius/chant-lexicon-temporal";
1
+ import { Op, phase, build, httpCheck } from "@intentius/chant/op";
2
+ import { awsApply } from "@intentius/chant-lexicon-aws";
2
3
 
3
4
  // Deploy the S3 bucket to the local Floci emulator via the CloudFormation API
4
5
  // (awsApply — direct create-or-update + poll, no aws CLI, honours the endpoint).
@@ -8,7 +9,6 @@ import { Op, phase, build, awsApply, httpCheck } from "@intentius/chant-lexicon-
8
9
  export default Op({
9
10
  name: "floci-apply",
10
11
  overview: "S3 bucket → local Floci (CloudFormation API), no cloud account",
11
- taskQueue: "behold-local",
12
12
  phases: [
13
13
  phase("Build", [build(".", { script: "build" })]),
14
14
  // Stack name = the env ("prod"), so `serve --local --env prod`'s overlay —
@@ -1,4 +1,4 @@
1
- import { ReconcileOp } from "@intentius/chant-lexicon-temporal";
1
+ import { ReconcileOp } from "@intentius/chant/op";
2
2
 
3
3
  // cloud → code. Snapshot live prod, diff vs source, open a PR for drift/orphans.
4
4
  // Scoped to chant-owned resources. behold's Adopt button triggers this.