@intentius/behold 0.19.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 (42) hide show
  1. package/AGENTS.md +90 -5
  2. package/README.md +5 -1
  3. package/demos.json +3 -2
  4. package/dist/cli.js +1106 -298
  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 +133 -56
  11. package/example-carve/README.md +14 -14
  12. package/example-carve/app/package-lock.json +72 -9
  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 +133 -56
  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 +129 -211
  28. package/example-k8s/package.json +2 -3
  29. package/example-terraform-estate/README.md +5 -5
  30. package/example-terraform-estate/chant.workspace.json +22 -0
  31. package/example-writes/README.md +5 -5
  32. package/example-writes/chant.config.ts +1 -1
  33. package/example-writes/ops/apply.op.ts +1 -1
  34. package/example-writes/ops/floci.op.ts +2 -2
  35. package/example-writes/ops/reconcile.op.ts +1 -1
  36. package/example-writes/package-lock.json +62 -175
  37. package/example-writes/package.json +2 -3
  38. package/package.json +6 -6
  39. package/web/app.js +54 -8
  40. package/web/behaviour-scale.js +32 -0
  41. package/web/behaviour-scale.test.js +39 -0
  42. package/example-choudoufu-estate/.behold.json +0 -8
@@ -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.
@@ -6,9 +6,8 @@
6
6
  "": {
7
7
  "name": "behold-example-writes",
8
8
  "dependencies": {
9
- "@intentius/chant": "^0.54.0",
10
- "@intentius/chant-lexicon-aws": "^0.54.0",
11
- "@intentius/chant-lexicon-temporal": "^0.54.0"
9
+ "@intentius/chant": "^0.95.0",
10
+ "@intentius/chant-lexicon-aws": "^0.95.0"
12
11
  }
13
12
  },
14
13
  "node_modules/@dagrejs/dagre": {
@@ -443,14 +442,16 @@
443
442
  }
444
443
  },
445
444
  "node_modules/@intentius/chant": {
446
- "version": "0.54.0",
447
- "resolved": "https://registry.npmjs.org/@intentius/chant/-/chant-0.54.0.tgz",
448
- "integrity": "sha512-ETYVPvDWqJiEsaaaYK+1U2lGrZZqHPU6/bOH797oQNw5gd7ESkEoYZxlh0qO90FQGVdx1pQMvmlCWskWD4xIlA==",
445
+ "version": "0.95.0",
446
+ "resolved": "https://registry.npmjs.org/@intentius/chant/-/chant-0.95.0.tgz",
447
+ "integrity": "sha512-zhReqEbP7apFyI9bwHATxSKPbc9HK8W2lJvZpib0YNEgSafydm+dd31Hn+7o3AcHhYbAkLpHFQwlFYbM2uVYCg==",
449
448
  "license": "Apache-2.0",
450
449
  "dependencies": {
451
450
  "@dagrejs/dagre": "^3.0.0",
451
+ "ajv": "^8.20.0",
452
452
  "esbuild": "^0.28.1",
453
453
  "fflate": "^0.8.2",
454
+ "js-yaml": "^4.3.1",
454
455
  "picomatch": "^4.0.3",
455
456
  "tsx": "^4.0.0",
456
457
  "typescript": "^5.5.0",
@@ -458,130 +459,44 @@
458
459
  },
459
460
  "bin": {
460
461
  "chant": "bin/chant"
462
+ },
463
+ "peerDependencies": {
464
+ "vitest": ">=2"
465
+ },
466
+ "peerDependenciesMeta": {
467
+ "vitest": {
468
+ "optional": true
469
+ }
461
470
  }
462
471
  },
463
472
  "node_modules/@intentius/chant-lexicon-aws": {
464
- "version": "0.54.0",
465
- "resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-aws/-/chant-lexicon-aws-0.54.0.tgz",
466
- "integrity": "sha512-HVyTEmOu73WJXpVpI9C/TyVo34Rbms9uKohb6sob5Y9Oa/T4yly9P5aqsuE6fMDyLwoLZmknW4v7qxENilrFLg==",
473
+ "version": "0.95.0",
474
+ "resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-aws/-/chant-lexicon-aws-0.95.0.tgz",
475
+ "integrity": "sha512-DHW/cHL4JEU1foEvFK1i2/1Ur4O9meu4dpuyh1o+rnr8DJwW2Ktf7g+84nVDhTjcPVPGE1DROnnQlSuAlu4u8g==",
467
476
  "license": "Apache-2.0",
468
477
  "dependencies": {
469
478
  "fflate": "^0.8.2",
470
479
  "js-yaml": "^4.3.1"
471
480
  },
472
481
  "peerDependencies": {
473
- "@intentius/chant": "^0.54.0",
482
+ "@intentius/chant": "^0.95.0",
474
483
  "typescript": "^5.9.3"
475
484
  }
476
485
  },
