From 1300133d746a10c9fa4bd3c9a92f5580ab36ef83 Mon Sep 17 00:00:00 2001 From: Kevin Tang <73975146+vt128@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:41:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[feat]=20json:=20validate/try=5Fvalidate=20?= =?UTF-8?q?=E2=80=94=20JSON=20Schema=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds json.validate / json.try_validate to lib/json (the fourth v0.2.1 increment; resolves the PKG-19 need inside the json module per user decision): check a document against a JSON Schema, drafts 4/6/7/2019-09/ 2020-12 auto-detected from $schema. data and schema each accept a JSON string/bytes or a Starlark value, so a schema can be written as a dict literal. validate returns None or fails with one line per violation, each prefixed with the JSON Pointer of the offending location ("at /age: must be >= 0 but found -3"). try_validate distinguishes three outcomes: (True, None) valid, (False, details) invalid, (None, error) when validation could not run. Engine: github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 as a module dependency — a deliberate, evaluated exception to the vendoring pattern: its go.mod is exactly go1.19 with ZERO requirements, so go.sum gains two lines and no transitive deps; Apache-2.0; measured +256 KiB on the go1.19 floor (Docker, -trimpath -s -w), under the +312 KiB the module survey already accepted for this capability. Hand-written validation DSLs stay rejected per that survey. Purity and stability: all external $ref loading (file:// and network) is blocked via the Compiler's LoadURL hook — schemas must be self-contained. A panic audit of the engine found no script-reachable panic (Must* helpers are init-only and unused; Validate recovers InfiniteLoopError/ InvalidJSONTypeError into errors, which the wrapper also handles), backed by hostile-input tests: self-referential $ref, invalid pattern regex, 200-deep nesting, uniqueItems hashing — error, never panic. Compiled schemas are cached (bounded, dropped wholesale on overflow so scripts cannot grow memory unboundedly). Tests are a section in json_test.go (no new file, no third-party test framework): conforming/violating documents, pointer-carrying messages, the three try_ outcomes, draft-7 via $schema, blocked $refs, cache eviction churn, the violation-list cap, and argument errors. -race -count=2 and the go1.19 floor are green. Co-Authored-By: Claude Fable 5 --- go.mod | 1 + go.sum | 2 + lib/json/README.md | 44 ++++++++++ lib/json/json.go | 24 +++--- lib/json/json_test.go | 188 ++++++++++++++++++++++++++++++++++++++++++ lib/json/validate.go | 150 +++++++++++++++++++++++++++++++++ 6 files changed, 398 insertions(+), 11 deletions(-) create mode 100644 lib/json/validate.go diff --git a/go.mod b/go.mod index 38d7122..49e9f47 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/h2so5/here v0.0.0-20200815043652-5e14eb691fae github.com/montanaflynn/stats v0.7.1 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/spyzhov/ajson v0.9.6 go.starlark.net v0.0.0-20260324133313-ffb3f39dd27a go.uber.org/atomic v1.11.0 diff --git a/go.sum b/go.sum index ea0f454..72ec90c 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= diff --git a/lib/json/README.md b/lib/json/README.md index 242dd36..eb35a43 100644 --- a/lib/json/README.md +++ b/lib/json/README.md @@ -329,3 +329,47 @@ print("Error:", error) # Result: {"a": 1} # Error: None ``` + +### `validate(data, schema) None` + +The validate function checks a JSON document against a [JSON Schema](https://json-schema.org) (drafts 4, 6, 7, 2019-09 and 2020-12, detected from the `$schema` keyword; 2020-12 by default). It accepts two positional arguments — both may be a JSON string, bytes, or a Starlark value (dict, list, etc.): +- data: the document to check +- schema: the JSON Schema + +It returns None when the data conforms. When the data is invalid, it fails with a message listing each violation prefixed by its [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) location, e.g. `at /age: must be >= 0 but found -3`. + +Schemas must be **self-contained**: a `$ref` to an external resource (a file or the network) is an error. Compiled schemas are cached, so repeated validation against the same schema text has no recompilation cost. + +#### Examples + +**Basic** + +Validate a decoded value against a schema written as a Starlark dict. + +```python +load('json', 'validate') +schema = {'type': 'object', 'required': ['name'], 'properties': {'name': {'type': 'string'}, 'age': {'type': 'integer', 'minimum': 0}}} +print(validate({'name': 'Ann', 'age': 3}, schema)) +# Output: None +``` + +### `try_validate(data, schema) tuple` + +The try_validate function is a variant of validate that distinguishes three outcomes instead of aborting: +- `(True, None)` — the data conforms to the schema. +- `(False, details)` — the data was checked and is invalid; details lists the violations with their JSON Pointer locations. +- `(None, error)` — validation could not run at all (invalid schema, malformed JSON text, or bad arguments). + +#### Examples + +**Basic** + +```python +load('json', 'try_validate') +ok, err = try_validate('{"age": -3}', '{"type":"object","properties":{"age":{"type":"integer","minimum":0}}}') +print("OK:", ok) +print("Error:", err) +# Output: +# OK: False +# Error: at /age: must be >= 0 but found -3 +``` diff --git a/lib/json/json.go b/lib/json/json.go index 5a6d42b..efe8349 100644 --- a/lib/json/json.go +++ b/lib/json/json.go @@ -31,17 +31,19 @@ func LoadModule() (starlark.StringDict, error) { mod := starlarkstruct.Module{ Name: ModuleName, Members: starlark.StringDict{ - "dumps": starlark.NewBuiltin(ModuleName+".dumps", dumps), - "try_dumps": starlark.NewBuiltin(ModuleName+".try_dumps", tryDumps), - "try_encode": starlark.NewBuiltin(ModuleName+".try_encode", tryEncode), - "try_decode": starlark.NewBuiltin(ModuleName+".try_decode", tryDecode), - "try_indent": starlark.NewBuiltin(ModuleName+".try_indent", tryIndent), - "path": starlark.NewBuiltin(ModuleName+".path", generateJsonPath(false)), - "try_path": starlark.NewBuiltin(ModuleName+".try_path", generateJsonPath(true)), - "eval": starlark.NewBuiltin(ModuleName+".eval", generateJsonEval(false)), - "try_eval": starlark.NewBuiltin(ModuleName+".try_eval", generateJsonEval(true)), - "repair": starlark.NewBuiltin(ModuleName+".repair", generateRepair(false)), - "try_repair": starlark.NewBuiltin(ModuleName+".try_repair", generateRepair(true)), + "dumps": starlark.NewBuiltin(ModuleName+".dumps", dumps), + "try_dumps": starlark.NewBuiltin(ModuleName+".try_dumps", tryDumps), + "try_encode": starlark.NewBuiltin(ModuleName+".try_encode", tryEncode), + "try_decode": starlark.NewBuiltin(ModuleName+".try_decode", tryDecode), + "try_indent": starlark.NewBuiltin(ModuleName+".try_indent", tryIndent), + "path": starlark.NewBuiltin(ModuleName+".path", generateJsonPath(false)), + "try_path": starlark.NewBuiltin(ModuleName+".try_path", generateJsonPath(true)), + "eval": starlark.NewBuiltin(ModuleName+".eval", generateJsonEval(false)), + "try_eval": starlark.NewBuiltin(ModuleName+".try_eval", generateJsonEval(true)), + "repair": starlark.NewBuiltin(ModuleName+".repair", generateRepair(false)), + "try_repair": starlark.NewBuiltin(ModuleName+".try_repair", generateRepair(true)), + "validate": starlark.NewBuiltin(ModuleName+".validate", generateValidate(false)), + "try_validate": starlark.NewBuiltin(ModuleName+".try_validate", generateValidate(true)), }, } for k, v := range stdjson.Module.Members { diff --git a/lib/json/json_test.go b/lib/json/json_test.go index 467acdc..73d35aa 100644 --- a/lib/json/json_test.go +++ b/lib/json/json_test.go @@ -978,3 +978,191 @@ func TestJSONRepair(t *testing.T) { }) } } + +func TestJSONValidate(t *testing.T) { + tests := []struct { + name string + script string + wantErr string + }{ + { + name: `validate: conforming data returns None`, + script: itn.HereDoc(` + load('json', 'validate') + schema = '{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"age":{"type":"integer","minimum":0}}}' + assert.eq(validate('{"name":"Ann","age":3}', schema), None) + `), + }, + { + name: `validate: schema and data as starlark values`, + script: itn.HereDoc(` + load('json', 'validate') + schema = {'type': 'object', 'required': ['name'], 'properties': {'name': {'type': 'string'}}} + assert.eq(validate({'name': 'Ann'}, schema), None) + assert.eq(validate([1, 2, 3], {'type': 'array', 'items': {'type': 'integer'}}), None) + `), + }, + { + name: `validate: violation message carries the JSON pointer`, + script: itn.HereDoc(` + load('json', 'validate') + schema = '{"type":"object","properties":{"age":{"type":"integer","minimum":0}}}' + validate('{"age":-3}', schema) + `), + wantErr: `at /age: must be >= 0`, + }, + { + name: `try_validate: the three outcomes`, + script: itn.HereDoc(` + load('json', 'try_validate') + schema = '{"type":"object","required":["name"]}' + ok, err = try_validate('{"name":"a"}', schema) + assert.eq(ok, True) + assert.eq(err, None) + bad, err2 = try_validate('{}', schema) + assert.eq(bad, False) + assert.true('missing properties' in err2) + cant, err3 = try_validate('{}', 'not a schema at all') + assert.eq(cant, None) + assert.true(err3 != None) + `), + }, + { + name: `validate: draft-7 schema via $schema`, + script: itn.HereDoc(` + load('json', 'validate') + schema = '{"$schema":"http://json-schema.org/draft-07/schema#","type":"string"}' + assert.eq(validate('"hello"', schema), None) + `), + }, + { + name: `validate: external file $ref is blocked`, + script: itn.HereDoc(` + load('json', 'validate') + validate('{}', '{"$ref":"file:///etc/passwd"}') + `), + wantErr: `not allowed`, + }, + { + name: `validate: external http $ref is blocked`, + script: itn.HereDoc(` + load('json', 'validate') + validate('{}', '{"$ref":"http://example.com/s.json"}') + `), + wantErr: `not allowed`, + }, + { + name: `validate: malformed data text cannot run`, + script: itn.HereDoc(` + load('json', 'validate') + validate('{not json', '{"type":"object"}') + `), + wantErr: `invalid data`, + }, + { + name: `validate: bad schema cannot run`, + script: itn.HereDoc(` + load('json', 'validate') + validate('{}', '{"type":"nope"}') + `), + wantErr: `invalid schema`, + }, + { + name: `validate: long violation list is capped`, + script: itn.HereDoc(` + load('json', 'try_validate') + schema = '{"type":"array","items":{"type":"integer"}}' + data = '["a","b","c","d","e","f","g","h","i","j","k","l"]' + ok, err = try_validate(data, schema) + assert.eq(ok, False) + assert.true('and' in err and 'more' in err) + `), + }, + { + name: `validate: compiled schema cache hit`, + script: itn.HereDoc(` + load('json', 'validate') + schema = '{"type":"integer"}' + assert.eq(validate('1', schema), None) + assert.eq(validate('2', schema), None) + `), + }, + { + name: `validate: missing arguments`, + script: itn.HereDoc(` + load('json', 'validate') + validate('{}') + `), + wantErr: `json.validate: missing argument for schema`, + }, + { + name: `try_validate: missing arguments`, + script: itn.HereDoc(` + load('json', 'try_validate') + v, err = try_validate() + assert.eq(v, None) + assert.true('missing argument' in err) + `), + }, + { + name: `validate: unserializable data value cannot run`, + script: itn.HereDoc(` + load('json', 'validate') + validate(lambda x: x, '{"type":"object"}') + `), + wantErr: `json.validate:`, + }, + { + name: `validate: many distinct schemas exercise cache eviction`, + script: itn.HereDoc(` + load('json', 'validate') + def churn(): + for i in range(70): + schema = '{"type":"object","maxProperties":' + str(i + 1) + '}' + assert.eq(validate('{}', schema), None) + churn() + `), + }, + { + name: `robustness: self-referential $ref errors, no panic`, + script: itn.HereDoc(` + load('json', 'try_validate') + ok, err = try_validate('{"a":{"a":{"a":{}}}}', '{"$ref":"#"}') + assert.true(ok == None or ok == True or ok == False) + `), + }, + { + name: `robustness: invalid pattern regex errors, no panic`, + script: itn.HereDoc(` + load('json', 'validate') + validate('"x"', '{"type":"string","pattern":"("}') + `), + wantErr: `invalid schema`, + }, + { + name: `robustness: deeply nested data validates, no panic`, + script: itn.HereDoc(` + load('json', 'validate') + deep = '[' * 200 + ']' * 200 + assert.eq(validate(deep, '{}'), None) + `), + }, + { + name: `robustness: uniqueItems over objects, no panic`, + script: itn.HereDoc(` + load('json', 'try_validate') + ok, err = try_validate('[{"a":1},{"a":1}]', '{"type":"array","uniqueItems":true}') + assert.eq(ok, False) + assert.true(err != None) + `), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := itn.ExecModuleWithErrorTest(t, json.ModuleName, json.LoadModule, tt.script, tt.wantErr, nil) + if (err != nil) != (tt.wantErr != "") { + t.Errorf("json(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) + } + }) + } +} diff --git a/lib/json/validate.go b/lib/json/validate.go new file mode 100644 index 0000000..a803664 --- /dev/null +++ b/lib/json/validate.go @@ -0,0 +1,150 @@ +package json + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "sync" + + "github.com/santhosh-tekuri/jsonschema/v5" + "go.starlark.net/starlark" +) + +// maxCachedSchemas bounds the compiled-schema cache: scripts can mint +// unbounded distinct schema texts, and compiled schemas hold their full +// structure. On overflow the cache is dropped wholesale (compilation is +// cheap relative to unbounded growth). +const maxCachedSchemas = 64 + +// maxValidateErrors caps how many violations are listed in a validation +// message; the remainder is summarized. +const maxValidateErrors = 10 + +var ( + schemaCacheMu sync.Mutex + schemaCache = make(map[string]*jsonschema.Schema) +) + +// compileSchema compiles (with caching) a JSON Schema from its text. $ref to +// external resources — files or the network — is blocked, keeping the json +// module pure: a schema must be self-contained. +func compileSchema(text string) (*jsonschema.Schema, error) { + schemaCacheMu.Lock() + defer schemaCacheMu.Unlock() + if s, ok := schemaCache[text]; ok { + return s, nil + } + c := jsonschema.NewCompiler() + c.LoadURL = func(s string) (io.ReadCloser, error) { + return nil, fmt.Errorf("external $ref %q not allowed: schemas must be self-contained", s) + } + if err := c.AddResource("inline://schema", strings.NewReader(text)); err != nil { + return nil, err + } + s, err := c.Compile("inline://schema") + if err != nil { + return nil, err + } + if len(schemaCache) >= maxCachedSchemas { + schemaCache = make(map[string]*jsonschema.Schema) + } + schemaCache[text] = s + return s, nil +} + +// formatValidationError flattens a validation result into one line per +// violation, each prefixed with the JSON Pointer of the offending location. +func formatValidationError(ve *jsonschema.ValidationError) string { + var lines []string + for _, u := range ve.BasicOutput().Errors { + if u.KeywordLocation == "" { // the root "doesn't validate with ..." wrapper + continue + } + loc := u.InstanceLocation + if loc == "" { + loc = "/" + } + lines = append(lines, fmt.Sprintf("at %s: %s", loc, u.Error)) + } + if len(lines) > maxValidateErrors { + rest := len(lines) - maxValidateErrors + lines = append(lines[:maxValidateErrors], fmt.Sprintf("... and %d more", rest)) + } + return strings.Join(lines, "\n") +} + +// generateValidate builds json.validate / json.try_validate, checking a JSON +// document against a JSON Schema (drafts 4/6/7/2019-09/2020-12, detected from +// $schema; default 2020-12). data and schema each accept a JSON string/bytes +// or a Starlark value. +// +// validate returns None when the data conforms and fails with a +// pointer-per-violation message otherwise. try_validate distinguishes the +// three outcomes: (True, None) valid, (False, details) invalid, and +// (None, error) when validation could not run (bad schema, bad arguments, +// malformed JSON text). +func generateValidate(try bool) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) { + return func(_ *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var data, schema starlark.Value + if err := starlark.UnpackArgs(fn.Name(), args, kwargs, "data", &data, "schema", &schema); err != nil { + return failValidate(try, err, fn) + } + compiled, doc, err := prepareValidation(data, schema) + if err != nil { + return failValidate(try, err, fn) + } + + verr := compiled.Validate(doc) + if verr == nil { + if try { + return starlark.Tuple{starlark.True, none}, nil + } + return none, nil + } + ve, ok := verr.(*jsonschema.ValidationError) + if !ok { + return failValidate(try, verr, fn) + } + details := formatValidationError(ve) + if try { + return starlark.Tuple{starlark.False, starlark.String(details)}, nil + } + return none, fmt.Errorf("%s: data does not conform to the schema:\n%s", fn.Name(), details) + } +} + +// prepareValidation resolves the schema (compiled, cached) and decodes the +// data document; any error here means validation could not run at all. +func prepareValidation(data, schema starlark.Value) (*jsonschema.Schema, interface{}, error) { + schemaBytes, err := getJsonBytes(schema) + if err != nil { + return nil, nil, fmt.Errorf("invalid schema: %w", err) + } + compiled, err := compileSchema(string(schemaBytes)) + if err != nil { + return nil, nil, fmt.Errorf("invalid schema: %w", err) + } + dataBytes, err := getJsonBytes(data) + if err != nil { + return nil, nil, err + } + dec := json.NewDecoder(bytes.NewReader(dataBytes)) + dec.UseNumber() + var doc interface{} + if err := dec.Decode(&doc); err != nil { + return nil, nil, fmt.Errorf("invalid data: %w", err) + } + return compiled, doc, nil +} + +// failValidate shapes a cannot-run failure (bad schema/arguments/JSON): the +// try_ variant returns (None, message) — distinct from (False, details) for +// data that was validated and found invalid. +func failValidate(try bool, err error, fn *starlark.Builtin) (starlark.Value, error) { + if try { + return starlark.Tuple{none, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("%s: %w", fn.Name(), err) +} From 74389cf213863c6830c6d741dabf156c80ce2823 Mon Sep 17 00:00:00 2001 From: Kevin Tang <73975146+vt128@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:42:47 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[doc]=20CLAUDE.md:=20license=20hygiene=20ca?= =?UTF-8?q?ps=20vendoring=20=E2=80=94=20same-license=20only;=20differently?= =?UTF-8?q?-licensed=20permissive=20deps=20go=20through=20the=20module-dep?= =?UTF-8?q?endency=20evaluation=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the jsonschema decision rationale: vendoring Apache-2.0 source into this MIT repository would make it mixed-license, so the engine is a module dependency instead — acceptable because its go.mod is exactly go1.19, it has zero requirements, and the binary delta measured +256 KiB on the floor toolchain. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2975a78..0651681 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,9 @@ A new module must satisfy **all three**, otherwise it goes to `starpkg/*`: 2. **Universally needed.** Broad, domain-neutral utility. Domain modules (sqlite, web, llm, mq, s3…) are starpkg's job no matter how clean they are. 3. **Zero third-party dependencies.** Stdlib-only (or an extension of an existing core module). Any `go.sum` entry is inherited by every downstream — one third-party requirement sends the module to starpkg. -**The vendoring exception** (the `lib/json/internal/jsonrepair` precedent): a frozen, permissively-licensed, **stdlib-only** third-party runtime may be vendored under `lib//internal//` to keep `go.sum` clean, when the capability is judged worth it. Requirements: pin to a specific upstream release (record it), copy runtime `.go` files only (no `_test.go`, no upstream test deps), keep the upstream LICENSE in the directory, add a `doc.go` stating provenance + "do not edit by hand; re-vendor to update", golden-lock the observed behavior in our tests, and exclude the path in `codecov.yml` and `.codacy.yml`. Measure the binary delta in the go1.19 container before committing to it. +**The vendoring exception** (the `lib/json/internal/jsonrepair` precedent): a frozen, **same-license (MIT)**, **stdlib-only** third-party runtime may be vendored under `lib//internal//` to keep `go.sum` clean, when the capability is judged worth it. Requirements: pin to a specific upstream release (record it), copy runtime `.go` files only (no `_test.go`, no upstream test deps), keep the upstream LICENSE in the directory, add a `doc.go` stating provenance + "do not edit by hand; re-vendor to update", golden-lock the observed behavior in our tests, and exclude the path in `codecov.yml` and `.codacy.yml`. Measure the binary delta in the go1.19 container before committing to it. + +**License hygiene caps vendoring.** Never vendor differently-licensed source — even permissive (Apache-2.0) — into this MIT repository: the copied files keep their license and the repo becomes mixed-license. For a capability worth a differently-licensed library, use a **module dependency** instead, and only when it passes the evaluation bar: its go.mod must not exceed this repo's Go floor, it should bring zero (or near-zero) transitive dependencies into `go.sum`, the binary delta is measured in the go1.19 container, and its panic surface is audited + hostile-input tested (the `lib/json` jsonschema decision: Apache-2.0, go1.19 exactly, zero requires, +256 KiB measured → module dep, repo stays pure MIT). **Python-parity rule.** If a module mirrors a Python stdlib API (`regex` ⇒ `re`), the shapes must match CPython exactly — signatures, return types (`findall`/`split` return **lists**, not tuples — a real bug class), group shaping, flag values. Where the Go engine genuinely can't (RE2: lookaround, backreferences), **fail to compile with a clear error**; never silently approximate. Same-name-different-shape is worse than absent.