@@ -38,6 +38,23 @@ func TestArchitecture_NoForbiddenExternalImports(t *testing.T) {
3838 }
3939}
4040
41+ func TestArchitecture_NoJSONMetadataInTextParts (t * testing.T ) {
42+ root , err := os .Getwd ()
43+ if err != nil {
44+ t .Fatalf ("getwd: %v" , err )
45+ }
46+ violations , err := findJSONMetadataInTextViolations (root )
47+ if err != nil {
48+ t .Fatalf ("scan: %v" , err )
49+ }
50+ if len (violations ) > 0 {
51+ t .Fatalf (
52+ "json metadata tunneling via TextContent forbidden in semantic core:\n %s" ,
53+ strings .Join (violations , "\n " ),
54+ )
55+ }
56+ }
57+
4158func TestArchitecture_NoBase64InCore (t * testing.T ) {
4259 root , err := os .Getwd ()
4360 if err != nil {
@@ -93,13 +110,84 @@ func isArchitectureSourceFile(path string) bool {
93110
94111func isArchitectureAllowlisted (path string ) bool {
95112 switch filepath .Base (path ) {
96- case "transform.go" , "views.go" , "model.go" :
113+ case "transform.go" , "views.go" , "model.go" , "provenance.go" , "serializer.go" , "content_part.go" :
97114 return true
98115 default :
99116 return false
100117 }
101118}
102119
120+ func findJSONMetadataInTextViolations (root string ) ([]string , error ) {
121+ fset := token .NewFileSet ()
122+ var violations []string
123+ err := filepath .Walk (root , func (path string , info os.FileInfo , walkErr error ) error {
124+ if walkErr != nil {
125+ return walkErr
126+ }
127+ if info .IsDir () {
128+ if shouldSkipArchitectureDir (filepath .Base (path )) {
129+ return filepath .SkipDir
130+ }
131+ return nil
132+ }
133+ if ! isArchitectureSourceFile (path ) || isArchitectureAllowlisted (path ) {
134+ return nil
135+ }
136+ file , parseErr := parser .ParseFile (fset , path , nil , 0 )
137+ if parseErr != nil {
138+ return parseErr
139+ }
140+ violations = append (violations , collectJSONMetadataInTextViolations (fset , file )... )
141+ return nil
142+ })
143+ return violations , err
144+ }
145+
146+ func collectJSONMetadataInTextViolations (fset * token.FileSet , file * ast.File ) []string {
147+ var violations []string
148+ ast .Inspect (file , func (n ast.Node ) bool {
149+ call , ok := n .(* ast.CallExpr )
150+ if ! ok || len (call .Args ) == 0 {
151+ return true
152+ }
153+ if ! isJSONUnmarshalCall (call ) {
154+ return true
155+ }
156+ if ! exprReferencesTextContent (call .Args [0 ]) {
157+ return true
158+ }
159+ pos := fset .Position (call .Pos ())
160+ violations = append (violations , pos .String ())
161+ return true
162+ })
163+ return violations
164+ }
165+
166+ func isJSONUnmarshalCall (call * ast.CallExpr ) bool {
167+ sel , ok := call .Fun .(* ast.SelectorExpr )
168+ if ! ok {
169+ return false
170+ }
171+ pkg , ok := sel .X .(* ast.Ident )
172+ return ok && pkg .Name == "json" && sel .Sel .Name == "Unmarshal"
173+ }
174+
175+ func exprReferencesTextContent (expr ast.Expr ) bool {
176+ found := false
177+ ast .Inspect (expr , func (n ast.Node ) bool {
178+ sel , ok := n .(* ast.SelectorExpr )
179+ if ! ok {
180+ return true
181+ }
182+ if sel .Sel .Name == "TextContent" {
183+ found = true
184+ return false
185+ }
186+ return true
187+ })
188+ return found
189+ }
190+
103191func collectStringHeuristicCalls (fset * token.FileSet , file * ast.File ) []string {
104192 var violations []string
105193 ast .Inspect (file , func (n ast.Node ) bool {
0 commit comments