477
- "node_modules/@intentius/chant-lexicon-temporal": {
478
- "version": "0.54.0",
479
- "resolved": "https://registry.npmjs.org/@intentius/chant-lexicon-temporal/-/chant-lexicon-temporal-0.54.0.tgz",
480
- "integrity": "sha512-gXjdbhoOKKyGAsCOrgG+O/hUoKyWKoTLLpC4whPiNHtXdoFqdnwkL/CTLfm+xlS898MjSY4widJMa2SWihmbpw==",
481
- "license": "Apache-2.0",
482
- "dependencies": {
483
- "@temporalio/common": "^1.17.2",
484
- "js-yaml": "^4.3.1"
485
- },
486
- "peerDependencies": {
487
- "@intentius/chant": "^0.54.0",
488
- "typescript": "^5.9.3",
489
- "zod": "^4.3.6"
490
- }
491
- },
492
- "node_modules/@protobufjs/aspromise": {
493
- "version": "1.1.2",
494
- "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
495
- "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
496
- "license": "BSD-3-Clause"
497
- },
498
- "node_modules/@protobufjs/base64": {
499
- "version": "1.1.2",
500
- "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
501
- "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
502
- "license": "BSD-3-Clause"
503
- },
504
- "node_modules/@protobufjs/codegen": {
505
- "version": "2.0.5",
506
- "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
507
- "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
508
- "license": "BSD-3-Clause"
509
- },
510
- "node_modules/@protobufjs/eventemitter": {
511
- "version": "1.1.1",
512
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
513
- "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
514
- "license": "BSD-3-Clause"
515
- },
516
- "node_modules/@protobufjs/fetch": {
517
- "version": "1.1.1",
518
- "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
519
- "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
520
- "license": "BSD-3-Clause",
521
- "dependencies": {
522
- "@protobufjs/aspromise": "^1.1.1"
523
- }
524
- },
525
- "node_modules/@protobufjs/float": {
526
- "version": "1.0.2",
527
- "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
528
- "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
529
- "license": "BSD-3-Clause"
530
- },
531
- "node_modules/@protobufjs/path": {
532
- "version": "1.1.2",
533
- "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
534
- "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
535
- "license": "BSD-3-Clause"
536
- },
537
- "node_modules/@protobufjs/pool": {
538
- "version": "1.1.0",
539
- "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
540
- "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
541
- "license": "BSD-3-Clause"
542
- },
543
- "node_modules/@protobufjs/utf8": {
544
- "version": "1.1.2",
545
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
546
- "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
547
- "license": "BSD-3-Clause"
548
- },
549
- "node_modules/@temporalio/common": {
550
- "version": "1.22.0",
551
- "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.22.0.tgz",
552
- "integrity": "sha512-1NpQpo/y6XU1ULbo/bXgBEhl6qQnOeB79NrsSJdjI38/hfXPom2IYGJcIxOFfsQICQ0zePEKzB9Yyo31KNpIxA==",
486
+ "node_modules/ajv": {
487
+ "version": "8.20.0",
488
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
489
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
553
490
  "license": "MIT",
554
491
  "dependencies": {
555
- "@temporalio/proto": "1.22.0",
556
- "long": "^5.2.3",
557
- "ms": "3.0.0-canary.1",
558
- "nexus-rpc": "^0.0.2",
559
- "proto3-json-serializer": "^2.0.0"
492
+ "fast-deep-equal": "^3.1.3",
493
+ "fast-uri": "^3.0.1",
494
+ "json-schema-traverse": "^1.0.0",
495
+ "require-from-string": "^2.0.2"
560
496
  },
561
- "engines": {
562
- "node": ">= 20.3.0"
563
- }
564
- },
565
- "node_modules/@temporalio/proto": {
566
- "version": "1.22.0",
567
- "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.22.0.tgz",
568
- "integrity": "sha512-X7NVa0Z6HK3l9irZR48FKwZ9w9YXetCdF2z2KCIRr0W7Kul64Wt9o5hwvSXShAtrZuCEEHH8WO/ffHTXxJieLg==",
569
- "license": "MIT",
570
- "dependencies": {
571
- "long": "^5.2.3",
572
- "protobufjs": "^7.6.4"
573
- },
574
- "engines": {
575
- "node": ">= 20.3.0"
576
- }
577
- },
578
- "node_modules/@types/node": {
579
- "version": "26.2.0",
580
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
581
- "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
582
- "license": "MIT",
583
- "dependencies": {
584
- "undici-types": "~8.3.0"
497
+ "funding": {
498
+ "type": "github",
499
+ "url": "https://github.com/sponsors/epoberezkin"
585
500
  }
586
501
  },
587
502
  "node_modules/argparse": {
@@ -631,6 +546,28 @@
631
546
  "@esbuild/win32-x64": "0.28.2"
632
547
  }
633
548
  },
549
+ "node_modules/fast-deep-equal": {
550
+ "version": "3.1.3",
551
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
552
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
553
+ "license": "MIT"
554
+ },
555
+ "node_modules/fast-uri": {
556
+ "version": "3.1.8",
557
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
558
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
559
+ "funding": [
560
+ {
561
+ "type": "github",
562
+ "url": "https://github.com/sponsors/fastify"
563
+ },
564
+ {
565
+ "type": "opencollective",
566
+ "url": "https://opencollective.com/fastify"
567
+ }
568
+ ],
569
+ "license": "BSD-3-Clause"
570
+ },
634
571
  "node_modules/fflate": {
635
572
  "version": "0.8.3",
636
573
  "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
@@ -673,29 +610,11 @@
673
610
  "js-yaml": "bin/js-yaml.js"
674
611
  }
675
612
  },
