feat(arch): add contracts skeleton and warn-mode arch-check

Give later waves a measurable import/table/path baseline without changing business behavior or occupying the integration slot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-14 13:00:32 +00:00
co-authored by Cursor
parent 241bfccc1e
commit e9d57ea772
6 changed files with 312 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# Wave 0 baseline allowlist. Generated from 2026-09-14 go list, not tightened.
# Target owners are recorded for table/path checks; extra mentions are warnings.
imports:
agent: config
auth: config
authz: docduty
bizdev: authz,config,ctxmgr,db,docduty,docgov,docrevision,freedoc,kb,projectcontext,projectworkflow,quote,squad,storage,stream,workflowdef
chat: bizdev,coordinator,ctxmgr,docgov,intent,projectinstance,replyexport,squad,stream,taskrun
config:
coordinator: agent,bizdev,ctxmgr,docgov,intent,kb,projectinstance,squad,stream
ctxmgr: docgov,replyexport,storage
db:
docduty:
docgov: docduty,docrevision,fsutil
docrevision: fsutil
freedoc: ctxmgr,docrevision,replyexport
fsutil: ""
intent: bizdev
kb: config
notification:
observability:
projectcontext: squad
projectinstance: docgov,projectworkflow,workflowdef
projectworkflow: auth
quote:
replyexport:
server: agent,auth,authz,bizdev,chat,config,coordinator,ctxmgr,docduty,docgov,docrevision,freedoc,intent,notification,observability,projectinstance,projectworkflow,replyexport,squad,stream,taskrun,upload,workflowdef,workspace
squad: agent,config,quote,stream,template
storage:
stream:
taskrun: bizdev,ctxmgr,projectworkflow,stream
template:
upload:
workflow:
workflowdef: projectworkflow,storage,upload,workflow
workspace:
contracts:
table_owners:
document_records: docgov
chat_messages: ctxmgr
chat_turns: ctxmgr
freeform_documents: freedoc
task_runs: taskrun
path_owners:
output.md: squad,workspace
data/dev: config,workspace
workspaces/: squad,workspace
+46
View File
@@ -0,0 +1,46 @@
package contracts
import (
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
)
func TestContractsHasNoBusinessImports(t *testing.T) {
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
ents, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
fset := token.NewFileSet()
for _, ent := range ents {
if ent.IsDir() || !strings.HasSuffix(ent.Name(), ".go") {
continue
}
f, err := parser.ParseFile(fset, filepath.Join(dir, ent.Name()), nil, parser.ImportsOnly)
if err != nil {
t.Fatal(err)
}
for _, spec := range f.Imports {
path := strings.Trim(spec.Path.Value, `"`)
if strings.HasPrefix(path, "chat-agent-system/internal/") && path != "chat-agent-system/internal/contracts" {
t.Fatalf("%s imports business package %s", ent.Name(), path)
}
}
}
}
func TestVocabularyAndErrorCodesStable(t *testing.T) {
if StageV01Triage != "V0.1_triage" || ErrorPlanFrozen != "PLAN_FROZEN" {
t.Fatal("Wave 0 vocabulary drifted")
}
if EventDocumentApproved == "" || EventRunFinalized == "" {
t.Fatal("event names required")
}
}
+19
View File
@@ -0,0 +1,19 @@
package contracts
// Stable API error codes. Handlers should eventually emit only these strings
// in the JSON envelope. Wave 0 records the table; it does not change HTTP.
const (
ErrorDocumentNotFound = "DOCUMENT_NOT_FOUND"
ErrorDocumentContentMissing = "DOCUMENT_CONTENT_PROJECTION_MISSING"
ErrorDocumentContentEmpty = "DOCUMENT_CONTENT_PROJECTION_EMPTY"
ErrorDocumentContentIntegrity = "DOCUMENT_CONTENT_INTEGRITY_MISMATCH"
ErrorDocumentContentDrift = "DOCUMENT_CONTENT_PROJECTION_DRIFT"
ErrorDocumentHeadConflict = "DOCUMENT_HEAD_CONFLICT"
ErrorDocumentEditForbidden = "DOCUMENT_EDIT_FORBIDDEN"
ErrorDocumentBusy = "DOCUMENT_BUSY"
ErrorPlanFrozen = "PLAN_FROZEN"
ErrorWorkflowAccessDenied = "WORKFLOW_ACCESS_DENIED"
ErrorTurnNotCompleted = "TURN_NOT_COMPLETED"
ErrorMessageNotExportable = "MESSAGE_NOT_EXPORTABLE"
ErrorMessageRevisionChanged = "MESSAGE_REVISION_CHANGED"
)
+34
View File
@@ -0,0 +1,34 @@
package contracts
import "time"
// Event is the typed bus payload. Implementations must stay serializable
// and must not embed domain structs from other packages.
type Event interface {
EventName() string
OccurredAt() time.Time
}
const (
EventDocumentApproved = "document.approved"
EventDocumentRejected = "document.rejected"
EventDocumentLeaseChanged = "document.lease_changed"
EventRunFinalized = "run.finalized"
EventStageAdvanced = "stage.advanced"
EventStageGateDenied = "stage.gate_denied"
EventMessageRecorded = "message.recorded"
EventNotificationRequested = "notification.requested"
)
type EventHandler func(ev Event) error
type EventBus interface {
Publish(ev Event) error
Subscribe(name string, h EventHandler)
}
// ProgressReporter is the transport-neutral progress port.
// Domain modules must not import internal/stream.
type ProgressReporter interface {
Report(step string, status string, detail map[string]string)
}
+12
View File
@@ -0,0 +1,12 @@
package contracts
// MaturityStage is the shared business-maturity vocabulary.
// Wave 0 only records the values; intent/bizdev still own their local aliases
// until Wave 1 deletes the reverse import.
type MaturityStage string
const (
StageV01Triage MaturityStage = "V0.1_triage"
StageV05Feasibility MaturityStage = "V0.5_feasibility"
StageV10Quoting MaturityStage = "V1.0_quoting"
)
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
# Architecture boundary check. Wave 0: warn by default, never rewrite code.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BOUNDARIES="${ROOT}/.doc/system-design/module-boundaries.yaml"
MODE="warn"
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="${2:-warn}"; shift 2 ;;
--mode=*) MODE="${1#--mode=}"; shift ;;
*) echo "usage: $0 [--mode warn|block]" >&2; exit 2 ;;
esac
done
if [[ "${MODE}" != "warn" && "${MODE}" != "block" ]]; then
echo "mode must be warn or block" >&2
exit 2
fi
violations=0
warn() {
printf 'WARN %s\n' "$1"
violations=$((violations + 1))
}
lookup_imports() {
local pkg="$1"
awk -v pkg="$pkg" '
$1=="imports:" {sec=1; next}
sec && $1 ~ /^(table_owners|path_owners):/ { exit }
sec && $1 ~ /^[A-Za-z0-9_]+:$/ || sec && $1 ~ /^[A-Za-z0-9_]+:/ {
key=$1; sub(":","",key)
$1=""
gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0)
gsub(/"/,"",$0)
if (key==pkg) { print $0; exit }
next
}
' "${BOUNDARIES}"
}
lookup_owners() {
local section="$1" key="$2"
awk -v secname="$section" -v key="$key" '
$0 ~ "^"secname":" {sec=1; next}
sec && $1 ~ /^(imports|table_owners|path_owners):/ && $1 !~ "^"secname":" { exit }
sec && $1 ~ /^[A-Za-z0-9_./]+:/ {
k=$1; sub(":","",k)
$1=""
gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0)
if (k==key) { print $0; exit }
next
}
' "${BOUNDARIES}"
}
allowed_has() {
local needle="$1"
local list="$2"
[[ -z "${list}" ]] && return 1
IFS=',' read -r -a parts <<< "${list}"
for p in "${parts[@]}"; do
p="${p// /}"
[[ "${p}" == "${needle}" ]] && return 0
done
return 1
}
root_pkg() {
local rel="${1#chat-agent-system/internal/}"
echo "${rel%%/*}"
}
echo "arch-check mode=${MODE}"
echo "boundaries=${BOUNDARIES}"
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
pkg="$(echo "${line}" | awk '{print $1}')"
root="$(root_pkg "${pkg}")"
[[ "${root}" == "workflow" ]] && continue
allow="$(lookup_imports "${root}")"
deps="$(echo "${line}" | awk '{for(i=2;i<=NF;i++) print $i}')"
for dep in ${deps}; do
[[ "${dep}" != chat-agent-system/internal/* ]] && continue
droot="$(root_pkg "${dep}")"
[[ "${droot}" == "${root}" ]] && continue
if ! allowed_has "${droot}" "${allow}"; then
warn "R-import ${root} -> ${droot} not in baseline allowlist"
fi
done
if [[ "${root}" == "contracts" ]]; then
for dep in ${deps}; do
if [[ "${dep}" == chat-agent-system/internal/* && "${dep}" != chat-agent-system/internal/contracts ]]; then
warn "R1 contracts imports ${dep}"
fi
done
fi
done < <(go list -f '{{.ImportPath}} {{join .Imports " "}}' ./internal/...)
declare -A seen_table
while IFS= read -r table; do
owners="$(lookup_owners table_owners "${table}")"
while IFS= read -r file; do
[[ -z "${file}" ]] && continue
rel="${file#${ROOT}/internal/}"
pkg="${rel%%/*}"
key="${table}|${pkg}"
[[ -n "${seen_table[$key]+x}" ]] && continue
seen_table[$key]=1
if ! allowed_has "${pkg}" "${owners}"; then
warn "R5 table ${table} mentioned in ${pkg} (target owner ${owners})"
fi
done < <(rg -l --glob '*.go' --glob '!*_test.go' --fixed-strings "${table}" "${ROOT}/internal" || true)
done <<'TABLES'
document_records
chat_messages
chat_turns
freeform_documents
task_runs
TABLES
declare -A seen_path
while IFS= read -r lit; do
owners="$(lookup_owners path_owners "${lit}")"
while IFS= read -r file; do
[[ -z "${file}" ]] && continue
rel="${file#${ROOT}/internal/}"
pkg="${rel%%/*}"
key="${lit}|${pkg}"
[[ -n "${seen_path[$key]+x}" ]] && continue
seen_path[$key]=1
if ! allowed_has "${pkg}" "${owners}"; then
warn "R6 path ${lit} mentioned in ${pkg} (target owner ${owners})"
fi
done < <(rg -l --glob '*.go' --glob '!*_test.go' --fixed-strings "${lit}" "${ROOT}/internal" || true)
done <<'PATHS'
output.md
data/dev
workspaces/
PATHS
echo "violations=${violations}"
if [[ "${MODE}" == "block" && "${violations}" -gt 0 ]]; then
echo "arch-check block: failing" >&2
exit 1
fi
echo "arch-check ${MODE}: recorded ${violations} baseline findings"
exit 0