ttsc 0.18.4 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +5 -6
  2. package/cmd/graphbench/main.go +1 -1
  3. package/cmd/platform/main.go +1 -48
  4. package/cmd/ttsc/build_branches_test.go +5 -1
  5. package/cmd/ttsc/main.go +0 -50
  6. package/cmd/ttsc/main_alias_branches_test.go +6 -12
  7. package/cmd/ttscgraph/dump.go +13 -3
  8. package/cmd/ttscserver/main.go +5 -3
  9. package/driver/lsp.go +13 -1
  10. package/driver/plugins.go +35 -0
  11. package/driver/program.go +7 -8
  12. package/driver/rewrite.go +381 -21
  13. package/go.mod +1 -0
  14. package/internal/graph/build.go +297 -3
  15. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
  16. package/internal/graph/diagnostics.go +5 -4
  17. package/internal/graph/dump.go +60 -4
  18. package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
  20. package/internal/graph/edges.go +122 -11
  21. package/internal/graph/exports.go +74 -17
  22. package/internal/graph/graph.go +33 -1
  23. package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
  24. package/internal/graph/resolve.go +24 -0
  25. package/internal/graphsymbols/provider.go +50 -14
  26. package/internal/lspserver/lsp_native_plugin_source.go +77 -23
  27. package/internal/lspserver/lsp_plugin.go +19 -2
  28. package/internal/lspserver/lsp_proxy.go +244 -52
  29. package/internal/lspserver/lsp_server.go +35 -27
  30. package/internal/lspserver/lsp_symbols.go +25 -16
  31. package/lib/TtscCompiler.d.ts +10 -0
  32. package/lib/TtscCompiler.js +24 -6
  33. package/lib/TtscCompiler.js.map +1 -1
  34. package/lib/TtscService.js +9 -7
  35. package/lib/TtscService.js.map +1 -1
  36. package/lib/compiler/internal/compileProjectInMemory.js +1 -0
  37. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  38. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
  39. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
  40. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
  41. package/lib/compiler/internal/project/readProjectConfig.js +53 -4
  42. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
  44. package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
  45. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  46. package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
  47. package/lib/compiler/internal/residentTransformProcess.js +68 -20
  48. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  49. package/lib/compiler/internal/runBuild.d.ts +11 -0
  50. package/lib/compiler/internal/runBuild.js +44 -8
  51. package/lib/compiler/internal/runBuild.js.map +1 -1
  52. package/lib/compiler/internal/runSingleFileEmit.js +7 -4
  53. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  54. package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
  55. package/lib/compiler/internal/sharedHostHelpers.js +23 -0
  56. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  57. package/lib/compiler/internal/startResidentTransform.js +18 -1
  58. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  59. package/lib/compiler/internal/transformProjectInMemory.js +34 -8
  60. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  61. package/lib/flags/parser.d.ts +17 -0
  62. package/lib/flags/parser.js +11 -1
  63. package/lib/flags/parser.js.map +1 -1
  64. package/lib/flags/schema.js +7 -0
  65. package/lib/flags/schema.js.map +1 -1
  66. package/lib/launcher/internal/prepareExecution.js +21 -2
  67. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  68. package/lib/launcher/internal/runTtsc.js +13 -53
  69. package/lib/launcher/internal/runTtsc.js.map +1 -1
  70. package/lib/launcher/internal/runTtscserver.js +11 -0
  71. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  72. package/lib/launcher/internal/runTtsx.js +27 -11
  73. package/lib/launcher/internal/runTtsx.js.map +1 -1
  74. package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
  75. package/lib/launcher/internal/runtimeHooks.js +632 -43
  76. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  77. package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
  78. package/lib/launcher/internal/servedSourceMap.js +164 -0
  79. package/lib/launcher/internal/servedSourceMap.js.map +1 -0
  80. package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
  81. package/lib/plugin/internal/buildSourcePlugin.js +651 -113
  82. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  83. package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
  84. package/lib/plugin/internal/loadProjectPlugins.js +33 -10
  85. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  86. package/lib/structures/ITtscCompilerContext.d.ts +16 -2
  87. package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
  88. package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
  89. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
  90. package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
  91. package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
  92. package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
  93. package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
  94. package/package.json +11 -8
  95. package/shim/ast/lint.go +10 -0
  96. package/shim/ast/shim.go +44 -0
  97. package/shim/checker/enums_gen.go +19 -0
  98. package/shim/checker/shim.go +130 -0
  99. package/shim/diagnosticwriter/lint.go +30 -3
  100. package/shim/scanner/regexp.go +27 -0
  101. package/src/TtscCompiler.ts +25 -6
  102. package/src/TtscService.ts +15 -7
  103. package/src/compiler/internal/compileProjectInMemory.ts +1 -0
  104. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
  105. package/src/compiler/internal/project/readProjectConfig.ts +61 -5
  106. package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
  107. package/src/compiler/internal/residentTransformProcess.ts +95 -21
  108. package/src/compiler/internal/runBuild.ts +76 -9
  109. package/src/compiler/internal/runSingleFileEmit.ts +7 -4
  110. package/src/compiler/internal/sharedHostHelpers.ts +24 -0
  111. package/src/compiler/internal/startResidentTransform.ts +21 -1
  112. package/src/compiler/internal/transformProjectInMemory.ts +49 -6
  113. package/src/flags/parser.ts +28 -1
  114. package/src/flags/schema.ts +8 -0
  115. package/src/launcher/internal/prepareExecution.ts +22 -2
  116. package/src/launcher/internal/runTtsc.ts +13 -61
  117. package/src/launcher/internal/runTtscserver.ts +14 -0
  118. package/src/launcher/internal/runTtsx.ts +28 -13
  119. package/src/launcher/internal/runtimeHooks.ts +771 -53
  120. package/src/launcher/internal/servedSourceMap.ts +183 -0
  121. package/src/plugin/internal/buildSourcePlugin.ts +859 -122
  122. package/src/plugin/internal/loadProjectPlugins.ts +36 -6
  123. package/src/structures/ITtscCompilerContext.ts +17 -2
  124. package/src/structures/ITtscPluginCapabilities.ts +9 -0
  125. package/src/structures/ITtscPluginFactoryContext.ts +15 -0
  126. package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
  127. package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
  128. package/src/structures/internal/TtscCommonOptions.ts +8 -0
  129. package/tools/shim_audit/baseline.json +16 -7
  130. package/tools/shim_audit/main.go +777 -71
  131. package/tools/shim_audit/main_test.go +28 -1
  132. package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
  133. package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
  134. package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
  135. package/utility/host.go +46 -20
  136. package/utility/serve.go +1 -1
  137. package/shim/checker/signature_api_completeness.go +0 -58
@@ -10,7 +10,7 @@
10
10
  // The invariant. The shim should be TRANSITIVELY CLOSED over the operations of