676
- "node_modules/long": {
677
- "version": "5.3.2",
678
- "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
679
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
680
- "license": "Apache-2.0"
681
- },
682
- "node_modules/ms": {
683
- "version": "3.0.0-canary.1",
684
- "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz",
685
- "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==",
686
- "license": "MIT",
687
- "engines": {
688
- "node": ">=12.13"
689
- }
690
- },
691
- "node_modules/nexus-rpc": {
692
- "version": "0.0.2",
693
- "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.2.tgz",
694
- "integrity": "sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg==",
695
- "license": "MIT",
696
- "engines": {
697
- "node": ">= 20.0.0"
698
- }
613
+ "node_modules/json-schema-traverse": {
614
+ "version": "1.0.0",
615
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
616
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
617
+ "license": "MIT"
699
618
  },
700
619
  "node_modules/picomatch": {
701
620
  "version": "4.0.5",
@@ -709,39 +628,13 @@
709
628
  "url": "https://github.com/sponsors/jonschlinkert"
710
629
  }
711
630
  },
712
- "node_modules/proto3-json-serializer": {
631
+ "node_modules/require-from-string": {
713
632
  "version": "2.0.2",
714
- "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
715
- "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
716
- "license": "Apache-2.0",
717
- "dependencies": {
718
- "protobufjs": "^7.2.5"
719
- },
720
- "engines": {
721
- "node": ">=14.0.0"
722
- }
723
- },
724
- "node_modules/protobufjs": {
725
- "version": "7.6.5",
726
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
727
- "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
728
- "hasInstallScript": true,
729
- "license": "BSD-3-Clause",
730
- "dependencies": {
731
- "@protobufjs/aspromise": "^1.1.2",
732
- "@protobufjs/base64": "^1.1.2",
733
- "@protobufjs/codegen": "^2.0.5",
734
- "@protobufjs/eventemitter": "^1.1.1",
735
- "@protobufjs/fetch": "^1.1.1",
736
- "@protobufjs/float": "^1.0.2",
737
- "@protobufjs/path": "^1.1.2",
738
- "@protobufjs/pool": "^1.1.0",
739
- "@protobufjs/utf8": "^1.1.1",
740
- "@types/node": ">=13.7.0",
741
- "long": "^5.3.2"
742
- },
633
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
634
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
635
+ "license": "MIT",
743
636
  "engines": {
744
- "node": ">=12.0.0"
637
+ "node": ">=0.10.0"
745
638
  }
746
639
  },
747
640
  "node_modules/tsx": {
@@ -775,12 +668,6 @@
775
668
  "node": ">=14.17"
776
669
  }
777
670
  },
