[object Object]

← back to Cli Printing Press

fix(cli): anchor openapi loader normalization (#730)

a4b0eb30590d1d322d6895072aa709db5ed46d78 · 2026-05-08 08:53:13 -0700 · Trevin Chow

* fix(cli): anchor openapi loader normalization

* refactor(cli): simplify openapi parser loading

* fix(cli): preserve strict remote ref guard

Files touched

Diff

commit a4b0eb30590d1d322d6895072aa709db5ed46d78
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri May 8 08:53:13 2026 -0700

    fix(cli): anchor openapi loader normalization (#730)
    
    * fix(cli): anchor openapi loader normalization
    
    * refactor(cli): simplify openapi parser loading
    
    * fix(cli): preserve strict remote ref guard
---
 cmd/manifest-gen/main.go                       |  8 ++-
 internal/cli/public_param_audit.go             |  6 +-
 internal/cli/root.go                           | 23 ++++--
 internal/openapi/description_normalize.go      | 61 ++++++++++++++--
 internal/openapi/description_normalize_test.go | 72 +++++++++++++++++++
 internal/openapi/parser.go                     | 96 ++++++++++++++++++++++++--
 internal/openapi/parser_test.go                | 84 ++++++++++++++++++++++
 internal/pipeline/dogfood.go                   | 17 ++---
 internal/pipeline/mcpsync/sync.go              |  2 +-
 internal/pipeline/publish.go                   |  2 +-
 internal/pipeline/spec_detect.go               | 20 ------
 11 files changed, 336 insertions(+), 55 deletions(-)

diff --git a/cmd/manifest-gen/main.go b/cmd/manifest-gen/main.go
index 1c2c7ddb..a04bef82 100644
--- a/cmd/manifest-gen/main.go
+++ b/cmd/manifest-gen/main.go
@@ -56,7 +56,11 @@ func main() {
 	var parsed *spec.APISpec
 	switch format {
 	case "openapi":
-		parsed, err = openapi.ParseLenient(data)
+		if openapi.IsRemoteSpecSource(*specFlag) {
+			parsed, err = openapi.ParseLenient(data)
+		} else {
+			parsed, err = openapi.ParseWithPathLenient(data, *specFlag)
+		}
 	case "internal":
 		parsed, err = spec.ParseBytes(data)
 	case "graphql":
@@ -107,7 +111,7 @@ func main() {
 }
 
 func loadSpec(source string) ([]byte, error) {
-	if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
+	if openapi.IsRemoteSpecSource(source) {
 		resp, err := http.Get(source)
 		if err != nil {
 			return nil, fmt.Errorf("fetching %s: %w", source, err)
diff --git a/internal/cli/public_param_audit.go b/internal/cli/public_param_audit.go
index 5cb2cefc..23db035f 100644
--- a/internal/cli/public_param_audit.go
+++ b/internal/cli/public_param_audit.go
@@ -93,11 +93,7 @@ func parsePublicParamAuditSpec(specFiles []string, cliName string, lenient bool)
 
 		var apiSpec *spec.APISpec
 		if openapi.IsOpenAPI(data) {
-			if lenient {
-				apiSpec, err = openapi.ParseLenient(data)
-			} else {
-				apiSpec, err = openapi.Parse(data)
-			}
+			apiSpec, err = parseOpenAPISpec(specFile, data, lenient)
 		} else if graphql.IsGraphQLSDL(data) {
 			apiSpec, err = graphql.ParseSDLBytes(specFile, data)
 		} else {
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 5f463654..04e88dd9 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -260,11 +260,7 @@ func newGenerateCmd() *cobra.Command {
 
 				var apiSpec *spec.APISpec
 				if openapi.IsOpenAPI(data) {
-					if lenient {
-						apiSpec, err = openapi.ParseLenient(data)
-					} else {
-						apiSpec, err = openapi.Parse(data)
-					}
+					apiSpec, err = parseOpenAPISpec(specFile, data, lenient)
 				} else if graphql.IsGraphQLSDL(data) {
 					apiSpec, err = graphql.ParseSDLBytes(specFile, data)
 				} else {
@@ -644,7 +640,7 @@ func inferTrafficAnalysisPath(specFiles []string, specSource string) string {
 		return ""
 	}
 	specPath := specFiles[0]
-	if strings.HasPrefix(specPath, "http://") || strings.HasPrefix(specPath, "https://") {
+	if openapi.IsRemoteSpecSource(specPath) {
 		return ""
 	}
 	candidate := browsersniff.DefaultTrafficAnalysisPath(specPath)
@@ -657,7 +653,7 @@ func inferTrafficAnalysisPath(specFiles []string, specSource string) string {
 func readSpec(specFile string, refresh bool, skipCache bool) ([]byte, error) {
 	var data []byte
 	var err error
-	if strings.HasPrefix(specFile, "http://") || strings.HasPrefix(specFile, "https://") {
+	if openapi.IsRemoteSpecSource(specFile) {
 		data, err = fetchOrCacheSpec(specFile, refresh, skipCache)
 	} else {
 		data, err = os.ReadFile(specFile)
@@ -671,6 +667,19 @@ func readSpec(specFile string, refresh bool, skipCache bool) ([]byte, error) {
 	return data, nil
 }
 
+func parseOpenAPISpec(specFile string, data []byte, lenient bool) (*spec.APISpec, error) {
+	if openapi.IsRemoteSpecSource(specFile) {
+		if lenient {
+			return openapi.ParseLenient(data)
+		}
+		return openapi.Parse(data)
+	}
+	if lenient {
+		return openapi.ParseWithPathLenient(data, specFile)
+	}
+	return openapi.ParseWithPath(data, specFile)
+}
+
 func mergeSpecs(specs []*spec.APISpec, name string) *spec.APISpec {
 	if len(specs) == 1 {
 		return specs[0]
diff --git a/internal/openapi/description_normalize.go b/internal/openapi/description_normalize.go
index dcd148c1..42054752 100644
--- a/internal/openapi/description_normalize.go
+++ b/internal/openapi/description_normalize.go
@@ -3,6 +3,7 @@ package openapi
 import (
 	"encoding/json"
 	"fmt"
+	"sort"
 
 	"gopkg.in/yaml.v3"
 )
@@ -34,7 +35,7 @@ func normalizeSpecData(data []byte) ([]byte, error) {
 		return nil, fmt.Errorf("normalize spec: yaml unmarshal: %w", err)
 	}
 	root = convertToStringKeyed(root)
-	flattenObjectDescriptions(root, "")
+	normalizeSpecTree(root, "", false)
 	out, err := json.Marshal(root)
 	if err != nil {
 		return nil, fmt.Errorf("normalize spec: json marshal: %w", err)
@@ -71,9 +72,10 @@ func convertToStringKeyed(node any) any {
 	}
 }
 
-// flattenObjectDescriptions walks the decoded spec tree and replaces any
-// `description` key whose value is a map or slice with an empty string. Scalar
-// descriptions (the common case) pass through untouched.
+// normalizeSpecTree walks the decoded spec tree and applies tolerant rewrites
+// kin-openapi needs before loading real-world specs. It replaces non-scalar
+// `description` fields with an empty string and normalizes schema `examples`
+// values to the array shape OpenAPI expects.
 //
 // Skips the flatten when the immediate parent key is one whose children are
 // user-named entries (Schema property names, response codes, pattern regexes,
@@ -83,7 +85,7 @@ func convertToStringKeyed(node any) any {
 // ... } }` that hits this case; flattening there would replace the entry's
 // schema with an empty string and produce
 // `cannot unmarshal string into field Schema.properties of type openapi3.Schema`.
-func flattenObjectDescriptions(node any, parentKey string) {
+func normalizeSpecTree(node any, parentKey string, inSchema bool) {
 	switch v := node.(type) {
 	case map[string]any:
 		if !isNameKeyedParent(parentKey) {
@@ -94,16 +96,61 @@ func flattenObjectDescriptions(node any, parentKey string) {
 				}
 			}
 		}
+		if inSchema {
+			if examples, ok := v["examples"]; ok {
+				v["examples"] = normalizeExamplesValue(examples)
+			}
+		}
 		for key, value := range v {
-			flattenObjectDescriptions(value, key)
+			normalizeSpecTree(value, key, childIsSchema(parentKey, key))
 		}
 	case []any:
 		for _, item := range v {
-			flattenObjectDescriptions(item, "")
+			normalizeSpecTree(item, "", schemaArrayItems(parentKey))
 		}
 	}
 }
 
+func normalizeExamplesValue(examples any) any {
+	switch v := examples.(type) {
+	case []any:
+		return v
+	case map[string]any:
+		keys := make([]string, 0, len(v))
+		for key := range v {
+			keys = append(keys, key)
+		}
+		sort.Strings(keys)
+		out := make([]any, 0, len(keys))
+		for _, key := range keys {
+			out = append(out, v[key])
+		}
+		return out
+	default:
+		return []any{v}
+	}
+}
+
+func childIsSchema(parentKey, key string) bool {
+	switch parentKey {
+	case "schemas", "definitions", "properties", "patternProperties":
+		return true
+	}
+	switch key {
+	case "schema", "items", "additionalProperties", "not":
+		return true
+	}
+	return false
+}
+
+func schemaArrayItems(parentKey string) bool {
+	switch parentKey {
+	case "allOf", "oneOf", "anyOf":
+		return true
+	}
+	return false
+}
+
 // isNameKeyedParent reports whether children of the given parent key are
 // user-defined names rather than OpenAPI structural fields. Inside these
 // containers a "description" key is the caller's chosen name and must not be
diff --git a/internal/openapi/description_normalize_test.go b/internal/openapi/description_normalize_test.go
index 3110eace..5ba0c9e4 100644
--- a/internal/openapi/description_normalize_test.go
+++ b/internal/openapi/description_normalize_test.go
@@ -1,6 +1,7 @@
 package openapi
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -146,3 +147,74 @@ paths:
 	require.NoError(t, err, "spec with object-shaped descriptions at info and operation levels must parse")
 	assert.Equal(t, "nestedapi", parsed.Name)
 }
+
+func TestNormalizeSpecDataConvertsSchemaExamplesMap(t *testing.T) {
+	t.Parallel()
+
+	data := []byte(`
+openapi: "3.0.0"
+info:
+  title: Legacy Examples
+  version: "0.1.0"
+paths:
+  /actions:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                actionParameters:
+                  type: object
+                  examples:
+                    first:
+                      value: one
+                    second:
+                      value: two
+            examples:
+              named:
+                value:
+                  actionParameters:
+                    value: preserved
+      responses:
+        '200':
+          description: ok
+components:
+  schemas:
+    Action:
+      type: object
+      examples:
+        named:
+          actionParameters:
+            value: preserved
+    OnlyExamples:
+      examples:
+        first:
+          value: structurally schema
+`)
+
+	normalized, err := normalizeSpecData(data)
+	require.NoError(t, err)
+
+	var root map[string]any
+	require.NoError(t, json.Unmarshal(normalized, &root))
+
+	actionParameters := root["paths"].(map[string]any)["/actions"].(map[string]any)["post"].(map[string]any)["requestBody"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)["properties"].(map[string]any)["actionParameters"].(map[string]any)
+	require.IsType(t, []any{}, actionParameters["examples"])
+	assert.Len(t, actionParameters["examples"], 2)
+
+	mediaType := root["paths"].(map[string]any)["/actions"].(map[string]any)["post"].(map[string]any)["requestBody"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)
+	require.IsType(t, map[string]any{}, mediaType["examples"])
+
+	component := root["components"].(map[string]any)["schemas"].(map[string]any)["Action"].(map[string]any)
+	require.IsType(t, []any{}, component["examples"])
+	assert.Len(t, component["examples"], 1)
+
+	onlyExamples := root["components"].(map[string]any)["schemas"].(map[string]any)["OnlyExamples"].(map[string]any)
+	require.IsType(t, []any{}, onlyExamples["examples"])
+	assert.Len(t, onlyExamples["examples"], 1)
+
+	_, err = Parse(data)
+	require.NoError(t, err, "schema examples in map form must parse after normalization")
+}
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 1f652934..8d1a791f 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -8,6 +8,7 @@ import (
 	"maps"
 	"net/url"
 	"os"
+	"path/filepath"
 	"regexp"
 	"slices"
 	"sort"
@@ -147,19 +148,63 @@ func Parse(data []byte) (*spec.APISpec, error) {
 	return parse(data, false)
 }
 
+// ParseFile parses an OpenAPI spec from a file and resolves local external
+// refs relative to that file.
+func ParseFile(path string) (*spec.APISpec, error) {
+	return parseFile(path, false)
+}
+
+// ParseWithPath parses OpenAPI spec bytes and resolves local external refs
+// relative to the given file path.
+func ParseWithPath(data []byte, path string) (*spec.APISpec, error) {
+	return parseWithPath(data, path, false)
+}
+
 // ParseLenient parses an OpenAPI spec, skipping validation errors from broken $refs.
 // It logs warnings to stderr for any issues found but continues parsing.
 func ParseLenient(data []byte) (*spec.APISpec, error) {
 	return parse(data, true)
 }
 
+// ParseFileLenient parses an OpenAPI spec from a file and skips validation
+// errors from broken $refs after resolving local external refs relative to
+// that file.
+func ParseFileLenient(path string) (*spec.APISpec, error) {
+	return parseFile(path, true)
+}
+
+// ParseWithPathLenient parses OpenAPI spec bytes, resolving local external refs
+// relative to the given file path and skipping validation errors from broken
+// refs.
+func ParseWithPathLenient(data []byte, path string) (*spec.APISpec, error) {
+	return parseWithPath(data, path, true)
+}
+
+func parseFile(path string, lenient bool) (*spec.APISpec, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("reading OpenAPI spec: %w", err)
+	}
+	return parseWithPath(data, path, lenient)
+}
+
+func parseWithPath(data []byte, path string, lenient bool) (*spec.APISpec, error) {
+	location, err := fileLocation(path)
+	if err != nil {
+		return nil, err
+	}
+	return parseWithLocation(data, lenient, location)
+}
+
 func parse(data []byte, lenient bool) (*spec.APISpec, error) {
+	return parseWithLocation(data, lenient, nil)
+}
+
+func parseWithLocation(data []byte, lenient bool, location *url.URL) (*spec.APISpec, error) {
 	if normalized, err := normalizeSpecData(data); err == nil {
 		data = normalized
 	}
-	loader := openapi3.NewLoader()
-	loader.IsExternalRefsAllowed = lenient
-	doc, err := loader.LoadFromData(data)
+	doc, err := loadOpenAPIDoc(data, lenient, location)
 	if err != nil {
 		if !lenient {
 			return nil, fmt.Errorf("loading OpenAPI spec: %w", err)
@@ -172,7 +217,7 @@ func parse(data []byte, lenient bool) (*spec.APISpec, error) {
 				break // stripBrokenRefs couldn't remove anything
 			}
 			data = cleaned
-			doc, err = loader.LoadFromData(data)
+			doc, err = loadOpenAPIDoc(data, lenient, location)
 		}
 		if err != nil {
 			return nil, fmt.Errorf("loading OpenAPI spec (even after cleanup): %w", err)
@@ -689,6 +734,49 @@ func authVarsExtension(raw any) ([]spec.AuthEnvVar, error) {
 	return out, nil
 }
 
+func loadOpenAPIDoc(data []byte, lenient bool, location *url.URL) (*openapi3.T, error) {
+	loader := openapi3.NewLoader()
+	loader.IsExternalRefsAllowed = lenient || location != nil
+	allowLocalExternalRefs := location != nil
+	loader.ReadFromURIFunc = func(loader *openapi3.Loader, refLocation *url.URL) ([]byte, error) {
+		if !lenient {
+			if !allowLocalExternalRefs || !isFileURI(refLocation) {
+				return nil, fmt.Errorf("encountered disallowed external reference: %q", refLocation.String())
+			}
+		}
+		data, err := openapi3.DefaultReadFromURI(loader, refLocation)
+		if err != nil {
+			return nil, err
+		}
+		if normalized, err := normalizeSpecData(data); err == nil {
+			return normalized, nil
+		}
+		return data, nil
+	}
+	if location != nil {
+		return loader.LoadFromDataWithPath(data, location)
+	}
+	return loader.LoadFromData(data)
+}
+
+func isFileURI(location *url.URL) bool {
+	return location != nil && location.Path != "" && location.Host == "" &&
+		(location.Scheme == "" || location.Scheme == "file")
+}
+
+func fileLocation(path string) (*url.URL, error) {
+	abs, err := filepath.Abs(path)
+	if err != nil {
+		return nil, fmt.Errorf("resolving OpenAPI spec path: %w", err)
+	}
+	return &url.URL{Scheme: "file", Path: filepath.ToSlash(abs)}, nil
+}
+
+// IsRemoteSpecSource reports whether a spec source should be loaded as a URL.
+func IsRemoteSpecSource(source string) bool {
+	return strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://")
+}
+
 func requiredStringField(m map[string]any, name string) (string, bool) {
 	raw, ok := m[name]
 	if !ok {
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index db47a5ce..472b541c 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -50,6 +50,90 @@ func TestParsePetstore(t *testing.T) {
 	assert.Contains(t, parsed.Types, "Pet")
 }
 
+func TestParseFileResolvesLocalRefsRelativeToSpecDir(t *testing.T) {
+	t.Parallel()
+
+	dir := t.TempDir()
+	appsDir := filepath.Join(dir, "apps")
+	commonDir := filepath.Join(dir, "common")
+	require.NoError(t, os.MkdirAll(appsDir, 0o755))
+	require.NoError(t, os.MkdirAll(commonDir, 0o755))
+
+	require.NoError(t, os.WriteFile(filepath.Join(commonDir, "schemas.json"), []byte(`{
+  "components": {
+    "schemas": {
+      "Widget": {
+        "type": "object",
+        "properties": {
+          "id": {"type": "string"}
+        }
+      }
+    }
+  }
+}`), 0o644))
+
+	specPath := filepath.Join(appsDir, "openapi.yaml")
+	require.NoError(t, os.WriteFile(specPath, []byte(`
+openapi: 3.0.3
+info:
+  title: Modular Widgets
+  version: 1.0.0
+servers:
+  - url: https://api.example.com
+paths:
+  /widgets:
+    get:
+      operationId: listWidgets
+      responses:
+        "200":
+          description: ok
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: "../common/schemas.json#/components/schemas/Widget"
+`), 0o644))
+
+	parsed, err := ParseFile(specPath)
+	require.NoError(t, err)
+
+	var foundWidgetID bool
+	for _, typ := range parsed.Types {
+		for _, field := range typ.Fields {
+			if field.Name == "id" && field.Type == "string" {
+				foundWidgetID = true
+			}
+		}
+	}
+	assert.True(t, foundWidgetID, "external schema fields must be available after local ref resolution")
+}
+
+func TestParseWithPathRejectsRemoteRefsInStrictMode(t *testing.T) {
+	t.Parallel()
+
+	data := []byte(`
+openapi: 3.0.3
+info:
+  title: Remote Ref
+  version: 1.0.0
+paths:
+  /widgets:
+    get:
+      operationId: listWidgets
+      responses:
+        "200":
+          description: ok
+          content:
+            application/json:
+              schema:
+                $ref: "https://example.com/schemas.json#/components/schemas/Widget"
+`)
+
+	_, err := ParseWithPath(data, filepath.Join(t.TempDir(), "openapi.yaml"))
+	require.ErrorContains(t, err, "encountered disallowed external reference")
+}
+
 func TestParsePreservesResponseDiscriminatorAndEnumFields(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/pipeline/dogfood.go b/internal/pipeline/dogfood.go
index 55916af0..1e2ce634 100644
--- a/internal/pipeline/dogfood.go
+++ b/internal/pipeline/dogfood.go
@@ -608,19 +608,20 @@ func writeDogfoodResults(report *DogfoodReport, dir string) error {
 }
 
 func loadDogfoodOpenAPISpec(specPath string) (*openAPISpec, error) {
-	// Try internal YAML spec format first (starts with "name:" + "resources:").
-	if internal, err := tryLoadInternalYAMLSpec(specPath); err != nil {
-		return nil, err
-	} else if internal != nil {
-		return internalSpecToDogfoodSpec(internal), nil
-	}
-
 	data, err := os.ReadFile(specPath)
 	if err != nil {
 		return nil, fmt.Errorf("reading spec: %w", err)
 	}
 
-	parsed, parseErr := openapiparser.ParseLenient(data)
+	if isInternalYAMLSpec(data) {
+		internal, err := apispec.ParseBytes(data)
+		if err != nil {
+			return nil, fmt.Errorf("parsing internal YAML spec: %w", err)
+		}
+		return internalSpecToDogfoodSpec(internal), nil
+	}
+
+	parsed, parseErr := openapiparser.ParseWithPathLenient(data, specPath)
 	if parseErr == nil {
 		return &openAPISpec{
 			Paths: collectDogfoodSpecPaths(parsed.Resources),
diff --git a/internal/pipeline/mcpsync/sync.go b/internal/pipeline/mcpsync/sync.go
index 2bc691e1..4f9cf449 100644
--- a/internal/pipeline/mcpsync/sync.go
+++ b/internal/pipeline/mcpsync/sync.go
@@ -210,7 +210,7 @@ func loadArchivedSpec(cliDir string) (*spec.APISpec, error) {
 			return nil, fmt.Errorf("reading %s: %w", path, err)
 		}
 		if openapi.IsOpenAPI(data) {
-			return openapi.ParseLenient(data)
+			return openapi.ParseWithPathLenient(data, path)
 		}
 		if graphql.IsGraphQLSDL(data) {
 			return graphql.ParseSDLBytes(path, data)
diff --git a/internal/pipeline/publish.go b/internal/pipeline/publish.go
index ecf0bd87..41dd04fc 100644
--- a/internal/pipeline/publish.go
+++ b/internal/pipeline/publish.go
@@ -253,7 +253,7 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
 		)
 		switch m.SpecFormat {
 		case "openapi3":
-			parsed, parseErr = openapi.Parse(data)
+			parsed, parseErr = openapi.ParseWithPath(data, specFile)
 		case "graphql":
 			parsed, parseErr = graphql.ParseSDLBytes(specFile, data)
 		case "internal":
diff --git a/internal/pipeline/spec_detect.go b/internal/pipeline/spec_detect.go
index 936c25a5..c8533f83 100644
--- a/internal/pipeline/spec_detect.go
+++ b/internal/pipeline/spec_detect.go
@@ -3,7 +3,6 @@ package pipeline
 import (
 	"bytes"
 	"fmt"
-	"os"
 	"slices"
 	"strings"
 
@@ -179,22 +178,3 @@ func collectInternalResourcePaths(r apispec.Resource, paths *[]string) {
 		collectInternalResourcePaths(sub, paths)
 	}
 }
-
-// tryLoadInternalYAMLSpec reads specPath and, if it's an internal YAML spec,
-// parses it and returns the APISpec. Returns nil, nil if not internal YAML.
-func tryLoadInternalYAMLSpec(specPath string) (*apispec.APISpec, error) {
-	data, err := os.ReadFile(specPath)
-	if err != nil {
-		return nil, fmt.Errorf("reading spec: %w", err)
-	}
-
-	if !isInternalYAMLSpec(data) {
-		return nil, nil
-	}
-
-	parsed, err := apispec.ParseBytes(data)
-	if err != nil {
-		return nil, fmt.Errorf("parsing internal YAML spec: %w", err)
-	}
-	return parsed, nil
-}

← a9b9aa65 feat(cli): emit AGENTS.md for printed CLIs (fast-track of #6  ·  back to Cli Printing Press  ·  fix(cli): score structural scorer behavior (#732) 979510c2 →