11
11
  // the types it already exposes. If the shim already aliases an upstream type T,
12
12
  // then everything a plugin can *reach* starting from T should also be reachable
13
- // through the shim. Three closure rules are checked:
13
+ // through the shim. Four closure rules are checked:
14
14
  //
15
15
  // - ENUM every exported const whose type is an already-exposed enum type
16
16
  // must be re-exported. (This is the `SignatureKindConstruct` class —
@@ -22,6 +22,10 @@
22
22
  // - ESCAPE every exported named type that appears in the signature of an
23
23
  // already-exposed operation but is not itself aliased. (A plugin can
24
24
  // obtain the value but cannot name its type.)
25
+ // - PRODUCER every pointer-like compiler object consumed by a public shim
26
+ // operation must be obtainable from an explicit root or a public operation
27
+ // whose compiler-object inputs and receiver are themselves obtainable. A
28
+ // type alias alone only makes the object nameable; it provides no value.
25
29
  //
26
30
  // What it deliberately does NOT find: UNEXPORTED helpers a plugin needs by name
27
31
  // (e.g. `(*Checker).getMinArgumentCount`, #230 rule 2). Those are invisible to
@@ -101,6 +105,79 @@ func (r reachable) add(pkg, name string) {
101
105
  r[pkg][name] = true
102
106
  }
103
107
 
108
+ // flowType identifies an internal compiler object in the public wrapper flow.
109
+ // Package suffixes retain nested shim ownership (for example vfs/osvfs).
110
+ type flowType struct {
111
+ pkg string
112
+ name string
113
+ }
114
+
115
+ // producerSurface records the public shim operations that consume and
116
+ // produce pointer-like compiler objects. Values are operation names so a gap
117
+ // explains which public wrapper requires the missing producer.
118
+ type producerSurface struct {
119
+ consumed map[flowType]map[string]bool
120
+ produced map[flowType]map[string]bool
121
+ operations []operationFlow
122
+ }
123
+
124
+ // operationFlow keeps results conditional on every compiler-object input and,
125
+ // for methods, an obtainable receiver. Flattening results into the unconditional
126
+ // producer set would let rootless function or method cycles satisfy the audit.
127
+ type operationFlow struct {
128
+ receiver *flowType
129
+ consumed map[flowType]map[string]bool
130
+ produced map[flowType]map[string]bool
131
+ }
132
+
133
+ // localFlowDefinition resolves named callback and container contracts declared
134
+ // by the shim itself. Alias definitions preserve the surrounding pointer state;
135
+ // defined types expose their callable/container underlying shape but are not
136
+ // mistaken for the internal named type they were declared from.
137
+ type localFlowDefinition struct {
138
+ expression ast.Expr
139
+ aliases map[string]string
140
+ alias bool
141
+ }
142
+
143
+ type localFlowVisit struct {
144
+ name string
145
+ direction flowDirection
146
+ pointerLike bool
147
+ }
148
+
149
+ func newProducerSurface() producerSurface {
150
+ return producerSurface{
151
+ consumed: map[flowType]map[string]bool{},
152
+ produced: map[flowType]map[string]bool{},
153
+ }
154
+ }
155
+
156
+ func (s producerSurface) add(direction flowDirection, typ flowType, operation string) {
157
+ target := s.consumed
158
+ if direction == flowProduce {
159
+ target = s.produced
160
+ }
161
+ if target[typ] == nil {
162
+ target[typ] = map[string]bool{}
163
+ }
164
+ target[typ][operation] = true
165
+ }
166
+
167
+ type flowDirection uint8
168
+
169
+ const (
170
+ flowConsume flowDirection = iota
171
+ flowProduce
172
+ )
173
+
174
+ func opposite(direction flowDirection) flowDirection {
175
+ if direction == flowConsume {
176
+ return flowProduce
177
+ }
178
+ return flowConsume
179
+ }
180
+
104
181
  // linknameRe captures the trailing symbol name of a go:linkname target like
105
182
  // `...internal/checker.(*Checker).getMinArgumentCount` or `...checker.foo`.