778
- "node_modules/undici-types": {
779
- "version": "8.3.0",
780
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
781
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
782
- "license": "MIT"
783
- },
784
671
  "node_modules/zod": {
785
672
  "version": "4.4.3",
786
673
  "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
@@ -7,8 +7,7 @@
7
7
  "build": "./node_modules/.bin/chant build src --lexicon aws -o template.json"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.54.0",
11
- "@intentius/chant-lexicon-aws": "^0.54.0",
12
- "@intentius/chant-lexicon-temporal": "^0.54.0"
10
+ "@intentius/chant": "^0.95.0",
11
+ "@intentius/chant-lexicon-aws": "^0.95.0"
13
12
  }
14
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/behold",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
@@ -41,20 +41,20 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@hono/node-server": "^2.0.5",
44
- "@intentius/chant": "^0.61.0",
45
- "@intentius/chant-k8s-client": "^0.61.0",
44
+ "@intentius/chant": "^0.95.0",
45
+ "@intentius/chant-k8s-client": "^0.95.0",
46
46
  "@intentius/pinhole": "^0.3.8",
47
47
  "hono": "^4.12.34"
48
48
  },
49
49
  "peerDependencies": {
50
- "@intentius/chant-lexicon-terraform": "^0.61.0",
51
- "@cdktf/hcl2json": "^0.21.0"
50
+ "@intentius/chant-lexicon-terraform": "^0.95.0",
51
+ "@cdktn/hcl2json": "^0.24.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@intentius/chant-lexicon-terraform": {
55
55
  "optional": true
56
56
  },
57
- "@cdktf/hcl2json": {
57
+ "@cdktn/hcl2json": {
58
58
  "optional": true
59
59
  }
60
60
  },
package/web/app.js CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  scaleEnds,
36
36
  totalsFor,
37
37
  totalsText,
38
+ deltaRows,
38
39
  } from "./behaviour-scale.js";
39
40
  import { addPanelTab, initPanel, setPanelTab, togglePanelCollapsed, isPanelCollapsed } from "./panel.js";
40
41
  // #254: the carve walkthrough's stepper — everything it DECIDES is a pure
