Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changes/unreleased/Added-20260809-120000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Added
body: Per-operation fidelity manifest declaring every operation as hand-verified, auto-crud or unimplemented, exposed at `GET /devcloud/api/fidelity` and enforced by a build-failing coverage test
time: 2026-08-09T12:00:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Added-20260809-140000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Added
body: Real tag support for KMS, CloudWatch and EventBridge — TagResource, UntagResource and ListTagsForResource/ListResourceTags now persist tags per resource ARN instead of being echoed by the generic CRUD engine
time: 2026-08-09T14:00:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Fixed-20260809-120100.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: Codegen now parses operations bound to Smithy resource shapes, recovering 285 operations that were invisible to the generator (bedrock 0 of 101, lambda 19 of 85, ecs 12 of 76, transfer 29 of 71, sso-admin 67 of 79)
time: 2026-08-09T12:01:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Fixed-20260809-140100.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: The fidelity manifest now reads each provider's actual dispatch instead of intersecting with the Smithy model, recovering 226 served operations it had hidden (dynamodbstreams listed 4 of its 22, acm's UpdateCertificate, bedrock's InvokeModelWithResponseStream) and dropping 5 non-operations it had invented (identitystore Description/DisplayName/Emails, pipes DELETE/POST)
time: 2026-08-09T14:01:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Fixed-20260809-160000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: EventBridge now drops a bus's or rule's tags when it is deleted. ARNs are derived from the name, so recreating a deleted resource reused its ARN and inherited the previous tags
time: 2026-08-09T16:00:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Fixed-20260809-160100.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: Restored CloudWatch's 17 CRUD-engine operations. The gateway picks the protocol from the request, not from the provider, so CloudWatch reaches the engine whenever a client speaks JSON — filtering the registry by the provider's declared protocol had removed that coverage outright
time: 2026-08-09T16:01:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Fixed-20260809-170000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: EventBridge rule ARNs now name their event bus, as AWS does. A rule name is unique per bus, so same-named rules on two custom buses previously shared one ARN — and with it, one tag set, where tagging one rule changed the other's and deleting one wiped the survivor's
time: 2026-08-09T17:00:00.000000+09:00
custom:
Issue: ""
5 changes: 5 additions & 0 deletions changes/unreleased/Fixed-20260809-170100.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: CloudWatch now drops an alarm's tags when the alarm is deleted, so recreating an alarm under the same name no longer inherits the old one's tags
time: 2026-08-09T17:01:00.000000+09:00
custom:
Issue: ""
51 changes: 51 additions & 0 deletions cmd/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func main() {
services := flag.String("services", "", "Comma-separated list of services to generate (empty = all)")
templateDir := flag.String("templates", "./internal/codegen/templates", "Directory containing Go templates")
scaffoldDir := flag.String("scaffold-output", "", "Output directory for scaffold files (provider.go, register.go)")
servicesDir := flag.String("services-dir", "./internal/services", "Directory containing hand-written providers, scanned for the fidelity manifest")
flag.Parse()

entries, err := os.ReadDir(*modelsDir)
Expand All @@ -45,6 +46,7 @@ func main() {
gen := codegen.NewGenerator(*templateDir)

var crudServices []codegen.CRUDServiceData
modelOps := make(map[string][]string)

for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
Expand All @@ -63,6 +65,15 @@ func main() {
continue
}

// Recorded before the filters below: the fidelity manifest needs the
// operation universe of every modelled service, including the ones whose
// provider is hand-written and generates no package.
names := make([]string, 0, len(model.Operations))
for _, op := range model.Operations {
names = append(names, op.Name)
}
modelOps[model.ServiceID] = names

if len(allowedServices) > 0 && !allowedServices[model.ServiceID] {
continue
}
Expand All @@ -86,6 +97,12 @@ func main() {
// Write the aggregate CRUD registry only when generating the full fleet
// (a filtered run would otherwise clobber it with a partial registry).
if len(allowedServices) == 0 {
providers, err := codegen.ScanProviders(*servicesDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error scanning providers: %v\n", err)
os.Exit(1)
}

sort.Slice(crudServices, func(i, j int) bool {
return crudServices[i].ServiceID < crudServices[j].ServiceID
})
Expand All @@ -103,7 +120,41 @@ func main() {
fmt.Fprintf(os.Stderr, "Error writing CRUD registry: %v\n", err)
os.Exit(1)
}

if err := writeFidelityManifest(gen, *outputDir, modelOps, providers, crudServices); err != nil {
fmt.Fprintf(os.Stderr, "Error writing fidelity manifest: %v\n", err)
os.Exit(1)
}
}

fmt.Println("Code generation complete.")
}

// writeFidelityManifest emits the per-operation fidelity manifest. Like the CRUD
// registry it is only written on a full-fleet run, since a filtered run would
// produce a partial manifest.
func writeFidelityManifest(
gen *codegen.Generator,
outputDir string,
modelOps map[string][]string,
providers map[string]codegen.ProviderScan,
crudServices []codegen.CRUDServiceData,
) error {
handVerified := make(map[string][]string, len(providers))
for id, scan := range providers {
handVerified[id] = scan.Operations
}

content, err := gen.GenerateFidelityManifest(
codegen.BuildFidelityData(modelOps, handVerified, crudServices),
)
if err != nil {
return err
}

dir := filepath.Join(outputDir, "fidelity")
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
return codegen.WriteGo(filepath.Join(dir, "manifest_gen.go"), content)
}
126 changes: 126 additions & 0 deletions cmd/devcloud/fidelity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: Apache-2.0

package main

import (
"testing"

"github.com/skyoo2003/devcloud/internal/generated/fidelity"
"github.com/skyoo2003/devcloud/internal/plugin"
"github.com/skyoo2003/devcloud/internal/shared/crud"
)

// validTiers is the closed set of fidelity tiers. See docs/fidelity-manifest.md.
var validTiers = map[fidelity.Tier]bool{
fidelity.TierHandVerified: true,
fidelity.TierAutoCRUD: true,
fidelity.TierUnimplemented: true,
}

// TestFidelityManifestCoverage is the guarantee behind v1.0's fidelity claim:
// every operation DevCloud can serve carries a declared tier. It runs after
// package init, so both the plugin registry and the CRUD registry hold their
// full surface. Regenerate with `make codegen` when it fails.
func TestFidelityManifestCoverage(t *testing.T) {
// Conservative floors, in the spirit of TestServicePluginConformance: a
// mangled generator or a dropped scan collapses these far below the real
// numbers without the fragility of asserting an exact count.
const (
minServices = 100
minOperations = 6000
)

if len(fidelity.Services) < minServices {
t.Fatalf("manifest covers %d services, want >= %d", len(fidelity.Services), minServices)
}

operations := 0
for id, svc := range fidelity.Services {
if len(svc.Operations) == 0 {
t.Errorf("%s: manifest lists no operations", id)
continue
}
operations += len(svc.Operations)

handVerified := 0
for op, tier := range svc.Operations {
if !validTiers[tier] {
t.Errorf("%s/%s: tier %q is not a declared tier", id, op, tier)
}
if tier == fidelity.TierHandVerified {
handVerified++
}
}
// A service with nothing hand-verified means the scan lost its provider —
// most likely a new path-routing provider that needs a pathRoutedOps
// entry in internal/codegen/scan_handverified.go.
if handVerified == 0 {
t.Errorf("%s: no hand-verified operations; the dispatch scan probably missed this provider", id)
}
}
if operations < minOperations {
t.Errorf("manifest covers %d operations, want >= %d", operations, minOperations)
}
}

// TestFidelityManifestCoversRegisteredServices keeps the manifest honest about
// the live surface: anything the gateway routes to must be classified.
func TestFidelityManifestCoversRegisteredServices(t *testing.T) {
for _, id := range plugin.DefaultRegistry.RegisteredServices() {
if _, ok := fidelity.Services[id]; !ok {
t.Errorf("%s: registered service is absent from the fidelity manifest", id)
}
}
}

// TestFidelityManifestCoversCRUDRegistry checks the other direction: every
// operation the CRUD engine would serve is declared, and never as unimplemented
// — that would promise an InvalidAction the runtime does not return.
func TestFidelityManifestCoversCRUDRegistry(t *testing.T) {
for _, id := range plugin.DefaultRegistry.RegisteredServices() {
for op := range crud.RegisteredOps(id) {
tier, ok := fidelity.Lookup(id, op)
if !ok {
t.Errorf("%s/%s: engine-registered operation is absent from the manifest", id, op)
continue
}
if tier == fidelity.TierUnimplemented {
t.Errorf("%s/%s: declared unimplemented but the CRUD engine serves it", id, op)
}
}
}
}

// TestAutoCRUDIsServedOverJSON pins what makes an auto-crud tier true. The
// gateway derives the protocol from the *request* (gateway.DetectProtocol), not
// from the provider, so a service whose provider declares Query still reaches
// the engine when a client speaks JSON. cloudwatch is the case that matters: its
// provider answers Query for boto3 and falls through to the engine for
// X-Amz-Target callers. Filtering the registry by the provider's declared
// protocol once removed that coverage outright.
func TestAutoCRUDIsServedOverJSON(t *testing.T) {
for id, svc := range fidelity.Services {
for op, tier := range svc.Operations {
if tier != fidelity.TierAutoCRUD {
continue
}
if _, err := crud.Handle(id, op, "json-1.1", []byte("{}")); err != nil {
t.Errorf("%s/%s: declared auto-crud but the engine refused it: %v", id, op, err)
}
}
}
}

// TestFidelityLookup pins the accessor's contract: an unknown service and an
// unknown operation are both misses, not a zero-value tier.
func TestFidelityLookup(t *testing.T) {
if tier, ok := fidelity.Lookup("s3", "PutObject"); !ok || tier != fidelity.TierHandVerified {
t.Errorf("Lookup(s3, PutObject) = %q, %v; want hand-verified, true", tier, ok)
}
if _, ok := fidelity.Lookup("nosuchservice", "PutObject"); ok {
t.Error("Lookup on an unknown service reported a hit")
}
if _, ok := fidelity.Lookup("s3", "NoSuchOperation"); ok {
t.Error("Lookup on an unknown operation reported a hit")
}
}
12 changes: 11 additions & 1 deletion docs/crud-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ operations as scaffolding for local wiring, not as behavioural parity.
|------|---------|
| **hand-verified** | Implemented by the service's provider (explicit dispatch `case`). Highest fidelity. |
| **auto-crud** | Served by the engine with plausible, store-backed responses. Reaches the engine only for engine-wired services (below). |
| **unimplemented** | Not implemented and not CRUD-classifiable — returns an honest `InvalidAction` error. |
| **unimplemented** | Not implemented and not CRUD-classifiable — the call is refused, never faked. JSON and Query services return `InvalidAction`; the path-routed providers (`s3`, `lambda`, `bedrock`) use their own error vocabulary. |

Hand-written operations always win: the engine is only reached when a request
falls through to the provider's `default:` case, so it never shadows a real
Expand Down Expand Up @@ -61,3 +61,13 @@ implementation.

To promote an operation from `auto-crud` to `hand-verified`, implement it as an
explicit `case` in the service provider following existing patterns.

## Which tier is *this* operation?

The tiers above are declared per operation by the generated
[fidelity manifest](fidelity-manifest.md). Ask it directly rather than inferring
from this page (requires `admin.enabled: true`):

```bash
curl -s 'localhost:4747/devcloud/api/fidelity?service=dynamodb'
```
Loading