106
183
  var linknameRe = regexp.MustCompile(`(?m)^//go:linkname\s+\S+\s+` +
@@ -160,6 +237,231 @@ func scanShimReachable(shimRoot string) (reachable, error) {
160
237
  return r, nil
161
238
  }
162
239
 
240
+ // scanShimProducerSurface parses normal shim source and models value flow over
241
+ // exported package functions and methods. A pointer to an internal named type is a
242
+ // compiler-owned graph object: parameters consume one and results produce one.
243
+ // Callback variance is reversed for callback parameters and preserved for
244
+ // callback results, so an input callback's arguments count as values produced
245
+ // by the shim rather than values the plugin must somehow manufacture.
246
+ func scanShimProducerSurface(shimRoot string, inner map[string]*packages.Package) (producerSurface, error) {
247
+ surface := newProducerSurface()
248
+ localTypes := map[string]map[string]localFlowDefinition{}
249
+ sources := map[string]map[string][]byte{}
250
+ for dir, suffix := range shimDirs {
251
+ paths, _ := filepath.Glob(filepath.Join(shimRoot, dir, "*.go"))
252
+ for _, path := range paths {
253
+ if strings.HasSuffix(path, "_test.go") {
254
+ continue
255
+ }
256
+ src, err := os.ReadFile(path)
257
+ if err != nil {
258
+ return producerSurface{}, err
259
+ }
260
+ if sources[suffix] == nil {
261
+ sources[suffix] = map[string][]byte{}
262
+ }
263
+ sources[suffix][path] = src
264
+ imports, err := internalImportAliases(src, path)
265
+ if err != nil {
266
+ return producerSurface{}, err
267
+ }
268
+ for _, importedSuffix := range imports {
269
+ if inner[importedSuffix] == nil {
270
+ return producerSurface{}, fmt.Errorf("%s: internal package %s has no loaded type information", path, importedSuffix)
271
+ }
272
+ }
273
+ definitions, err := scanLocalFlowDefinitions(src, path)
274
+ if err != nil {
275
+ return producerSurface{}, err
276
+ }
277
+ if localTypes[suffix] == nil {
278
+ localTypes[suffix] = map[string]localFlowDefinition{}
279
+ }
280
+ for name, definition := range definitions {
281
+ localTypes[suffix][name] = definition
282
+ }
283
+ }
284
+ }
285
+ for suffix, files := range sources {
286
+ for path, src := range files {
287
+ if err := scanProducerFile(src, path, suffix, localTypes[suffix], inner, &surface); err != nil {
288
+ return producerSurface{}, err
289
+ }
290
+ }
291
+ }
292
+ return surface, nil
293
+ }
294
+
295
+ func scanLocalFlowDefinitions(src []byte, filename string) (map[string]localFlowDefinition, error) {
296
+ aliases, err := internalImportAliases(src, filename)
297
+ if err != nil {
298
+ return nil, err
299
+ }
300
+ file, err := parser.ParseFile(token.NewFileSet(), filename, src, parser.SkipObjectResolution)
301
+ if err != nil {
302
+ return nil, fmt.Errorf("%s: %w", filename, err)
303
+ }
304
+ definitions := map[string]localFlowDefinition{}
305
+ for _, decl := range file.Decls {
306
+ gen, ok := decl.(*ast.GenDecl)
307
+ if !ok || gen.Tok != token.TYPE {
308
+ continue
309
+ }
310
+ for _, spec := range gen.Specs {
311
+ typ, ok := spec.(*ast.TypeSpec)
312
+ if !ok {
313
+ continue
314
+ }
315
+ definitions[typ.Name.Name] = localFlowDefinition{
316
+ expression: typ.Type,
317
+ aliases: aliases,
318
+ alias: typ.Assign.IsValid(),
319
+ }
320
+ }
321
+ }
322
+ return definitions, nil
323
+ }
324
+
325
+ func internalImportAliases(src []byte, filename string) (map[string]string, error) {
326
+ file, err := parser.ParseFile(token.NewFileSet(), filename, src, parser.SkipObjectResolution)
327
+ if err != nil {
328
+ return nil, fmt.Errorf("%s: %w", filename, err)
329
+ }
330
+ aliases := map[string]string{}
331
+ for _, imp := range file.Imports {
332
+ path := strings.Trim(imp.Path.Value, `"`)
333
+ if !strings.HasPrefix(path, internalPrefix) {
334
+ continue
335
+ }
336
+ importedSuffix := strings.TrimPrefix(path, internalPrefix)
337
+ name := importedSuffix[strings.LastIndex(importedSuffix, "/")+1:]
338
+ if imp.Name != nil {
339
+ name = imp.Name.Name
340
+ }
341
+ aliases[name] = importedSuffix
342
+ }
343
+ return aliases, nil
344
+ }
345
+
346
+ func scanProducerFile(src []byte, filename, suffix string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, surface *producerSurface) error {
347
+ file, err := parser.ParseFile(token.NewFileSet(), filename, src, parser.SkipObjectResolution)
348
+ if err != nil {
349
+ return fmt.Errorf("%s: %w", filename, err)
350
+ }
351
+ aliases, err := internalImportAliases(src, filename)
352
+ if err != nil {
353
+ return err
354
+ }
355
+ for _, decl := range file.Decls {
356
+ fn, ok := decl.(*ast.FuncDecl)
357
+ if !ok || !ast.IsExported(fn.Name.Name) {
358
+ continue
359
+ }
360
+ operation := suffix + "." + fn.Name.Name
361
+ if fn.Recv != nil && len(fn.Recv.List) > 0 {
362
+ operation = suffix + "." + receiverTypeName(fn.Recv.List[0].Type) + "." + fn.Name.Name
363
+ }
364
+ operationSurface := newProducerSurface()
365
+ collectFieldFlow(fn.Type.Params, aliases, localTypes, inner, flowConsume, false, operation, operationSurface)
366
+ collectFieldFlow(fn.Type.Results, aliases, localTypes, inner, flowProduce, false, operation, operationSurface)
367
+ if len(operationSurface.consumed) == 0 && len(operationSurface.produced) == 0 {
368
+ continue
369
+ }
370
+ surface.operations = append(surface.operations, operationFlow{
371
+ consumed: operationSurface.consumed,
372
+ produced: operationSurface.produced,
373
+ })
374
+ }
375
+ return nil
376
+ }
377
+
378
+ func receiverTypeName(expr ast.Expr) string {
379
+ switch receiver := expr.(type) {
380
+ case *ast.Ident:
381
+ return receiver.Name
382
+ case *ast.StarExpr:
383
+ return receiverTypeName(receiver.X)
384
+ case *ast.IndexExpr:
385
+ return receiverTypeName(receiver.X)
386
+ case *ast.IndexListExpr:
387
+ return receiverTypeName(receiver.X)
388
+ default:
389
+ return "receiver"
390
+ }
391
+ }
392
+
393
+ func collectFieldFlow(fields *ast.FieldList, aliases map[string]string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, direction flowDirection, pointerLike bool, operation string, surface producerSurface) {
394
+ collectFieldFlowSeen(fields, aliases, localTypes, inner, direction, pointerLike, operation, surface, map[localFlowVisit]bool{})
395
+ }
396
+
397
+ func collectFieldFlowSeen(fields *ast.FieldList, aliases map[string]string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, direction flowDirection, pointerLike bool, operation string, surface producerSurface, seen map[localFlowVisit]bool) {
398
+ if fields == nil {
399
+ return
400
+ }
401
+ for _, field := range fields.List {
402
+ collectTypeFlowSeen(field.Type, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
403
+ }
404
+ }
405
+
406
+ func collectTypeFlowSeen(expr ast.Expr, aliases map[string]string, localTypes map[string]localFlowDefinition, inner map[string]*packages.Package, direction flowDirection, pointerLike bool, operation string, surface producerSurface, seen map[localFlowVisit]bool) {
407
+ switch node := expr.(type) {
408
+ case *ast.StarExpr:
409
+ collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, true, operation, surface, seen)
410
+ case *ast.ArrayType:
411
+ collectTypeFlowSeen(node.Elt, aliases, localTypes, inner, direction, false, operation, surface, seen)
412
+ case *ast.Ellipsis:
413
+ collectTypeFlowSeen(node.Elt, aliases, localTypes, inner, direction, false, operation, surface, seen)
414
+ case *ast.MapType:
415
+ collectTypeFlowSeen(node.Key, aliases, localTypes, inner, direction, false, operation, surface, seen)
416
+ collectTypeFlowSeen(node.Value, aliases, localTypes, inner, direction, false, operation, surface, seen)
417
+ case *ast.ChanType:
418
+ collectTypeFlowSeen(node.Value, aliases, localTypes, inner, direction, false, operation, surface, seen)
419
+ case *ast.ParenExpr:
420
+ collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
421
+ case *ast.IndexExpr:
422
+ collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
423
+ collectTypeFlowSeen(node.Index, aliases, localTypes, inner, direction, false, operation, surface, seen)
424
+ case *ast.IndexListExpr:
425
+ collectTypeFlowSeen(node.X, aliases, localTypes, inner, direction, pointerLike, operation, surface, seen)
426
+ for _, index := range node.Indices {
427
+ collectTypeFlowSeen(index, aliases, localTypes, inner, direction, false, operation, surface, seen)
428
+ }
429
+ case *ast.FuncType:
430
+ collectFieldFlowSeen(node.Params, aliases, localTypes, inner, opposite(direction), false, operation, surface, seen)
431
+ collectFieldFlowSeen(node.Results, aliases, localTypes, inner, direction, false, operation, surface, seen)
432
+ case *ast.InterfaceType:
433
+ collectFieldFlowSeen(node.Methods, aliases, localTypes, inner, direction, false, operation, surface, seen)
434
+ case *ast.Ident:
435
+ definition, local := localTypes[node.Name]
436
+ if !local {
437
+ return
438
+ }
439
+ visit := localFlowVisit{name: node.Name, direction: direction, pointerLike: pointerLike}
440
+ if seen[visit] {
441
+ return
442
+ }
443
+ seen[visit] = true
444
+ collectTypeFlowSeen(definition.expression, definition.aliases, localTypes, inner, direction, definition.alias && pointerLike, operation, surface, seen)
445
+ case *ast.SelectorExpr:
446
+ qualifier, ok := node.X.(*ast.Ident)
447
+ if !ok || !ast.IsExported(node.Sel.Name) {
448
+ return
449
+ }
450
+ importedSuffix, internal := aliases[qualifier.Name]
451
+ if !internal {
452
+ return
453
+ }
454
+ typ := flowType{pkg: importedSuffix, name: node.Sel.Name}
455
+ if pointerLike {
456
+ surface.add(direction, typ, operation)
457
+ } else if pkg := inner[importedSuffix]; pkg != nil && pkg.Types != nil {
458
+ if typeName, ok := pkg.Types.Scope().Lookup(node.Sel.Name).(*types.TypeName); ok {
459
+ collectGoTypeFlow(typeName.Type(), direction, false, operation, surface)
460
+ }
461
+ }
462
+ }
463
+ }
464
+
163
465
  // hasShimSource reports whether dir holds non-test Go source — i.e. an actual
164
466
  // shim re-export package, not a test-only directory like ast/test (whose only
165
467
  // *.go are *_test.go and which is not part of the re-export surface).
@@ -230,36 +532,69 @@ func loadInner(anchorDir string) (map[string]*packages.Package, error) {
230
532
  if err != nil {
231
533
  return nil, err
232
534
  }
233
- out := map[string]*packages.Package{}
234
- errored := map[string]string{}
235
- for _, p := range loaded {
236
- suffix := strings.TrimPrefix(p.PkgPath, internalPrefix)
237
- if len(p.Errors) > 0 {
238
- errored[suffix] = p.Errors[0].Error()
239
- continue
240
- }
241
- out[suffix] = p
242
- }
535
+ indexed, errored := indexInternalPackages(loaded)
243
536
  // A package that fails to load (or is missing entirely) must FAIL the audit,
244
537
  // never be silently skipped — otherwise a load error in any environment turns
245
538
  // the gate into a no-op that passes blind.
246
- var failed []string
539
+ failures := map[string]string{}
247
540
  for suffix := range expected {
248
- if _, ok := out[suffix]; ok {
541
+ if _, ok := indexed[suffix]; ok {
249
542
  continue
250
543
  }
251
- msg := errored[suffix]
252
- if msg == "" {
253
- msg = "did not load"
544
+ failures[suffix] = errored[suffix]
545
+ if failures[suffix] == "" {
546
+ failures[suffix] = "did not load"
254
547
  }
255
- failed = append(failed, suffix+": "+msg)
256
548
  }
257
- if len(failed) > 0 {
549
+ if len(failures) > 0 {
550
+ failed := make([]string, 0, len(failures))
551
+ for suffix, message := range failures {
552
+ failed = append(failed, suffix+": "+message)
553
+ }
258
554
  sort.Strings(failed)
259
- return nil, fmt.Errorf("%d shim package(s) failed to load (audit would be incomplete):\n %s",
555
+ return nil, fmt.Errorf("%d internal package(s) failed to load (audit would be incomplete):\n %s",
260
556
  len(failed), strings.Join(failed, "\n "))
261
557
  }
262
- return out, nil
558
+ roots := map[string]*packages.Package{}
559
+ for suffix := range expected {
560
+ roots[suffix] = indexed[suffix]
561
+ }
562
+ return roots, nil
563
+ }
564
+
565
+ // indexInternalPackages includes dependency packages because public contracts
566
+ // in a registered shim package can name callback/container types declared in an
567
+ // unregistered internal dependency. Dropping such a package would make the AST
568
+ // producer scan silently stop at the selector.
569
+ func indexInternalPackages(roots []*packages.Package) (map[string]*packages.Package, map[string]string) {
570
+ indexed := map[string]*packages.Package{}
571
+ errored := map[string]string{}
572
+ visited := map[*packages.Package]bool{}
573
+ var visit func(*packages.Package)
574
+ visit = func(pkg *packages.Package) {
575
+ if pkg == nil || visited[pkg] {
576
+ return
577
+ }
578
+ visited[pkg] = true
579
+ if strings.HasPrefix(pkg.PkgPath, internalPrefix) {
580
+ suffix := strings.TrimPrefix(pkg.PkgPath, internalPrefix)
581
+ switch {
582
+ case len(pkg.Errors) > 0:
583
+ errored[suffix] = pkg.Errors[0].Error()
584
+ case pkg.Types == nil:
585
+ errored[suffix] = "type information did not load"
586
+ default:
587
+ indexed[suffix] = pkg
588
+ }
589
+ }
590
+ for _, imported := range pkg.Imports {
591
+ visit(imported)
592
+ }
593
+ }
594
+ for _, root := range roots {
595
+ visit(root)
596
+ }
597
+ return indexed, errored
263
598
  }
264
599
 
265
600
  // namedInfo returns the defining package suffix and name of a named OR
@@ -416,7 +751,7 @@ func attachUntypedConsts(enumNames []string, typedMembers map[string][]string, u
416
751
 
417
752
  // Finding kinds.
418
753
  type finding struct {
419
- kind string // ENUM / FUNC / ESCAPE
754
+ kind string // ENUM / FUNC / ESCAPE / PRODUCER
420
755
  pkg string
421
756
  symbol string
422
757
  detail string
@@ -449,8 +784,20 @@ func main() {
449
784
  fmt.Fprintln(os.Stderr, "shim_audit:", err)
450
785
  os.Exit(1)
451
786
  }
787
+ rootPackages := make([]*packages.Package, 0, len(inner))
788
+ for _, pkg := range inner {
789
+ rootPackages = append(rootPackages, pkg)
790
+ }
791
+ typeGraph, _ := indexInternalPackages(rootPackages)
792
+ producerSurface, err := scanShimProducerSurface(*shimRoot, typeGraph)
793
+ if err != nil {
794
+ fmt.Fprintln(os.Stderr, "shim_audit:", err)
795
+ os.Exit(1)
796
+ }
452
797
 
453
798
  findings, pool := analyze(r, inner)
799
+ addExposedMethodFlow(r, inner, &producerSurface)
800
+ producerSurface = canonicalizeProducerSurface(producerSurface, typeGraph)
454
801
 
455
802
  switch {
456
803
  case *fix:
@@ -458,13 +805,314 @@ func main() {
458
805
  case *writeBaseline:
459
806
  runWriteBaseline(findings, *baselinePath)
460
807
  case *check:
461
- runCheck(findings, *baselinePath)
808
+ runCheck(findings, producerSurface, *baselinePath)
462
809
  default:
463
- report(findings, pool, *md)
810
+ producerEvaluation := evaluateProducerSurface(producerSurface, nil)
811
+ report(dedupe(append(findings, producerEvaluation.gaps...)), pool, *md)
464
812
  }
465
813
  }
466
814
 
467
- // analyze runs the three closure checks plus the unexported demand-pool scan and
815
+ type producerEvaluation struct {
816
+ gaps []finding
817
+ usedRoots map[string]bool
818
+ }
819
+
820
+ // evaluateProducerSurface computes the obtainable compiler-object set to a
821
+ // fixed point. Package operations are exposed immediately and methods after
822
+ // their receiver is obtainable, but either unlocks results only after every
823
+ // compiler-object input is obtainable. Explicit baseline roots participate in
824
+ // the same graph without exempting downstream objects.
825
+ func evaluateProducerSurface(surface producerSurface, roots map[string]string) producerEvaluation {
826
+ reachable := map[flowType]bool{}
827
+ for typ := range surface.produced {
828
+ reachable[typ] = true
829
+ }
830
+ allTypes := map[flowType]bool{}
831
+ for typ := range surface.consumed {
832
+ allTypes[typ] = true
833
+ }
834
+ for typ := range surface.produced {
835
+ allTypes[typ] = true
836
+ }
837
+ for _, operation := range surface.operations {
838
+ if operation.receiver != nil {
839
+ allTypes[*operation.receiver] = true
840
+ }
841
+ for typ := range operation.consumed {
842
+ allTypes[typ] = true
843
+ }
844
+ for typ := range operation.produced {
845
+ allTypes[typ] = true
846
+ }
847
+ }
848
+ usedRoots := map[string]bool{}
849
+ for key, rationale := range roots {
850
+ if strings.TrimSpace(rationale) == "" {
851
+ continue
852
+ }
853
+ typ, ok := parseProducerExemptionKey(key)
854
+ if !ok || !allTypes[typ] || reachable[typ] {
855
+ continue
856
+ }
857
+ reachable[typ] = true
858
+ usedRoots[key] = true
859
+ }
860
+
861
+ consumed := map[flowType]map[string]bool{}
862
+ mergeFlows := func(target map[flowType]map[string]bool, flows map[flowType]map[string]bool) {
863
+ for typ, operations := range flows {
864
+ if target[typ] == nil {
865
+ target[typ] = map[string]bool{}
866
+ }
867
+ for operation := range operations {
868
+ target[typ][operation] = true
869
+ }
870
+ }
871
+ }
872
+ mergeFlows(consumed, surface.consumed)
873
+ exposed := make([]bool, len(surface.operations))
874
+ callable := make([]bool, len(surface.operations))
875
+ for changed := true; changed; {
876
+ changed = false
877
+ for index, operation := range surface.operations {
878
+ if !exposed[index] {
879
+ if operation.receiver != nil && !reachable[*operation.receiver] {
880
+ continue
881
+ }
882
+ exposed[index] = true
883
+ changed = true
884
+ mergeFlows(consumed, operation.consumed)
885
+ }
886
+ if callable[index] {
887
+ continue
888
+ }
889
+ requirementsMet := true
890
+ for typ := range operation.consumed {
891
+ if !reachable[typ] {
892
+ requirementsMet = false
893
+ break
894
+ }
895
+ }
896
+ if !requirementsMet {
897
+ continue
898
+ }
899
+ callable[index] = true
900
+ changed = true
901
+ for typ := range operation.produced {
902
+ if !reachable[typ] {
903
+ reachable[typ] = true
904
+ }
905
+ }
906
+ }
907
+ }
908
+
909
+ var findings []finding
910
+ for typ, operations := range consumed {
911
+ if reachable[typ] {
912
+ continue
913
+ }
914
+ names := make([]string, 0, len(operations))
915
+ for operation := range operations {
916
+ names = append(names, operation)
917
+ }
918
+ sort.Strings(names)
919
+ findings = append(findings, finding{
920
+ kind: "PRODUCER",
921
+ pkg: typ.pkg,
922
+ symbol: typ.name,
923
+ detail: "consumed by " + strings.Join(names, ", ") + " but no reachable public shim operation produces it",
924
+ })
925
+ }
926
+ return producerEvaluation{gaps: dedupe(findings), usedRoots: usedRoots}
927
+ }
928
+
929
+ func parseProducerExemptionKey(key string) (flowType, bool) {
930
+ separator := strings.LastIndexByte(key, '.')
931
+ if separator <= 0 || separator == len(key)-1 {
932
+ return flowType{}, false
933
+ }
934
+ return flowType{pkg: key[:separator], name: key[separator+1:]}, true
935
+ }
936
+
937
+ func canonicalizeProducerSurface(surface producerSurface, inner map[string]*packages.Package) producerSurface {
938
+ canonical := newProducerSurface()
939
+ copyDirection := func(direction flowDirection, flows map[flowType]map[string]bool) {
940
+ for typ, operations := range flows {
941
+ typ = canonicalFlowType(typ, inner)
942
+ for operation := range operations {
943
+ canonical.add(direction, typ, operation)
944
+ }
945
+ }
946
+ }
947
+ copyDirection(flowConsume, surface.consumed)
948
+ copyDirection(flowProduce, surface.produced)
949
+ for _, operation := range surface.operations {
950
+ converted := operationFlow{
951
+ consumed: map[flowType]map[string]bool{},
952
+ produced: map[flowType]map[string]bool{},
953
+ }
954
+ if operation.receiver != nil {
955
+ receiver := canonicalFlowType(*operation.receiver, inner)
956
+ converted.receiver = &receiver
957
+ }
958
+ copyOperationDirection := func(target map[flowType]map[string]bool, flows map[flowType]map[string]bool) {
959
+ for typ, operations := range flows {
960
+ typ = canonicalFlowType(typ, inner)
961
+ if target[typ] == nil {
962
+ target[typ] = map[string]bool{}
963
+ }
964
+ for operation := range operations {
965
+ target[typ][operation] = true
966
+ }
967
+ }
968
+ }
969
+ copyOperationDirection(converted.consumed, operation.consumed)
970
+ copyOperationDirection(converted.produced, operation.produced)
971
+ canonical.operations = append(canonical.operations, converted)
972
+ }
973
+ return canonical
974
+ }
975
+
976
+ func canonicalFlowType(typ flowType, inner map[string]*packages.Package) flowType {
977
+ pkg := inner[typ.pkg]
978
+ if pkg == nil || pkg.Types == nil {
979
+ return typ
980
+ }
981
+ named, ok := pkg.Types.Scope().Lookup(typ.name).(*types.TypeName)
982
+ if !ok {
983
+ return typ
984
+ }
985
+ if pkgSuffix, name, ok := namedInfo(types.Unalias(named.Type())); ok {
986
+ return flowType{pkg: pkgSuffix, name: name}
987
+ }
988
+ return typ
989
+ }
990
+
991
+ // addExposedMethodFlow covers the method set published automatically by each
992
+ // exposed type alias. Those methods do not appear as declarations in shim
993
+ // source, but their receiver, parameter, callback, and result flows are part of
994
+ // the same public surface as hand-written package wrappers.
995
+ func addExposedMethodFlow(r reachable, inner map[string]*packages.Package, surface *producerSurface) {
996
+ for suffix, pkg := range inner {
997
+ scope := pkg.Types.Scope()
998
+ for _, name := range scope.Names() {
999
+ typeName, ok := scope.Lookup(name).(*types.TypeName)
1000
+ if !ok || !r.has(suffix, name) {
1001
+ continue
1002
+ }
1003
+ exposed := types.Unalias(typeName.Type())
1004
+ named, ok := exposed.(*types.Named)
1005
+ if !ok {
1006
+ continue
1007
+ }
1008
+ receiver := types.Type(named)
1009
+ if _, isInterface := named.Underlying().(*types.Interface); !isInterface {
1010
+ receiver = types.NewPointer(named)
1011
+ }
1012
+ methods := types.NewMethodSet(receiver)
1013
+ for i := 0; i < methods.Len(); i++ {
1014
+ method, ok := methods.At(i).Obj().(*types.Func)
1015
+ if !ok || !method.Exported() {
1016
+ continue
1017
+ }
1018
+ signature := method.Signature()
1019
+ operation := suffix + "." + name + "." + method.Name()
1020
+ methodSurface := newProducerSurface()
1021
+ collectGoTupleFlow(signature.Params(), flowConsume, operation, methodSurface)
1022
+ collectGoTupleFlow(signature.Results(), flowProduce, operation, methodSurface)
1023
+ if len(methodSurface.consumed) == 0 && len(methodSurface.produced) == 0 {
1024
+ continue
1025
+ }
1026
+ // Key the dependency to the exposed outer type, not the declaring
1027
+ // receiver of a promoted method: callers need an outer value to invoke
1028
+ // the selected method and never manufacture an embedded implementation
1029
+ // receiver directly.
1030
+ receiverType := flowType{pkg: suffix, name: name}
1031
+ surface.operations = append(surface.operations, operationFlow{
1032
+ receiver: &receiverType,
1033
+ consumed: methodSurface.consumed,
1034
+ produced: methodSurface.produced,
1035
+ })
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ func collectGoTupleFlow(tuple *types.Tuple, direction flowDirection, operation string, surface producerSurface) {
1042
+ collectGoTupleFlowSeen(tuple, direction, operation, surface, map[flowVisit]bool{})
1043
+ }
1044
+
1045
+ type flowVisit struct {
1046
+ typ types.Type
1047
+ direction flowDirection
1048
+ pointerLike bool
1049
+ }
1050
+
1051
+ func collectGoTupleFlowSeen(tuple *types.Tuple, direction flowDirection, operation string, surface producerSurface, seen map[flowVisit]bool) {
1052
+ if tuple == nil {
1053
+ return
1054
+ }
1055
+ for i := 0; i < tuple.Len(); i++ {
1056
+ collectGoTypeFlowSeen(tuple.At(i).Type(), direction, false, operation, surface, seen)
1057
+ }
1058
+ }
1059
+
1060
+ func collectGoTypeFlow(typ types.Type, direction flowDirection, pointerLike bool, operation string, surface producerSurface) {
1061
+ collectGoTypeFlowSeen(typ, direction, pointerLike, operation, surface, map[flowVisit]bool{})
1062
+ }
1063
+
1064
+ func collectGoTypeFlowSeen(typ types.Type, direction flowDirection, pointerLike bool, operation string, surface producerSurface, seen map[flowVisit]bool) {
1065
+ visit := flowVisit{typ: typ, direction: direction, pointerLike: pointerLike}
1066
+ if seen[visit] {
1067
+ return
1068
+ }
1069
+ seen[visit] = true
1070
+ switch current := typ.(type) {
1071
+ case *types.Alias:
1072
+ collectGoTypeFlowSeen(types.Unalias(current), direction, pointerLike, operation, surface, seen)
1073
+ case *types.Pointer:
1074
+ collectGoTypeFlowSeen(current.Elem(), direction, true, operation, surface, seen)
1075
+ case *types.Slice:
1076
+ collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
1077
+ case *types.Array:
1078
+ collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
1079
+ case *types.Map:
1080
+ collectGoTypeFlowSeen(current.Key(), direction, false, operation, surface, seen)
1081
+ collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
1082
+ case *types.Chan:
1083
+ collectGoTypeFlowSeen(current.Elem(), direction, false, operation, surface, seen)
1084
+ case *types.Signature:
1085
+ collectGoTupleFlowSeen(current.Params(), opposite(direction), operation, surface, seen)
1086
+ collectGoTupleFlowSeen(current.Results(), direction, operation, surface, seen)
1087
+ case *types.Interface:
1088
+ current.Complete()
1089
+ for i := 0; i < current.NumMethods(); i++ {
1090
+ signature, ok := current.Method(i).Type().(*types.Signature)
1091
+ if !ok {
1092
+ continue
1093
+ }
1094
+ collectGoTupleFlowSeen(signature.Params(), opposite(direction), operation, surface, seen)
1095
+ collectGoTupleFlowSeen(signature.Results(), direction, operation, surface, seen)
1096
+ }
1097
+ case *types.Named:
1098
+ if pointerLike {
1099
+ if pkg, name, ok := namedInfo(current); ok {
1100
+ surface.add(direction, flowType{pkg: pkg, name: name}, operation)
1101
+ }
1102
+ }
1103
+ if args := current.TypeArgs(); args != nil {
1104
+ for i := 0; i < args.Len(); i++ {
1105
+ collectGoTypeFlowSeen(args.At(i), direction, false, operation, surface, seen)
1106
+ }
1107
+ }
1108
+ switch underlying := current.Underlying().(type) {
1109
+ case *types.Interface, *types.Signature, *types.Slice, *types.Array, *types.Map, *types.Chan, *types.Pointer:
1110
+ collectGoTypeFlowSeen(underlying, direction, false, operation, surface, seen)
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ // analyze runs the upstream closure checks plus the unexported demand-pool scan and
468
1116
  // returns the deduped findings and pool.
469
1117
  func analyze(r reachable, inner map[string]*packages.Package) (findings, unexportedPool []finding) {
470
1118
  for suffix, pkg := range inner {
@@ -731,7 +1379,7 @@ func tierOf(kind string) int {
731
1379
  switch kind {
732
1380
  case "ENUM": // partial enum — some members exposed, siblings missing
733
1381
  return 1
734
- case "FUNC": // exported op over already-reachable types
1382
+ case "FUNC", "PRODUCER": // callable ops and constructible object flow
735
1383
  return 2
736
1384
  case "ESCAPE": // type returned by an exposed op but not aliased
737
1385
  return 3
@@ -771,7 +1419,7 @@ func report(findings, pool []finding, md bool) {
771
1419
  } else {
772
1420
  fmt.Printf("=== Shim closure audit ===\n")
773
1421
  }
774
- fmt.Printf("Tier 1 PARTIAL-ENUM (near-certain bugs): %d | Tier 2 FUNC-over-exposed: %d | Tier 3 ESCAPE: %d | Tier 4 type-only enums: %d | Unexported demand pool: %d\n",
1422
+ fmt.Printf("Tier 1 PARTIAL-ENUM (near-certain bugs): %d | Tier 2 FUNC/PRODUCER: %d | Tier 3 ESCAPE: %d | Tier 4 type-only enums: %d | Unexported demand pool: %d\n",
775
1423
  len(tiers[1]), len(tiers[2]), len(tiers[3]), len(tiers[4]), len(pool))
776
1424
 
777
1425
  h("TIER 1 — partial enums (a sibling const is missing; this is the #230 class)")
@@ -780,7 +1428,7 @@ func report(findings, pool []finding, md bool) {
780
1428
  row(f)
781
1429
  }
782
1430
 
783
- h("TIER 2 — exported funcs whose params/results are all already reachable")
1431
+ h("TIER 2 — reachable funcs and consumed compiler objects without producers")
784
1432
  thead()
785
1433
  for _, f := range tiers[2] {
786
1434
  row(f)
@@ -825,10 +1473,60 @@ func countByPkg(in []finding) string {
825
1473
  func findingKey(f finding) string { return f.kind + "|" + f.pkg + "|" + f.symbol }
826
1474
 
827
1475
  // baselineFile is the on-disk schema of accepted TIER-2/3 gaps. TIER-1 enum
828
- // gaps are never baselined — they are zero-tolerance and fixed by -fix.
1476
+ // gaps are never baselined. Producer gaps require a separate, reasoned root or
1477
+ // ownership-boundary exemption because accepting an ordinary symbol gap must
1478
+ // not waive object constructibility.
829
1479
  type baselineFile struct {
830
- Note string `json:"note"`
831
- Accepted []string `json:"accepted"`
1480
+ Note string `json:"note"`
1481
+ Accepted []string `json:"accepted"`
1482
+ ProducerExemptions map[string]string `json:"producer_exemptions,omitempty"`
1483
+ }
1484
+
1485
+ type baselineEvaluation struct {
1486
+ enumGaps []finding
1487
+ newGaps []finding
1488
+ producerGaps []finding
1489
+ invalidReasons []string
1490
+ staleAccepted int
1491
+ staleProducers int
1492
+ }
1493
+
1494
+ func evaluateBaseline(findings []finding, baseline baselineFile, usedProducerRoots map[string]bool) baselineEvaluation {
1495
+ accepted := map[string]bool{}
1496
+ for _, key := range baseline.Accepted {
1497
+ accepted[key] = true
1498
+ }
1499
+ evaluation := baselineEvaluation{}
1500
+ liveAccepted := map[string]bool{}
1501
+ for _, f := range findings {
1502
+ switch f.kind {
1503
+ case "ENUM":
1504
+ evaluation.enumGaps = append(evaluation.enumGaps, f)
1505
+ case "FUNC", "ESCAPE":
1506
+ key := findingKey(f)
1507
+ liveAccepted[key] = true
1508
+ if !accepted[key] {
1509
+ evaluation.newGaps = append(evaluation.newGaps, f)
1510
+ }
1511
+ case "PRODUCER":
1512
+ evaluation.producerGaps = append(evaluation.producerGaps, f)
1513
+ }
1514
+ }
1515
+ for key := range accepted {
1516
+ if !liveAccepted[key] {
1517
+ evaluation.staleAccepted++
1518
+ }
1519
+ }
1520
+ for key, rationale := range baseline.ProducerExemptions {
1521
+ if strings.TrimSpace(rationale) == "" {
1522
+ evaluation.invalidReasons = append(evaluation.invalidReasons, key)
1523
+ }
1524
+ if !usedProducerRoots[key] {
1525
+ evaluation.staleProducers++
1526
+ }
1527
+ }
1528
+ sort.Strings(evaluation.invalidReasons)
1529
+ return evaluation
832
1530
  }
833
1531
 
834
1532
  // shimPackageName reads the `package` clause from a .go file in dir.
@@ -906,7 +1604,8 @@ func runFix(findings []finding, shimRoot string) {
906
1604
  }
907
1605
 
908
1606
  // runWriteBaseline records the current TIER-2/3 gaps as accepted, so the gate
909
- // fails only on NEW ones. The list can only shrink as gaps get exposed.
1607
+ // fails only on NEW ones. Producer exemptions are preserved rather than
1608
+ // inferred: each root needs an explicit human rationale.
910
1609
  func runWriteBaseline(findings []finding, path string) {
911
1610
  var keys []string
912
1611
  for _, f := range findings {
@@ -915,9 +1614,19 @@ func runWriteBaseline(findings []finding, path string) {
915
1614
  }
916
1615
  }
917
1616
  sort.Strings(keys)
1617
+ producerExemptions := map[string]string{}
1618
+ if existing, err := os.ReadFile(path); err == nil {
1619
+ var baseline baselineFile
1620
+ if err := json.Unmarshal(existing, &baseline); err != nil {
1621
+ fmt.Fprintf(os.Stderr, "shim_audit: parse %s: %v\n", path, err)
1622
+ os.Exit(2)
1623
+ }
1624
+ producerExemptions = baseline.ProducerExemptions
1625
+ }
918
1626
  data, err := json.MarshalIndent(baselineFile{
919
- Note: "Accepted TIER-2 (reachable funcs) and TIER-3 (escaping types) shim gaps. The gate fails on any gap NOT listed here; expose a symbol and remove its line, or run -write-baseline to accept new ones deliberately. TIER-1 enum gaps are never listed — they are zero-tolerance (run -fix).",
920
- Accepted: keys,
1627
+ Note: "Accepted TIER-2 (reachable funcs) and TIER-3 (escaping types) shim gaps. The gate fails on any gap NOT listed here; expose a symbol and remove its line, or run -write-baseline to accept new ones deliberately. TIER-1 enum and PRODUCER gaps are zero-tolerance; producer_exemptions must name genuine roots or ownership boundaries with non-empty rationales.",
1628
+ Accepted: keys,
1629
+ ProducerExemptions: producerExemptions,
921
1630
  }, "", " ")
922
1631
  if err != nil {
923
1632
  fmt.Fprintln(os.Stderr, "shim_audit:", err)
@@ -930,66 +1639,63 @@ func runWriteBaseline(findings []finding, path string) {
930
1639
  fmt.Printf("shim_audit: wrote %s (%d accepted gaps)\n", path, len(keys))
931
1640
  }
932
1641
 
933
- // runCheck is the CI gate. It fails on any TIER-1 enum gap (zero tolerance) or
934
- // any TIER-2/3 gap not present in the baseline.
935
- func runCheck(findings []finding, path string) {
936
- accepted := map[string]bool{}
1642
+ // runCheck is the CI gate. It fails on any TIER-1 enum gap, any unreasoned
1643
+ // producer gap, or any TIER-2/3 gap not present in the baseline.
1644
+ func runCheck(findings []finding, surface producerSurface, path string) {
1645
+ baseline := baselineFile{}
937
1646
  if data, err := os.ReadFile(path); err == nil {
938
- var bf baselineFile
939
- if err := json.Unmarshal(data, &bf); err != nil {
1647
+ if err := json.Unmarshal(data, &baseline); err != nil {
940
1648
  fmt.Fprintf(os.Stderr, "shim_audit: parse %s: %v\n", path, err)
941
1649
  os.Exit(2)
942
1650
  }
943
- for _, k := range bf.Accepted {
944
- accepted[k] = true
945
- }
946
1651
  } else {
947
- fmt.Fprintf(os.Stderr, "shim_audit: WARNING no baseline at %s; treating all func/type gaps as new\n", path)
1652
+ fmt.Fprintf(os.Stderr, "shim_audit: WARNING no baseline at %s; treating all func/type and producer gaps as new\n", path)
948
1653
  }
949
1654
 
950
- var enumGaps, newGaps []finding
951
- live := map[string]bool{}
952
- for _, f := range findings {
953
- switch f.kind {
954
- case "ENUM":
955
- enumGaps = append(enumGaps, f)
956
- case "FUNC", "ESCAPE":
957
- live[findingKey(f)] = true
958
- if !accepted[findingKey(f)] {
959
- newGaps = append(newGaps, f)
960
- }
961
- }
962
- }
1655
+ producerEvaluation := evaluateProducerSurface(surface, baseline.ProducerExemptions)
1656
+ findings = dedupe(append(findings, producerEvaluation.gaps...))
1657
+ evaluation := evaluateBaseline(findings, baseline, producerEvaluation.usedRoots)
963
1658
  // Non-failing hygiene note: baseline lines that no longer match a gap (the
964
1659
  // symbol got exposed) are dead weight; the ratchet stays safe but suggest a prune.
965
- var stale int
966
- for k := range accepted {
967
- if !live[k] {
968
- stale++
969
- }
1660
+ if evaluation.staleAccepted > 0 {
1661
+ fmt.Fprintf(os.Stderr, "shim_audit: note: %d baseline entr(y/ies) no longer match a gap; prune with -write-baseline\n", evaluation.staleAccepted)
970
1662
  }
971
- if stale > 0 {
972
- fmt.Fprintf(os.Stderr, "shim_audit: note: %d baseline entr(y/ies) no longer match a gap; prune with -write-baseline\n", stale)
1663
+ if evaluation.staleProducers > 0 {
1664
+ fmt.Fprintf(os.Stderr, "shim_audit: note: %d producer exemption(s) no longer match a gap; remove them\n", evaluation.staleProducers)
973
1665
  }
974
1666
 
975
- if len(enumGaps) == 0 && len(newGaps) == 0 {
976
- fmt.Println("shim_audit: OK — every exposed enum family is complete and no new reachable gaps")
1667
+ if len(evaluation.enumGaps) == 0 && len(evaluation.newGaps) == 0 &&
1668
+ len(evaluation.producerGaps) == 0 && len(evaluation.invalidReasons) == 0 {
1669
+ fmt.Println("shim_audit: OK — exposed enums, reachable symbols, and compiler-object producers are closed")
977
1670
  return
978
1671
  }
979
- if len(enumGaps) > 0 {
980
- fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d enum-family member(s) of an EXPOSED enum are not re-exported.\n", len(enumGaps))
1672
+ if len(evaluation.enumGaps) > 0 {
1673
+ fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d enum-family member(s) of an EXPOSED enum are not re-exported.\n", len(evaluation.enumGaps))
981
1674
  fmt.Fprintf(os.Stderr, " This is the #230 class. Fix mechanically: `pnpm --filter ttsc shim:audit -fix`.\n")
982
- for _, f := range enumGaps {
1675
+ for _, f := range evaluation.enumGaps {
983
1676
  fmt.Fprintf(os.Stderr, " ENUM %s.%s\n", f.pkg, f.symbol)
984
1677
  }
985
1678
  }
986
- if len(newGaps) > 0 {
987
- fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d new reachable gap(s) not in the baseline.\n", len(newGaps))
1679
+ if len(evaluation.newGaps) > 0 {
1680
+ fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d new reachable gap(s) not in the baseline.\n", len(evaluation.newGaps))
988
1681
  fmt.Fprintf(os.Stderr, " Expose them in the shim, or accept deliberately: `pnpm --filter ttsc shim:audit -write-baseline`.\n")
989
- for _, f := range newGaps {
1682
+ for _, f := range evaluation.newGaps {
990
1683
  fmt.Fprintf(os.Stderr, " %-6s %s.%s — %s\n", f.kind, f.pkg, f.symbol, f.detail)
991
1684
  }
992
1685
  }
1686
+ if len(evaluation.producerGaps) > 0 {
1687
+ fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d consumed compiler object(s) have no public producer.\n", len(evaluation.producerGaps))
1688
+ fmt.Fprintf(os.Stderr, " Add a general exported producer, or document a genuine root or ownership boundary in producer_exemptions.\n")
1689
+ for _, f := range evaluation.producerGaps {
1690
+ fmt.Fprintf(os.Stderr, " PRODUCER %s.%s — %s\n", f.pkg, f.symbol, f.detail)
1691
+ }
1692
+ }
1693
+ if len(evaluation.invalidReasons) > 0 {
1694
+ fmt.Fprintf(os.Stderr, "\nshim_audit: FAIL — %d producer exemption(s) have an empty rationale.\n", len(evaluation.invalidReasons))
1695
+ for _, key := range evaluation.invalidReasons {
1696
+ fmt.Fprintf(os.Stderr, " PRODUCER-EXEMPTION %s\n", key)
1697
+ }
1698
+ }
993
1699
  fmt.Fprintln(os.Stderr)
994
1700
  os.Exit(1)
995
1701
  }