@@ -727,6 +728,15 @@ function inspect(node) {
727
728
  // statement about this entity is legible — and where a figure without its
728
729
  // provenance would be visible as the contract violation it is.
729
730
  renderBehaviourSection(section, blockOf(node));
731
+ // #402: what the FILE says this entity costs, beside what the account does.
732
+ // On a card the account no longer holds this is the only behaviour row, and
733
+ // it says so rather than reading as the card's cost.
734
+ const declared = node.attrs && node.attrs._behaviourDeclared;
735
+ if (declared && declared.cost) {
736
+ const add = section("behaviour · declared");
737
+ add("cost", figureWithBadge(fmtCost(declared.cost.perHour, declared.cost.currency), [declared], behaviourMeta()));
738
+ if (!blockOf(node)) add("live", "no live figure — the prediction of the account does not include this entity");
739
+ }
730
740
 
731
741
  // Render diff (#146's deferred half): what `chant helm diff <digest> <env>
732
742
  // --live --json` said about this chart's PINNED render. Never rely on the
@@ -858,15 +868,22 @@ function inspect(node) {
858
868
  const attrKeys = Object.keys(node.attrs || {}).filter((k) => !k.startsWith("_") && !(k === "release" && release));
859
869
  if (attrKeys.length) {
860
870
  const decl = section("declared");
861
- // #393 item 10: an UNOWNED choudoufu card leads with the write that adopts
862
- // the object — the two marker tags choudoufu named (`adopt_tofu_estate` /
863
- // `adopt_tofu_address`), or its own one-line command if a release ever
864
- // sends one. behold composes neither (src/choudoufu-live.ts); this puts
865
- // the line first and makes it copyable, the way the moves panel already
866
- // does for a `live-mv` line. The paragraph explaining the omission — which
867
- // used to be the whole section, with the tags under it as JSON — follows.
871
+ // #393 item 10: a choudoufu card that can be adopted leads with the write
872
+ // that does it — the two marker tags choudoufu named (`adopt_tofu_estate` /
873
+ // `adopt_tofu_address`), or its own one-line command when it sends one.
874
+ // behold composes neither (src/choudoufu-live.ts); this puts the line
875
+ // first and makes it copyable, the way the moves panel already does for a
876
+ // `live-mv` line. The paragraph explaining the omission follows.
877
+ //
878
+ // #413: both kinds reach here now. An UNOWNED row carries the marker pair,
879
+ // and an adoptable one carries choudoufu's own command — so the tooltip
880
+ // says which of the two this is rather than claiming tags either way.
868
881
  const adoptable = node.lexicon === "choudoufu" && typeof node.attrs.adopt === "string" ? "adopt" : null;
869
- if (adoptable) copyableRow(decl, adoptable, node.attrs.adopt, "Copy the tags that adopt this object");
882
+ if (adoptable) {
883
+ const isCommand = /\s/.test(node.attrs.adopt.trim()) && !node.attrs.adopt.startsWith("tofu-estate=");
884
+ copyableRow(decl, adoptable, node.attrs.adopt,
885
+ isCommand ? "Copy the command that adopts this object" : "Copy the tags that adopt this object");
886
+ }
870
887
  for (const k of attrKeys) {
871
888
  if (k === adoptable) continue;
872
889
  decl(k, fmtValue(node.attrs[k]));
@@ -1735,6 +1752,35 @@ function renderBehaviourTotals(host) {
1735
1752
  // precedence in one place rather than at each call site.
1736
1753
  row(b.total ? "estate · engine total" : "estate", all, b.sum, b.total, b.total ? "the engine stated this estate total itself; behold added nothing" : "behold's sum over the engine's per-entity figures");
1737
1754
  if (mode === "headroom") host.appendChild(panelMuted("min and median computed by behold over the engine's per-entity figures — no engine states a headroom aggregate."));
1755
+ if (mode === "cost") renderBehaviourDelta(host, b);
1756
+ }
1757
+
1758
+ /**
1759
+ * Live versus declared (#402): what the account costs as it stands, what the
1760
+ * file says it should, and the difference. Present only when the overlay was
1761
+ * asked for at a traffic level, because both figures are predictions *at* one.
1762
+ * The wording of every row is `deltaRows`' (web/behaviour-scale.js).
1763
+ */
1764
+ function renderBehaviourDelta(host, b) {
1765
+ const rows = deltaRows(b);
1766
+ if (!rows.length) return;
1767
+ host.appendChild(panelHeading(`live versus declared${b.at ? ` · at ${b.at.traffic}` : ""}`));
1768
+ for (const t of rows) {
1769
+ const r = document.createElement("div");
1770
+ r.className = "behaviour-total";
1771
+ const name = document.createElement("div");
1772
+ name.className = "grow";
1773
+ name.style.fontWeight = "600";
1774
+ name.textContent = t.label;
1775
+ r.appendChild(name);
1776
+ for (const [word, text] of [["live", t.live], ["declared", t.declared], ["delta", t.delta]]) {
1777
+ const line = document.createElement("div");
1778
+ line.textContent = `${word} ${text}`;
1779
+ r.appendChild(line);
1780
+ }
1781
+ host.appendChild(r);
1782
+ }
1783
+ host.appendChild(panelMuted("delta is live minus declared. A negative one means the account holds less than the file declares."));
1738
1784
  }
1739
1785
 
1740
1786
  function renderPanelScope() {
@@ -199,6 +199,38 @@ export function fmtCost(perHour, currency) {
199
199
  }
200
200
 
201
201
  /** A headroom fraction as the percentage of capacity still free. */
202
+ /** A signed delta, in the engine's own currency: the sign is the statement. */
203
+ export function fmtDelta(perHour, currency) {
204
+ const figure = fmtCost(Math.abs(perHour), currency);
205
+ if (perHour === 0) return `${figure}, no difference`;
206
+ return `${perHour > 0 ? "+" : "-"}${figure}`;
207
+ }
208
+
209
+ /**
210
+ * Live versus declared (#402), as the rows the Scope panel prints: one triple
211
+ * for the estate, one per member that has a side. Empty when the payload
212
+ * carries neither figure, which is a request that named no traffic level.
213
+ *
214
+ * Each figure says whose it is. `total` is the engine's own statement and
215
+ * `sum` is behold adding the engine's per-entity figures; the delta is always
216
+ * behold's subtraction of the two beside it, and a missing side is said, not
217
+ * left blank, so a reader never takes an absent live figure for a zero.
218
+ */
219
+ export function deltaRows(meta) {
220
+ if (!meta || (!meta.live && !meta.declared)) return [];
221
+ const whose = (f) => (f.basis === "total" ? "engine total" : "behold's sum");
222
+ const side = (f, missing) => (f ? `${fmtCost(f.perHour, f.currency)} · ${whose(f)}` : missing);
223
+ const triple = (label, t) => ({
224
+ label,
225
+ live: side(t.live, "no live figure"),
226
+ declared: side(t.declared, "no declared figure"),
227
+ delta: t.delta ? fmtDelta(t.delta.perHour, t.delta.currency) : "no delta, see diagnostics",
228
+ });
229
+ const rows = [triple("estate", meta)];
230
+ for (const [name, t] of Object.entries(meta.members || {}).sort(([a], [b]) => a.localeCompare(b))) rows.push(triple(name, t));
231
+ return rows;
232
+ }
233
+
202
234
  export function fmtHeadroom(v) {
203
235
  return typeof v === "number" && Number.isFinite(v) ? `${Math.round(v * 100)}% free` : "—";
204
236
  }
@@ -9,6 +9,8 @@ import { describe, it, expect } from "vitest";
9
9
  import { THEMES } from "./themes.js";
10
10
  import { tokensFor, hexToOklch } from "./theme.js";
11
11
  import {
12
+ deltaRows,
13
+ fmtDelta,
12
14
  badgeFor,
13
15
  blockOf,
14
16
  countFigures,
@@ -305,3 +307,40 @@ describe("what a refusal and an absence do to the modes (#401)", () => {
305
307
  expect(a.refusal).toBeNull();
306
308
  });
307
309
  });
310
+
311
+ describe("live versus declared rows (#402)", () => {
312
+ const live = { perHour: 0.005, currency: "USD", basis: "sum" };
313
+ const declared = { perHour: 0.01, currency: "USD", basis: "sum" };
314
+
315
+ it("signs the delta, and says when there is none", () => {
316
+ expect(fmtDelta(-0.005, "USD")).toBe("-0.0050 USD/h");
317
+ expect(fmtDelta(1.5, "USD")).toBe("+1.50 USD/h");
318
+ expect(fmtDelta(0, "USD")).toBe("0.0000 USD/h, no difference");
319
+ });
320
+
321
+ it("is empty for a payload with neither figure: no traffic level was named", () => {
322
+ expect(deltaRows(null)).toEqual([]);
323
+ expect(deltaRows({ engine: "e", sum: { perHour: 1, currency: "USD", priced: 1, unpriced: 0 } })).toEqual([]);
324
+ });
325
+
326
+ it("prints the estate, then each member by name, and says whose each figure is", () => {
327
+ const rows = deltaRows({
328
+ live,
329
+ declared,
330
+ delta: { perHour: -0.005, currency: "USD" },
331
+ members: {
332
+ "team-a": { live: { perHour: 0, currency: "USD", basis: "total" }, declared: { perHour: 0, currency: "USD", basis: "total" }, delta: { perHour: 0, currency: "USD" } },
333
+ monolith: { live: { ...live, basis: "total" }, declared: { ...declared, basis: "total" }, delta: { perHour: -0.005, currency: "USD" } },
334
+ },
335
+ });
336
+ expect(rows.map((r) => r.label)).toEqual(["estate", "monolith", "team-a"]);
337
+ expect(rows[0]).toEqual({ label: "estate", live: "0.0050 USD/h · behold's sum", declared: "0.0100 USD/h · behold's sum", delta: "-0.0050 USD/h" });
338
+ expect(rows[1].live).toBe("0.0050 USD/h · engine total");
339
+ });
340
+
341
+ it("says a missing side is missing rather than printing a zero", () => {
342
+ const [estate] = deltaRows({ declared });
343
+ expect(estate.live).toBe("no live figure");
344
+ expect(estate.delta).toBe("no delta, see diagnostics");
345
+ });
346
+ });
@@ -1,8 +0,0 @@
1
- {
2
- "members": [
3
- { "dir": "monolith", "kind": "choudoufu" },
4
- { "dir": "team-a", "kind": "choudoufu" },
5
- { "dir": "team-b", "kind": "choudoufu" },
6
- { "dir": "team-c", "kind": "choudoufu" }
7
- ]
8
- }