From aefb1e45a6c88824bdceca5cc5e7a637cbf02fd5 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 16:20:19 +0000 Subject: [PATCH] feat(events): register intent.plan contracts and table owners Freeze the three fact events, intentflow allowlist, and plan table ownership so later units cannot invent producers or write paths outside app/intentflow. Co-authored-by: Cursor --- .doc/system-design/module-boundaries.yaml | 30 +++++++++++++++++++++++ internal/archtest/scan.go | 4 +-- internal/events/registry.go | 22 +++++++++++------ internal/events/registry_test.go | 25 +++++++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index c6c25629..e834ad06 100644 --- a/.doc/system-design/module-boundaries.yaml +++ b/.doc/system-design/module-boundaries.yaml @@ -65,6 +65,7 @@ imports: - app/docflow - app/docnotify - app/docproject + - app/intentflow - app/stageflow - app/uow - artifactvault @@ -113,6 +114,10 @@ imports: - docquery - docrevision - events + app/intentflow: + - app/uow + - events + - intent app/uow: [] app/stageflow: - bizdev @@ -304,6 +309,12 @@ table_owners: - dataset dataset_destroy_requests: - dataset + intent_plans: + - app/intentflow + intent_plan_steps: + - app/intentflow + intent_plan_decisions: + - app/intentflow path_owners: output.md: @@ -348,6 +359,15 @@ event_producers: - taskrun fav.artifact_staged: - app/docflow + intent.plan.awaiting_input: + - app/intentflow + - events + intent.plan.step_changed: + - app/intentflow + - events + intent.plan.completed: + - app/intentflow + - events event_consumers: outbox.notify: @@ -371,6 +391,15 @@ event_consumers: - notify fav.artifact_staged: - fav + intent.plan.awaiting_input: + - projection + - notify + intent.plan.step_changed: + - projection + - notify + intent.plan.completed: + - projection + - notify unwrap_owners: - docgov @@ -380,6 +409,7 @@ unwrap_owners: - events - artifactvault - ctxmgr + - app/intentflow exceptions: - kind: sql_write diff --git a/internal/archtest/scan.go b/internal/archtest/scan.go index 6f26471a..1b11f31c 100644 --- a/internal/archtest/scan.go +++ b/internal/archtest/scan.go @@ -14,8 +14,8 @@ import ( const internalPrefix = "chat-agent-system/internal/" var ( - sqlWriteRe = regexp.MustCompile(`(?is)\b(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)`) - eventNameRe = regexp.MustCompile(`document\.[a-z0-9_.]+`) + sqlWriteRe = regexp.MustCompile(`(?is)\b(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)`) + eventNameRe = regexp.MustCompile(`(?:document|intent\.plan)\.[a-z0-9_.]+`) ) type fileClass int diff --git a/internal/events/registry.go b/internal/events/registry.go index 9028f1d5..f615123f 100644 --- a/internal/events/registry.go +++ b/internal/events/registry.go @@ -12,14 +12,17 @@ const ( ConsumerWorkflowAdvance = "workflow_advance" ConsumerFAV = "fav" - EventCandidateReady = "document.agent_candidate_ready" - EventVersionCommitted = "document.agent_version_committed" - EventDownstreamImpact = "document.downstream_impact_detected" - EventChangeReportDegraded = "document.change_report_degraded" - EventApproved = "document.approved" - EventTaskRunCompleted = "taskrun.completed" - EventTaskRunCancelled = "taskrun.cancelled" - EventFAVArtifactStaged = "fav.artifact_staged" + EventCandidateReady = "document.agent_candidate_ready" + EventVersionCommitted = "document.agent_version_committed" + EventDownstreamImpact = "document.downstream_impact_detected" + EventChangeReportDegraded = "document.change_report_degraded" + EventApproved = "document.approved" + EventTaskRunCompleted = "taskrun.completed" + EventTaskRunCancelled = "taskrun.cancelled" + EventFAVArtifactStaged = "fav.artifact_staged" + EventIntentPlanAwaiting = "intent.plan.awaiting_input" + EventIntentPlanStepChanged = "intent.plan.step_changed" + EventIntentPlanCompleted = "intent.plan.completed" ) // Contract is the static allowlist row for one event name. @@ -44,6 +47,9 @@ func StaticContracts() []Contract { {EventName: EventTaskRunCompleted, SchemaVersion: 1, Enabled: true, Producers: []string{"taskrun"}, Consumers: []string{ConsumerNotify}, PayloadFields: []string{"run_id"}}, {EventName: EventTaskRunCancelled, SchemaVersion: 1, Enabled: true, Producers: []string{"taskrun"}, Consumers: []string{ConsumerNotify}, PayloadFields: []string{"run_id"}}, {EventName: EventFAVArtifactStaged, SchemaVersion: 1, Enabled: false, Producers: []string{"app/docflow"}, Consumers: []string{ConsumerFAV}, PayloadFields: []string{"doc_id", "snapshot_id"}}, + {EventName: EventIntentPlanAwaiting, SchemaVersion: 1, Enabled: true, Producers: []string{"app/intentflow", "events"}, Consumers: []string{ConsumerProjection, ConsumerNotify}, PayloadFields: []string{"plan_id", "revision", "status", "environment", "project_id", "user_id", "session_id", "summary_ref"}}, + {EventName: EventIntentPlanStepChanged, SchemaVersion: 1, Enabled: true, Producers: []string{"app/intentflow", "events"}, Consumers: []string{ConsumerProjection, ConsumerNotify}, PayloadFields: []string{"plan_id", "revision", "step_id", "status", "environment", "project_id", "user_id", "session_id", "summary_ref"}}, + {EventName: EventIntentPlanCompleted, SchemaVersion: 1, Enabled: true, Producers: []string{"app/intentflow", "events"}, Consumers: []string{ConsumerProjection, ConsumerNotify}, PayloadFields: []string{"plan_id", "revision", "status", "environment", "project_id", "user_id", "session_id", "summary_ref"}}, } } diff --git a/internal/events/registry_test.go b/internal/events/registry_test.go index bca117cd..6dfa8ae3 100644 --- a/internal/events/registry_test.go +++ b/internal/events/registry_test.go @@ -49,6 +49,31 @@ func TestManifestJSONIncludesDisabledFAV(t *testing.T) { } } +func TestIntentPlanContractsOmitSensitivePayload(t *testing.T) { + forbidden := []string{"prompt", "slots", "permissions", "result", "input"} + for _, c := range StaticContracts() { + if !strings.HasPrefix(c.EventName, "intent.plan.") { + continue + } + if !c.Enabled { + t.Fatalf("%s must be enabled for U4 append", c.EventName) + } + if !containsName(c.Producers, "app/intentflow") || !containsName(c.Producers, "events") { + t.Fatalf("%s producers = %v", c.EventName, c.Producers) + } + if !containsName(c.Consumers, ConsumerProjection) || !containsName(c.Consumers, ConsumerNotify) { + t.Fatalf("%s consumers = %v", c.EventName, c.Consumers) + } + for _, field := range c.PayloadFields { + for _, bad := range forbidden { + if field == bad { + t.Fatalf("%s payload contains %s", c.EventName, field) + } + } + } + } +} + func TestRegistryParityWithYAML(t *testing.T) { root := moduleRoot(t) b, err := archtest.LoadBoundaries(archtest.DefaultBoundariesPath(root))