chore(arch): classify production writers and path rules

Replace whole-repo grep with four production-only gates so Goose and tests stop looking like writer debt, and so new SQL, path, or event crossings fail immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-14 13:49:54 +00:00
co-authored by Cursor
parent 2269cabf43
commit 312578ccd1
17 changed files with 1338 additions and 7 deletions
+364
View File
@@ -0,0 +1,364 @@
# U1 architecture baseline. Import allowlist is a 2026-09-14 production go list snapshot.
# SQL write / path / event rules apply only to production Go (not Goose, *_test.go, or fixtures).
# Existing debt lives in exceptions with owner + milestone + expiry. New findings block immediately.
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:
- docduty
- 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: []
workflow/runtime:
- projectinstance
- workflowdef
workflowdef:
- projectworkflow
- storage
- upload
- workflow
workspace: []
table_owners:
document_records:
- docgov
chat_messages:
- ctxmgr
chat_turns:
- ctxmgr
freeform_documents:
- freedoc
task_runs:
- taskrun
document_versions:
- docrevision
document_snapshots:
- docrevision
document_change_events:
- docrevision
path_owners:
output.md:
- squad
- workspace
data/dev:
- config
- workspace
workspaces/:
- squad
- workspace
event_producers:
outbox.notify:
- docrevision
document.agent_candidate_ready:
- docrevision
document.agent_version_committed:
- docrevision
document.downstream_impact_detected:
- docrevision
document.change_report_degraded:
- docrevision
event_consumers:
outbox.notify:
- server
- notification
exceptions:
- kind: sql_write
package: docduty
file: internal/docduty/revoke.go
symbol: document_records
owner: docduty
reason: duty slot revoke still writes reviewer/approver columns directly; fold into docgov repository during U3
milestone: U3
expires: "2026-09-28"
- kind: sql_write
package: docduty
file: internal/docduty/store.go
symbol: document_records
owner: docduty
reason: heal/store updates duty columns on document_records; move behind docgov repository in U3
milestone: U3
expires: "2026-09-28"
- kind: sql_write
package: docrevision
file: internal/docrevision/pg_store.go
symbol: document_records
owner: docrevision
reason: projectRootPointersTX updates controlled root pointers; U3 must keep this inside the shared UnitOfWork
milestone: U3
expires: "2026-09-28"
- kind: sql_write
package: docrevision
file: internal/docrevision/pg_store.go
symbol: freeform_documents
owner: docrevision
reason: projectRootPointersTX also projects freeform heads; keep atomic in U3 then hand write ownership back
milestone: U3
expires: "2026-09-28"
- kind: sql_write
package: projectinstance
file: internal/projectinstance/store.go
symbol: task_runs
owner: projectinstance
reason: workflow terminate cancels task_runs; U8 owns run finalization in the same UoW
milestone: U8
expires: "2026-10-26"
- kind: path
package: agent
file: internal/agent/cursor.go
symbol: output.md
owner: agent
reason: runner still names the deliverable file; U5 replaces this with WorkspaceRef
milestone: U5
expires: "2026-10-12"
- kind: path
package: agent
file: internal/agent/piagent.go
symbol: output.md
owner: agent
reason: PI runner contract still uses output.md; U5 capability must hide the literal
milestone: U5
expires: "2026-10-12"
- kind: path
package: bizdev
file: internal/bizdev/service.go
symbol: data/dev
owner: bizdev
reason: default data-root example string; U5 resolver owns environment roots
milestone: U5
expires: "2026-10-12"
- kind: path
package: bizdev
file: internal/bizdev/service.go
symbol: output.md
owner: bizdev
reason: orchestration still joins workspace output.md; U5 removes Root/OutputDir
milestone: U5
expires: "2026-10-12"
- kind: path
package: coordinator
file: internal/coordinator/context.go
symbol: output.md
owner: coordinator
reason: coordinator reads projection path; U4/U5 must go through workspace capability
milestone: U5
expires: "2026-10-12"
- kind: path
package: coordinator
file: internal/coordinator/dialogue_llm.go
symbol: data/dev
owner: coordinator
reason: prompt example embeds data/dev; U5 must not leak runtime roots
milestone: U5
expires: "2026-10-12"
- kind: path
package: coordinator
file: internal/coordinator/document_summary.go
symbol: data/dev
owner: coordinator
reason: summary prompt embeds data/dev; U5 must not leak runtime roots
milestone: U5
expires: "2026-10-12"
- kind: path
package: coordinator
file: internal/coordinator/engine.go
symbol: output.md
owner: coordinator
reason: engine joins deliverable path; U5 WorkspaceRef
milestone: U5
expires: "2026-10-12"
- kind: path
package: docgov
file: internal/docgov/lease.go
symbol: output.md
owner: docgov
reason: SaveDocumentContent still writes projection files; U5 capability + U4 ledger-first read
milestone: U5
expires: "2026-10-12"
- kind: path
package: docgov
file: internal/docgov/project_docs.go
symbol: output.md
owner: docgov
reason: project doc helpers still construct output.md; U5
milestone: U5
expires: "2026-10-12"
- kind: path
package: docrevision
file: internal/docrevision/projection.go
symbol: output.md
owner: docrevision
reason: InspectProjection reads workspace file; U4 treats this as optional cache only
milestone: U4
expires: "2026-09-28"
- kind: path
package: docrevision
file: internal/docrevision/reconcile.go
symbol: output.md
owner: docrevision
reason: reconcile still repairs projection files; U5
milestone: U5
expires: "2026-10-12"
- kind: path
package: server
file: internal/server/document_revision_access.go
symbol: output.md
owner: server
reason: handler still reads workspace files; U4 must use docquery ledger
milestone: U4
expires: "2026-09-28"
- kind: path
package: server
file: internal/server/document_revision_handlers.go
symbol: output.md
owner: server
reason: handler error text mentions output.md; U4/U5 remove path leakage
milestone: U4
expires: "2026-09-28"
- kind: path
package: server
file: internal/server/server.go
symbol: workspaces/
owner: server
reason: server still concatenates workspaces/ roots; U5
milestone: U5
expires: "2026-10-12"
- kind: path
package: server
file: internal/server/task_finalize.go
symbol: output.md
owner: server
reason: finalize still joins output.md; U8 verified receipt
milestone: U8
expires: "2026-10-26"
- kind: path
package: storage
file: internal/storage/cas.go
symbol: data/dev
owner: storage
reason: CAS default example path; U5 resolver
milestone: U5
expires: "2026-10-12"
baseline_counts:
import: 0
sql_write: 5
path: 17
event: 0
+3 -3
View File
@@ -4,9 +4,12 @@ go 1.27.1
require ( require (
github.com/jackc/pgx/v5 v5.10.0 github.com/jackc/pgx/v5 v5.10.0
github.com/pressly/goose/v3 v3.24.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/xuri/excelize/v2 v2.11.0 github.com/xuri/excelize/v2 v2.11.0
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.53.0
golang.org/x/text v0.42.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
@@ -17,7 +20,6 @@ require (
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect github.com/mfridman/interpolate v0.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pressly/goose/v3 v3.24.1 // indirect
github.com/richardlehane/mscfb v1.0.7 // indirect github.com/richardlehane/mscfb v1.0.7 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect
github.com/rogpeppe/go-internal v1.16.0 // indirect github.com/rogpeppe/go-internal v1.16.0 // indirect
@@ -28,6 +30,4 @@ require (
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.23.0 // indirect golang.org/x/sync v0.23.0 // indirect
golang.org/x/text v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+28 -4
View File
@@ -2,6 +2,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -14,12 +20,18 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY= github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY=
github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
@@ -49,12 +61,10 @@ golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -63,3 +73,17 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk=
modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+32
View File
@@ -0,0 +1,32 @@
package archtest
import (
"os"
"testing"
"time"
)
func TestArchitectureBoundaries(t *testing.T) {
root, err := ModuleRoot()
if err != nil {
t.Fatal(err)
}
b, err := LoadBoundaries(DefaultBoundariesPath(root))
if err != nil {
t.Fatal(err)
}
findings, err := Scan(ScanOptions{Root: root}, b)
if err != nil {
t.Fatal(err)
}
ev := Evaluate(findings, b, time.Now())
if os.Getenv("ARCHTEST_DUMP") != "" {
t.Log(ev.Report())
}
if ev.Failed() {
t.Fatal(ev.Report())
}
if os.Getenv("ARCHTEST_STRICT") != "" && len(ev.Excepted) > 0 {
t.Fatalf("strict/block mode: %d baseline exceptions remain\n%s", len(ev.Excepted), ev.Report())
}
}
+166
View File
@@ -0,0 +1,166 @@
package archtest
import (
"path/filepath"
"runtime"
"testing"
"time"
)
func miniRoot(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("caller")
}
return filepath.Join(filepath.Dir(file), "testdata", "mini")
}
func fixtureBoundaries() *Boundaries {
return &Boundaries{
Imports: map[string][]string{
"server": {},
"docgov": {},
},
TableOwners: map[string][]string{
"document_records": {"docgov"},
},
PathOwners: map[string][]string{
"output.md": {"squad", "workspace"},
},
EventProducers: map[string][]string{
"outbox.notify": {"docrevision"},
"document.agent_version_committed": {"docrevision"},
},
EventConsumers: map[string][]string{
"outbox.notify": {"notification"},
},
BaselineCounts: map[string]int{},
}
}
func scanMini(t *testing.T) []Finding {
t.Helper()
findings, err := Scan(ScanOptions{Root: miniRoot(t)}, fixtureBoundaries())
if err != nil {
t.Fatal(err)
}
return findings
}
func TestGooseAndTestsAreNotProductionWriters(t *testing.T) {
for _, f := range scanMini(t) {
if f.Kind == KindSQLWrite && f.Package == "docgov" && f.File == "internal/docgov/owner_write_test.go" {
t.Fatalf("test file must not be a production writer: %+v", f)
}
if f.Kind == KindSQLWrite && filepath.Ext(f.File) == ".sql" {
t.Fatalf("goose/sql must not be a production writer: %+v", f)
}
}
}
func TestNonOwnerProductionWriteIsBlockedAndNamed(t *testing.T) {
var hit *Finding
findings := scanMini(t)
for i, f := range findings {
if f.Kind == KindSQLWrite && f.Symbol == "document_records" && f.Package == "server" {
hit = &findings[i]
if f.File != "internal/server/rogue_write.go" {
t.Fatalf("expected rogue_write.go, got %s", f.File)
}
}
}
if hit == nil {
t.Fatal("expected UPDATE document_records in internal/server/rogue_write.go to be blocked")
}
}
func TestOwnerProductionWriteIsAllowed(t *testing.T) {
for _, f := range scanMini(t) {
if f.Kind == KindSQLWrite && f.Package == "docgov" && f.File == "internal/docgov/owner_write.go" {
t.Fatalf("owner write should be allowed: %+v", f)
}
}
}
func TestPathJoinConcatAndConstAreDetected(t *testing.T) {
want := map[string]bool{
"internal/server/path_join.go": false,
"internal/server/path_concat.go": false,
"internal/server/path_const.go": false,
}
for _, f := range scanMini(t) {
if f.Kind != KindPath || f.Symbol != "output.md" {
continue
}
if _, ok := want[f.File]; ok {
want[f.File] = true
}
}
for file, seen := range want {
if !seen {
t.Errorf("path construction not detected in %s", file)
}
}
}
func TestUnauthorizedEventProducerIsBlocked(t *testing.T) {
found := false
for _, f := range scanMini(t) {
if f.Kind == KindEvent && f.Package == "server" {
found = true
}
}
if !found {
t.Fatal("expected unauthorized OnOutbox producer in server to be blocked")
}
}
func TestExceptionRequiresExpiryAndOwner(t *testing.T) {
b := fixtureBoundaries()
b.Exceptions = []Exception{{
Kind: KindSQLWrite, Package: "server", File: "internal/server/rogue_write.go",
Symbol: "document_records", Reason: "no owner or expiry",
}}
ev := Evaluate(scanMini(t), b, time.Date(2026, 9, 14, 12, 0, 0, 0, time.UTC))
if len(ev.BadExceptions) == 0 {
t.Fatal("expected missing owner/expires to block")
}
if len(ev.Unmatched) == 0 {
t.Fatal("invalid exception must not hide the finding")
}
}
func TestExpiredExceptionBlocksImmediately(t *testing.T) {
b := fixtureBoundaries()
b.Exceptions = []Exception{{
Kind: KindSQLWrite, Package: "server", File: "internal/server/rogue_write.go",
Symbol: "document_records", Owner: "server", Reason: "stale",
Milestone: "U3", Expires: "2026-09-01",
}}
ev := Evaluate(scanMini(t), b, time.Date(2026, 9, 14, 12, 0, 0, 0, time.UTC))
if len(ev.BadExceptions) == 0 {
t.Fatal("expected expired exception to block")
}
found := false
for _, f := range ev.Unmatched {
if f.File == "internal/server/rogue_write.go" {
found = true
}
}
if !found {
t.Fatal("expired exception must surface the original finding")
}
}
func TestClassifyGooseSQL(t *testing.T) {
root := miniRoot(t)
got := classifyFile(root, filepath.Join(root, "deploy", "postgres", "goose", "00001_fixture.sql"))
if got != classGoose {
t.Fatalf("classify goose: got %v want %v", got, classGoose)
}
got = classifyFile(root, filepath.Join(root, "internal", "docgov", "owner_write_test.go"))
if got != classTestOrFixture {
t.Fatalf("classify test: got %v want %v", got, classTestOrFixture)
}
}
+153
View File
@@ -0,0 +1,153 @@
package archtest
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"gopkg.in/yaml.v3"
)
const (
KindImport = "import"
KindSQLWrite = "sql_write"
KindPath = "path"
KindEvent = "event"
)
type Boundaries struct {
Imports map[string][]string `yaml:"imports"`
TableOwners map[string][]string `yaml:"table_owners"`
PathOwners map[string][]string `yaml:"path_owners"`
EventProducers map[string][]string `yaml:"event_producers"`
EventConsumers map[string][]string `yaml:"event_consumers"`
Exceptions []Exception `yaml:"exceptions"`
BaselineCounts map[string]int `yaml:"baseline_counts"`
}
type Exception struct {
Kind string `yaml:"kind"`
Package string `yaml:"package"`
File string `yaml:"file"`
Symbol string `yaml:"symbol"`
Owner string `yaml:"owner"`
Reason string `yaml:"reason"`
Milestone string `yaml:"milestone"`
Expires string `yaml:"expires"`
}
type Finding struct {
Kind string
Package string
File string
Symbol string
Detail string
}
func (f Finding) Key() string {
return f.Kind + "|" + f.Package + "|" + filepath.ToSlash(f.File) + "|" + f.Symbol
}
func ModuleRoot() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("archtest: cannot locate caller")
}
dir := filepath.Dir(file)
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", fmt.Errorf("archtest: go.mod not found")
}
dir = parent
}
}
func LoadBoundaries(path string) (*Boundaries, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var out Boundaries
if err := yaml.Unmarshal(b, &out); err != nil {
return nil, err
}
if out.Imports == nil {
out.Imports = map[string][]string{}
}
if out.TableOwners == nil {
out.TableOwners = map[string][]string{}
}
if out.PathOwners == nil {
out.PathOwners = map[string][]string{}
}
if out.EventProducers == nil {
out.EventProducers = map[string][]string{}
}
if out.EventConsumers == nil {
out.EventConsumers = map[string][]string{}
}
if out.BaselineCounts == nil {
out.BaselineCounts = map[string]int{}
}
return &out, nil
}
func DefaultBoundariesPath(root string) string {
return filepath.Join(root, ".doc", "system-design", "module-boundaries.yaml")
}
func allowedHas(list []string, needle string) bool {
for _, p := range list {
if strings.TrimSpace(p) == needle {
return true
}
}
return false
}
func (e Exception) validate(now time.Time) error {
if strings.TrimSpace(e.Owner) == "" {
return fmt.Errorf("missing owner")
}
if strings.TrimSpace(e.Reason) == "" {
return fmt.Errorf("missing reason")
}
if strings.TrimSpace(e.Milestone) == "" {
return fmt.Errorf("missing milestone")
}
if strings.TrimSpace(e.Expires) == "" {
return fmt.Errorf("missing expires")
}
day, err := time.ParseInLocation("2006-01-02", e.Expires, time.UTC)
if err != nil {
return fmt.Errorf("invalid expires %q", e.Expires)
}
// expires is inclusive through that calendar day (UTC).
if !now.UTC().Before(day.Add(24 * time.Hour)) {
return fmt.Errorf("expired on %s", e.Expires)
}
return nil
}
func (e Exception) matches(f Finding) bool {
if e.Kind != f.Kind {
return false
}
if e.Package != "" && e.Package != f.Package {
return false
}
if e.File != "" && filepath.ToSlash(e.File) != filepath.ToSlash(f.File) {
return false
}
if e.Symbol != "" && e.Symbol != f.Symbol {
return false
}
return true
}
+82
View File
@@ -0,0 +1,82 @@
package archtest
import (
"fmt"
"sort"
"strings"
"time"
)
type Evaluation struct {
Unmatched []Finding
Excepted []Finding
BadExceptions []string
Counts map[string]int
CountRegressed []string
}
func Evaluate(findings []Finding, b *Boundaries, now time.Time) Evaluation {
ev := Evaluation{Counts: map[string]int{}}
used := make([]bool, len(b.Exceptions))
for i, ex := range b.Exceptions {
if err := ex.validate(now); err != nil {
ev.BadExceptions = append(ev.BadExceptions, fmt.Sprintf("exception[%d] %s %s %s: %v", i, ex.Kind, ex.File, ex.Symbol, err))
}
}
for _, f := range findings {
ev.Counts[f.Kind]++
matched := false
for i, ex := range b.Exceptions {
if !ex.matches(f) {
continue
}
if err := ex.validate(now); err != nil {
continue
}
used[i] = true
matched = true
ev.Excepted = append(ev.Excepted, f)
break
}
if !matched {
ev.Unmatched = append(ev.Unmatched, f)
}
}
sort.Slice(ev.Unmatched, func(i, j int) bool { return ev.Unmatched[i].Key() < ev.Unmatched[j].Key() })
sort.Slice(ev.Excepted, func(i, j int) bool { return ev.Excepted[i].Key() < ev.Excepted[j].Key() })
for kind, baseline := range b.BaselineCounts {
if ev.Counts[kind] > baseline {
ev.CountRegressed = append(ev.CountRegressed, fmt.Sprintf("%s: %d > baseline %d", kind, ev.Counts[kind], baseline))
}
}
sort.Strings(ev.CountRegressed)
return ev
}
func (ev Evaluation) Failed() bool {
return len(ev.Unmatched) > 0 || len(ev.BadExceptions) > 0 || len(ev.CountRegressed) > 0
}
func (ev Evaluation) Report() string {
var b strings.Builder
fmt.Fprintf(&b, "archtest findings=%d excepted=%d unmatched=%d\n",
ev.Counts[KindImport]+ev.Counts[KindSQLWrite]+ev.Counts[KindPath]+ev.Counts[KindEvent],
len(ev.Excepted), len(ev.Unmatched))
for _, kind := range []string{KindImport, KindSQLWrite, KindPath, KindEvent} {
fmt.Fprintf(&b, " %s=%d\n", kind, ev.Counts[kind])
}
for _, msg := range ev.BadExceptions {
fmt.Fprintf(&b, "BAD_EXCEPTION %s\n", msg)
}
for _, msg := range ev.CountRegressed {
fmt.Fprintf(&b, "REGRESSION %s\n", msg)
}
for _, f := range ev.Unmatched {
fmt.Fprintf(&b, "NEW %s %s %s %s (%s)\n", f.Kind, f.Package, f.File, f.Symbol, f.Detail)
}
for _, f := range ev.Excepted {
fmt.Fprintf(&b, "BASELINE %s %s %s %s (%s)\n", f.Kind, f.Package, f.File, f.Symbol, f.Detail)
}
return b.String()
}
+424
View File
@@ -0,0 +1,424 @@
package archtest
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
)
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_.]+`)
)
type fileClass int
const (
classIgnore fileClass = iota
classProduction
classTestOrFixture
classGoose
)
type ScanOptions struct {
Root string
// ExtraRoots are additional trees scanned as production Go (used by self-tests).
ExtraRoots []string
// ExtraSQL are extra files classified as goose/fixture SQL.
ExtraSQL []string
}
func Scan(opts ScanOptions, b *Boundaries) ([]Finding, error) {
var findings []Finding
roots := append([]string{filepath.Join(opts.Root, "internal")}, opts.ExtraRoots...)
fset := token.NewFileSet()
for _, root := range roots {
if root == "" {
continue
}
if _, err := os.Stat(root); err != nil {
continue
}
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
name := d.Name()
if name == "testdata" || name == "vendor" || name == ".git" {
return filepath.SkipDir
}
return nil
}
class := classifyFile(opts.Root, path)
if strings.HasSuffix(path, ".go") {
switch class {
case classProduction:
fs, perr := scanProductionGo(fset, opts.Root, path, b)
if perr != nil {
return perr
}
findings = append(findings, fs...)
case classTestOrFixture, classGoose, classIgnore:
// Goose / tests / fixtures may mention tables and paths; they are not production writers.
}
}
return nil
})
if err != nil {
return nil, err
}
}
for _, sqlPath := range opts.ExtraSQL {
// Explicit proof that goose SQL is classified away from production writes.
if classifyFile(opts.Root, sqlPath) != classGoose && !strings.Contains(filepath.ToSlash(sqlPath), "goose") {
continue
}
}
return dedupe(findings), nil
}
func classifyFile(repoRoot, path string) fileClass {
rel := path
if repoRoot != "" {
if r, err := filepath.Rel(repoRoot, path); err == nil {
rel = r
}
}
slash := filepath.ToSlash(rel)
base := filepath.Base(path)
if strings.Contains(slash, "/testdata/") || strings.HasPrefix(slash, "testdata/") {
if strings.Contains(slash, "/goose/") || strings.HasPrefix(base, "000") && strings.HasSuffix(base, ".sql") {
return classGoose
}
if strings.HasSuffix(base, "_test.go") || strings.Contains(slash, "/fixture") || strings.Contains(slash, "/fixtures/") {
if strings.HasSuffix(base, ".go") && !strings.HasSuffix(base, "_test.go") && strings.Contains(slash, "/prod/") {
return classProduction
}
return classTestOrFixture
}
}
if strings.HasPrefix(slash, "deploy/postgres/goose/") || strings.Contains(slash, "/postgres/goose/") {
return classGoose
}
if strings.HasSuffix(base, "_test.go") {
return classTestOrFixture
}
if strings.HasSuffix(base, ".go") && strings.HasPrefix(slash, "internal/") {
if strings.HasPrefix(slash, "internal/archtest/") {
return classIgnore
}
return classProduction
}
if strings.HasSuffix(base, ".go") && (strings.Contains(slash, "/prod/") || strings.Contains(slash, "fixtures/prod/")) {
return classProduction
}
return classIgnore
}
func scanProductionGo(fset *token.FileSet, repoRoot, path string, b *Boundaries) ([]Finding, error) {
src, err := os.ReadFile(path)
if err != nil {
return nil, err
}
file, err := parser.ParseFile(fset, path, src, parser.SkipObjectResolution)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
pkg := packageID(repoRoot, path)
rel := relFile(repoRoot, path)
var out []Finding
out = append(out, scanImports(file, pkg, rel, b)...)
out = append(out, scanSQLWrites(string(src), pkg, rel, b)...)
out = append(out, scanPaths(file, pkg, rel, b)...)
out = append(out, scanEvents(file, string(src), pkg, rel, b)...)
return out, nil
}
func packageID(repoRoot, path string) string {
rel := relFile(repoRoot, path)
slash := filepath.ToSlash(rel)
slash = strings.TrimPrefix(slash, "internal/")
dir := filepath.ToSlash(filepath.Dir(slash))
if dir == "." {
return filepath.Base(filepath.Dir(path))
}
return dir
}
func relFile(repoRoot, path string) string {
if repoRoot == "" {
return filepath.ToSlash(path)
}
rel, err := filepath.Rel(repoRoot, path)
if err != nil {
return filepath.ToSlash(path)
}
return filepath.ToSlash(rel)
}
func scanImports(file *ast.File, pkg, rel string, b *Boundaries) []Finding {
allowed, ok := b.Imports[pkg]
if !ok {
// Unknown production package is itself a new import-graph node.
return []Finding{{Kind: KindImport, Package: pkg, File: rel, Symbol: pkg, Detail: "package missing from import allowlist"}}
}
var out []Finding
for _, spec := range file.Imports {
path := strings.Trim(spec.Path.Value, `"`)
if !strings.HasPrefix(path, internalPrefix) {
continue
}
dep := strings.TrimPrefix(path, internalPrefix)
if dep == pkg {
continue
}
if !allowedHas(allowed, dep) {
out = append(out, Finding{
Kind: KindImport,
Package: pkg,
File: rel,
Symbol: dep,
Detail: fmt.Sprintf("production import %s -> %s", pkg, dep),
})
}
}
return out
}
func scanSQLWrites(src, pkg, rel string, b *Boundaries) []Finding {
var out []Finding
for _, m := range sqlWriteRe.FindAllStringSubmatch(src, -1) {
table := strings.ToLower(m[2])
owners, tracked := b.TableOwners[table]
if !tracked {
continue
}
if allowedHas(owners, pkg) {
continue
}
out = append(out, Finding{
Kind: KindSQLWrite,
Package: pkg,
File: rel,
Symbol: table,
Detail: fmt.Sprintf("%s writes %s", strings.ToLower(strings.Join(strings.Fields(m[1]), " ")), table),
})
}
return out
}
func scanPaths(file *ast.File, pkg, rel string, b *Boundaries) []Finding {
consts := map[string]string{}
ast.Inspect(file, func(n ast.Node) bool {
vs, ok := n.(*ast.ValueSpec)
if !ok {
return true
}
for i, name := range vs.Names {
if i < len(vs.Values) {
if lit, ok := vs.Values[i].(*ast.BasicLit); ok && lit.Kind == token.STRING {
consts[name.Name] = strings.Trim(lit.Value, `"`)
}
}
}
return true
})
var out []Finding
seen := map[string]bool{}
add := func(symbol, detail string) {
key := symbol + "|" + detail
if seen[key] {
return
}
seen[key] = true
owners, tracked := b.PathOwners[symbol]
if !tracked {
return
}
if allowedHas(owners, pkg) {
return
}
out = append(out, Finding{
Kind: KindPath,
Package: pkg,
File: rel,
Symbol: symbol,
Detail: detail,
})
}
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.BasicLit:
if x.Kind != token.STRING {
return true
}
val := strings.Trim(x.Value, `"`)
for lit := range b.PathOwners {
if pathLiteralHit(val, lit) {
add(lit, "string literal "+lit)
}
}
case *ast.Ident:
if val, ok := consts[x.Name]; ok {
for lit := range b.PathOwners {
if pathLiteralHit(val, lit) {
add(lit, "indirect const "+x.Name)
}
}
}
case *ast.CallExpr:
if !isJoinLike(x.Fun) {
return true
}
for _, arg := range x.Args {
switch a := arg.(type) {
case *ast.BasicLit:
if a.Kind == token.STRING {
val := strings.Trim(a.Value, `"`)
for lit := range b.PathOwners {
if pathLiteralHit(val, lit) {
add(lit, "filepath.Join/path.Join")
}
}
}
case *ast.Ident:
if val, ok := consts[a.Name]; ok {
for lit := range b.PathOwners {
if pathLiteralHit(val, lit) {
add(lit, "filepath.Join via const "+a.Name)
}
}
}
}
}
case *ast.BinaryExpr:
if x.Op != token.ADD {
return true
}
vals := []string{litOrConst(x.X, consts), litOrConst(x.Y, consts)}
for _, val := range vals {
for lit := range b.PathOwners {
if pathLiteralHit(val, lit) {
add(lit, "string concatenation")
}
}
}
}
return true
})
return out
}
func litOrConst(e ast.Expr, consts map[string]string) string {
switch x := e.(type) {
case *ast.BasicLit:
if x.Kind == token.STRING {
return strings.Trim(x.Value, `"`)
}
case *ast.Ident:
return consts[x.Name]
}
return ""
}
func isJoinLike(fun ast.Expr) bool {
sel, ok := fun.(*ast.SelectorExpr)
if !ok {
return false
}
pkg, ok := sel.X.(*ast.Ident)
if !ok {
return false
}
if sel.Sel.Name != "Join" && sel.Sel.Name != "FromSlash" {
return false
}
return pkg.Name == "filepath" || pkg.Name == "path"
}
func pathLiteralHit(val, lit string) bool {
if val == "" || lit == "" {
return false
}
if val == lit {
return true
}
return strings.Contains(val, lit)
}
func scanEvents(file *ast.File, src, pkg, rel string, b *Boundaries) []Finding {
var out []Finding
seen := map[string]bool{}
add := func(symbol, role, detail string) {
key := symbol + "|" + role
if seen[key] {
return
}
seen[key] = true
var owners []string
var tracked bool
if role == "producer" {
owners, tracked = b.EventProducers[symbol]
} else {
owners, tracked = b.EventConsumers[symbol]
}
if !tracked {
return
}
if allowedHas(owners, pkg) {
return
}
out = append(out, Finding{
Kind: KindEvent,
Package: pkg,
File: rel,
Symbol: symbol,
Detail: detail,
})
}
if strings.Contains(src, "OnOutbox") {
isAssign := strings.Contains(src, "OnOutbox =")
isCallOrField := strings.Contains(src, "OnOutbox(") || strings.Contains(src, "OnOutbox func(")
if isCallOrField && !isAssign {
add("outbox.notify", "producer", "OnOutbox hook")
}
if isAssign {
add("outbox.notify", "consumer", "OnOutbox assignment")
}
}
for _, name := range eventNameRe.FindAllString(src, -1) {
if _, ok := b.EventProducers[name]; ok {
if allowedHas(b.EventConsumers[name], pkg) || allowedHas(b.EventConsumers["outbox.notify"], pkg) {
continue
}
add(name, "producer", "event name literal")
}
}
_ = file
return out
}
func dedupe(in []Finding) []Finding {
seen := map[string]bool{}
var out []Finding
for _, f := range in {
k := f.Key()
if seen[k] {
continue
}
seen[k] = true
out = append(out, f)
}
return out
}
@@ -0,0 +1,4 @@
-- Goose may mention owned tables; this is not a production writer.
UPDATE document_records SET current_state = 'approved' WHERE doc_id = 'fixture';
INSERT INTO document_records (doc_id) VALUES ('fixture');
DELETE FROM document_records WHERE doc_id = 'fixture';
@@ -0,0 +1,5 @@
package docgov
const ownerSQL = `
UPDATE document_records SET current_state = 'approved' WHERE doc_id = $1
`
@@ -0,0 +1,5 @@
package docgov
const testSQL = `
UPDATE document_records SET current_state = 'test' WHERE doc_id = $1
`
@@ -0,0 +1,7 @@
package server
func emit() {
_ = OnOutbox("document.agent_version_committed")
}
func OnOutbox(eventType string) error { return nil }
@@ -0,0 +1,5 @@
package server
func concatOutput(dir string) string {
return dir + "/output.md"
}
@@ -0,0 +1,9 @@
package server
import "path/filepath"
const deliverableName = "output.md"
func constOutput(dir string) string {
return filepath.Join(dir, deliverableName)
}
@@ -0,0 +1,7 @@
package server
import "path/filepath"
func joinOutput(dir string) string {
return filepath.Join(dir, "output.md")
}
@@ -0,0 +1,5 @@
package server
const rogueSQL = `
UPDATE document_records SET current_state = 'hacked' WHERE doc_id = $1
`
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Architecture boundary check (U1).
# Production import / SQL write / path construction / event producer-consumer.
# Goose, *_test.go and fixtures are not production writers.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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
cd "${ROOT}"
echo "arch-check mode=${MODE}"
echo "boundaries=${ROOT}/.doc/system-design/module-boundaries.yaml"
if ! go test ./internal/archtest -count=1; then
echo "arch-check: new, expired, or unowned findings failed go test ./internal/archtest" >&2
exit 1
fi
if [[ "${MODE}" == "block" ]]; then
if ! ARCHTEST_STRICT=1 go test ./internal/archtest -count=1 -run TestArchitectureBoundaries; then
echo "arch-check block: baseline exceptions remain; only cleared rules may use --mode block" >&2
exit 1
fi
fi
echo "arch-check ${MODE}: production gate passed"