xp-libs-analytics 0.4.0__py3-none-any.whl

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 (65) hide show
  1. xp_libs_analytics/__init__.py +18 -0
  2. xp_libs_analytics/engine/__init__.py +1 -0
  3. xp_libs_analytics/engine/backend.py +379 -0
  4. xp_libs_analytics/engine/planner.py +450 -0
  5. xp_libs_analytics/engine/worker.py +121 -0
  6. xp_libs_analytics/errors/__init__.py +43 -0
  7. xp_libs_analytics/errors/catalog.json +75 -0
  8. xp_libs_analytics/errors/codes.py +81 -0
  9. xp_libs_analytics/ingest/__init__.py +309 -0
  10. xp_libs_analytics/interfaces.py +70 -0
  11. xp_libs_analytics/matching/__init__.py +1 -0
  12. xp_libs_analytics/matching/compiler.py +344 -0
  13. xp_libs_analytics/matching/diagnostics.py +115 -0
  14. xp_libs_analytics/matching/models.py +98 -0
  15. xp_libs_analytics/matching/normalize.py +16 -0
  16. xp_libs_analytics/matching/patterns.py +71 -0
  17. xp_libs_analytics/mcp/__init__.py +1 -0
  18. xp_libs_analytics/mcp/server.py +248 -0
  19. xp_libs_analytics/models/__init__.py +654 -0
  20. xp_libs_analytics/parser/__init__.py +142 -0
  21. xp_libs_analytics/policy/__init__.py +69 -0
  22. xp_libs_analytics/py.typed +0 -0
  23. xp_libs_analytics/registry.py +204 -0
  24. xp_libs_analytics/sdk/__init__.py +259 -0
  25. xp_libs_analytics/skills/generic-analytics/SKILL.md +236 -0
  26. xp_libs_analytics/skills/generic-analytics/examples/README.md +69 -0
  27. xp_libs_analytics/skills/generic-analytics/examples/queries/01_count_all.yaml +8 -0
  28. xp_libs_analytics/skills/generic-analytics/examples/queries/02_filter_and_count.yaml +15 -0
  29. xp_libs_analytics/skills/generic-analytics/examples/queries/03_group_stats_and_topk.yaml +21 -0
  30. xp_libs_analytics/skills/generic-analytics/examples/queries/04_nested_or_predicates.yaml +17 -0
  31. xp_libs_analytics/skills/generic-analytics/examples/queries/05_is_missing.yaml +11 -0
  32. xp_libs_analytics/skills/generic-analytics/examples/queries/06_missing_vs_null.yaml +17 -0
  33. xp_libs_analytics/skills/generic-analytics/examples/queries/07_conditional_aggregation.yaml +19 -0
  34. xp_libs_analytics/skills/generic-analytics/examples/queries/08_ratio_if.yaml +16 -0
  35. xp_libs_analytics/skills/generic-analytics/examples/queries/09_tumbling_window.yaml +23 -0
  36. xp_libs_analytics/skills/generic-analytics/examples/queries/10_sliding_window.yaml +32 -0
  37. xp_libs_analytics/skills/generic-analytics/examples/queries/11_explicit_cast.yaml +16 -0
  38. xp_libs_analytics/skills/generic-analytics/examples/queries/12_text_range_distinct.yaml +23 -0
  39. xp_libs_analytics/skills/generic-analytics/examples/queries/13_summary_statistics.yaml +15 -0
  40. xp_libs_analytics/skills/generic-analytics/examples/queries/14_nested_and_not.yaml +20 -0
  41. xp_libs_analytics/skills/generic-analytics/examples/queries/15_conditional_sum.yaml +13 -0
  42. xp_libs_analytics/skills/generic-analytics/examples/queries/16_mixed_types_cast.yaml +17 -0
  43. xp_libs_analytics/skills/generic-analytics/examples/queries/17_relative_last_hour.yaml +15 -0
  44. xp_libs_analytics/skills/generic-analytics/examples/queries/18_string_exact.yaml +11 -0
  45. xp_libs_analytics/skills/generic-analytics/examples/queries/19_string_normalization.yaml +15 -0
  46. xp_libs_analytics/skills/generic-analytics/examples/queries/20_prefix_suffix.yaml +16 -0
  47. xp_libs_analytics/skills/generic-analytics/examples/queries/21_whole_word.yaml +13 -0
  48. xp_libs_analytics/skills/generic-analytics/examples/queries/22_ilike.yaml +11 -0
  49. xp_libs_analytics/skills/generic-analytics/examples/queries/23_rust_regex.yaml +11 -0
  50. xp_libs_analytics/skills/generic-analytics/examples/queries/24_fuzzy.yaml +17 -0
  51. xp_libs_analytics/skills/generic-analytics/examples/queries/25_multi_field_matching.yaml +24 -0
  52. xp_libs_analytics/skills/generic-analytics/examples/queries/26_conditional_fuzzy.yaml +20 -0
  53. xp_libs_analytics/skills/generic-analytics/examples/queries/27_temporal_fuzzy.yaml +38 -0
  54. xp_libs_analytics/skills/generic-analytics/examples/queries/28_safe_matching_diagnostics.yaml +22 -0
  55. xp_libs_analytics/skills/generic-analytics/references/errors.md +55 -0
  56. xp_libs_analytics/skills/generic-analytics/references/query-language-full.md +306 -0
  57. xp_libs_analytics/skills/generic-analytics/references/query-language.md +39 -0
  58. xp_libs_analytics/skills/generic-analytics/references/string-matching-full.md +122 -0
  59. xp_libs_analytics/skills/generic-analytics/references/string-matching.md +11 -0
  60. xp_libs_analytics-0.4.0.dist-info/METADATA +199 -0
  61. xp_libs_analytics-0.4.0.dist-info/RECORD +65 -0
  62. xp_libs_analytics-0.4.0.dist-info/WHEEL +4 -0
  63. xp_libs_analytics-0.4.0.dist-info/entry_points.txt +2 -0
  64. xp_libs_analytics-0.4.0.dist-info/licenses/LICENSE +22 -0
  65. xp_libs_analytics-0.4.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +175 -0
@@ -0,0 +1,15 @@
1
+ # Literal substring after explicit Unicode/case normalization.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: contains
8
+ value: Peter
9
+ normalization:
10
+ case_sensitive: false
11
+ unicode: nfkc
12
+ trim: true
13
+ aggregations:
14
+ - op: count
15
+ as: events
@@ -0,0 +1,16 @@
1
+ # Literal prefix or suffix; each record contributes once.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - op: or
7
+ conditions:
8
+ - field: /actor/name
9
+ op: starts_with
10
+ value: Peter
11
+ - field: /actor/name
12
+ op: ends_with
13
+ value: Peter
14
+ aggregations:
15
+ - op: count
16
+ as: events
@@ -0,0 +1,13 @@
1
+ # Rust Unicode word boundaries; underscore remains a word character.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: word
8
+ value: Peter
9
+ normalization:
10
+ case_sensitive: false
11
+ aggregations:
12
+ - op: count
13
+ as: events
@@ -0,0 +1,11 @@
1
+ # Whole-value wildcard pattern, normalized case-insensitively.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: ilike
8
+ value: '%peter%'
9
+ aggregations:
10
+ - op: count
11
+ as: events
@@ -0,0 +1,11 @@
1
+ # Rust regex search with explicit absolute anchors; no Python regex execution.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: regex
8
+ value: \APeter(?:[-_]|\z)
9
+ aggregations:
10
+ - op: count
11
+ as: events
@@ -0,0 +1,17 @@
1
+ # Exhaustive ratio similarity >=85, not identity confidence.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: fuzzy
8
+ value: Peter
9
+ algorithm: ratio
10
+ min_score: 85
11
+ normalization:
12
+ case_sensitive: false
13
+ unicode: nfkc
14
+ trim: true
15
+ aggregations:
16
+ - op: count
17
+ as: events
@@ -0,0 +1,24 @@
1
+ # Any rule on any authorized field; no row multiplication.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - match:
7
+ fields:
8
+ - /actor/name
9
+ - /actor/email
10
+ patterns:
11
+ - op: word
12
+ value: Peter
13
+ - op: fuzzy
14
+ value: Pter
15
+ algorithm: ratio
16
+ min_score: 85
17
+ normalization:
18
+ case_sensitive: false
19
+ unicode: nfkc
20
+ trim: true
21
+ mode: any
22
+ aggregations:
23
+ - op: count
24
+ as: events
@@ -0,0 +1,20 @@
1
+ # Fuzzy selection inside a conditional aggregate preserves total-record denominator.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters: []
6
+ aggregations:
7
+ - op: count
8
+ as: all_events
9
+ - op: count_if
10
+ as: matching_events
11
+ where:
12
+ field: /actor/name
13
+ op: fuzzy
14
+ value: Peter
15
+ algorithm: ratio
16
+ min_score: 85
17
+ normalization:
18
+ case_sensitive: false
19
+ unicode: nfkc
20
+ trim: true
@@ -0,0 +1,38 @@
1
+ # Exhaustive matching plus time/status filters, five-minute windows, grouping and P95.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: fuzzy
8
+ value: Peter
9
+ algorithm: ratio
10
+ min_score: 85
11
+ normalization:
12
+ case_sensitive: false
13
+ unicode: nfkc
14
+ trim: true
15
+ - field: /status
16
+ op: eq
17
+ value: error
18
+ aggregations:
19
+ - op: count
20
+ as: events
21
+ - op: p95
22
+ field: /latency_ms
23
+ as: p95_latency
24
+ casts:
25
+ - field: /timestamp
26
+ to: datetime
27
+ format: rfc3339
28
+ on_error: reject
29
+ time_range:
30
+ field: /timestamp
31
+ start: '2026-11-06T10:00:00Z'
32
+ end: '2026-11-06T10:10:00Z'
33
+ window:
34
+ type: tumbling
35
+ size: 5m
36
+ group_by:
37
+ - field: /service
38
+ as: service
@@ -0,0 +1,22 @@
1
+ # Diagnostic top two is output-only; all matching rows remain in the aggregate.
2
+ version: analytics.query/v1
3
+ dataset:
4
+ name: example_data
5
+ filters:
6
+ - field: /actor/name
7
+ op: fuzzy
8
+ value: Peter
9
+ algorithm: ratio
10
+ min_score: 85
11
+ normalization:
12
+ case_sensitive: false
13
+ unicode: nfkc
14
+ trim: true
15
+ aggregations:
16
+ - op: count
17
+ as: events
18
+ options:
19
+ matching_details:
20
+ include: true
21
+ top_values: 2
22
+ include_raw_values: false
@@ -0,0 +1,55 @@
1
+ # Agent Error Handling for YAML Queries
2
+
3
+ The **only** runtime error message registry is `src/xp_libs_analytics/errors/catalog.json`. It maps each stable error code directly to one canonical string with a `Fix:` and `Example:`. Never copy messages to a second runtime module or generate free-form exceptions. Use `analytics_error_info(code)` to retrieve the canonical value.
4
+
5
+ Tool error JSON includes `error.code`, `error.message` (exact catalog lookup), `error.path` (YAML AST location such as `$.aggregations[0].op`), `retryable`, and optionally safe 1-based YAML `line` and `column` in `details`. Do not include raw YAML payload text or dataset records in errors.
6
+
7
+ ## Correct an unsupported aggregation
8
+
9
+ Invalid query (structurally plausible, but `average` isn't a V1 operator):
10
+
11
+ ```yaml
12
+ version: analytics.query/v1
13
+ dataset:
14
+ name: example_data
15
+ aggregations:
16
+ - op: average
17
+ field: /value
18
+ as: average_value
19
+ ```
20
+
21
+ `QUERY_UNKNOWN_OPERATOR` → replace with `avg`, revalidate:
22
+
23
+ ```yaml
24
+ version: analytics.query/v1
25
+ dataset:
26
+ name: example_data
27
+ aggregations:
28
+ - op: avg
29
+ field: /value
30
+ as: average_value
31
+ ```
32
+
33
+ ## Correct YAML syntax/format errors
34
+
35
+ - `QUERY_YAML_INVALID`: make one indented mapping, quote ambiguous strings.
36
+ - `QUERY_YAML_DUPLICATE_KEY`: remove a repeated key; never assume which repeated value should win.
37
+ - `QUERY_YAML_ALIAS_FORBIDDEN`, `QUERY_YAML_ANCHOR_FORBIDDEN`, `QUERY_YAML_TAG_FORBIDDEN`: express a small explicit mapping/list instead.
38
+ - `QUERY_YAML_MULTI_DOCUMENT`: one YAML document per call.
39
+ - `QUERY_YAML_ROOT_INVALID`: root must be mapping with `version`, `dataset`, `aggregations`.
40
+ - `QUERY_YAML_COMPLEXITY_LIMIT`: split/narrow query; do not try a recursive alias workaround.
41
+ - `QUERY_YAML_SCALAR_INVALID`: finite decimal numbers only; quote text.
42
+ - `QUERY_MALFORMED`: YAML parsed, but typed AST fields/types invalid; inspect supported model/schema.
43
+
44
+ ## Correct semantic errors
45
+
46
+ - `DATA_FIELD_UNKNOWN`: call `analytics_describe` and select an authorized JSON Pointer exactly.
47
+ - `DATA_FIELD_TYPE_CONFLICT`: add a permitted cast or choose compatible field. Never silently coerce.
48
+ - `QUERY_WINDOW_INVALID`: verify permitted size/step and add time range with explicit datetime conversion.
49
+ - `QUERY_HAVING_INVALID`, `QUERY_ORDER_INVALID`: refer only to declared result aliases.
50
+ - `ACCESS_*`: stop. Do not enumerate dataset names, hidden fields, tenants or scopes to work around denial.
51
+ - `RESOURCE_*`: simplify, reduce groups or time window overlap, check `retryable` before retry.
52
+
53
+ `NO_DATA` and `INSUFFICIENT_DATA` are **non-error statuses**. Report evidence honestly. See the complete recovery matrix in `../SKILL.md` and examples in `../../examples/queries/`.
54
+
55
+ Matching-specific repairs are indexed by the product skill and analytics://string-matching/v1. All MATCH_* codes still resolve through the single catalog using analytics_error_info. Candidate/comparison failures return no partial aggregate; raw-diagnostic denial is not permission to enumerate values; similarity is not verified identity.
@@ -0,0 +1,306 @@
1
+ # Analytics YAML Query Language v1 — Normative Contract
2
+
3
+ - **Version literal:** `analytics.query/v1`
4
+ - **Input:** one UTF-8 YAML query document referring to a previously admitted, immutable, authorized logical dataset name
5
+ - **Output:** `analytics.result/v1` JSON envelope
6
+ **Policy:** strict, typed, bounded, deterministic. Implement and test every operation listed here; do not silently ignore keys, unsupported options, or type conflicts.
7
+
8
+ ## 1. End-to-end example
9
+
10
+ Assume incoming arbitrary JSON data:
11
+
12
+ ```json
13
+ [
14
+ {"ts":"2026-11-06T10:00:05Z","tenant":"alpha","region":"west","metrics":{"delay":12.2},"status":"warning"},
15
+ {"ts":"2026-11-06T10:01:06Z","tenant":"alpha","region":"west","metrics":{"delay":16.9},"status":"error"},
16
+ {"ts":"2026-11-06T10:02:07Z","tenant":"alpha","region":"east","metrics":{"delay":null},"status":"ok"},
17
+ {"ts":"2026-11-06T10:03:08Z","tenant":"alpha","region":"west","status":"ok"}
18
+ ]
19
+ ```
20
+
21
+ Dataset schema is discovered; it is **not predefined**. An authorized app registers records under a logical name in an execution context. The **YAML query document** is:
22
+
23
+ ```yaml
24
+ version: analytics.query/v1
25
+ dataset:
26
+ name: example_data
27
+ casts:
28
+ - field: /ts
29
+ to: datetime
30
+ format: rfc3339
31
+ on_error: reject
32
+ time_range:
33
+ field: /ts
34
+ start: '2026-11-06T10:00:00Z'
35
+ end: '2026-11-06T11:00:00Z'
36
+ filters:
37
+ - field: /tenant
38
+ op: eq
39
+ value: alpha
40
+ window:
41
+ type: tumbling
42
+ size: 5m
43
+ group_by:
44
+ - field: /region
45
+ as: region
46
+ aggregations:
47
+ - op: count
48
+ as: records
49
+ - op: count_non_null
50
+ field: /metrics/delay
51
+ as: observed_delay
52
+ - op: missing_count
53
+ field: /metrics/delay
54
+ as: missing_delay
55
+ - op: avg
56
+ field: /metrics/delay
57
+ as: avg_delay
58
+ - op: p95
59
+ field: /metrics/delay
60
+ as: p95_delay
61
+ - op: count_if
62
+ where:
63
+ field: /status
64
+ op: eq
65
+ value: error
66
+ as: errors
67
+ having:
68
+ - field: records
69
+ op: gte
70
+ value: 1
71
+ order_by:
72
+ - field: window_start
73
+ direction: asc
74
+ - field: region
75
+ direction: asc
76
+ limit: 100
77
+ ```
78
+
79
+ > The logical name `example_data` must be registered by the application in the current context, or selected from analytics_list_datasets. It is independent of Python variables and internal snapshot IDs. Absolute time examples are fixed for repeatable tests. Timestamp parsing is explicitly declared because JSON strings are not intrinsically datetimes.
80
+
81
+ ## 1.1 YAML document format and trust boundary
82
+
83
+ The query **authoring format** is YAML. Input record formats remain JSON/NDJSON/Python mappings; results and error envelopes remain strict JSON. MCP itself uses JSON-RPC transport; the tool parameter named `query_yaml` is a **string containing YAML text**, not a JSON query object. Python `engine.query_yaml(query_yaml)` uses the same parser; a trusted local-only `engine.query_file(path)` is convenience, never a remote MCP file path.
84
+
85
+ A strict parser MUST be implemented and is specified in `docs/yaml-security.md`. Contract highlights:
86
+
87
+ - Exactly one mapping document, UTF-8, bounded bytes (default 64 KiB), depth, nodes, and string lengths; comments and ordinary block/flow mapping syntax allowed.
88
+ - Disallow anchors (`&x`), aliases (`*x`), merge keys (`<<`), explicit tags (`!`, `!!`), `%TAG` directives, multiple documents, custom constructors, recursive/cyclic structures and arbitrary object construction. Reject duplicate mapping keys at **every depth**.
89
+ - Implement a restricted YAML 1.2-like **JSON-compatible implicit scalar subset**: lowercase `true`/`false`, `null`, decimal integers/floats; anything else remains a string. In particular `yes`, `no`, `on`, `off`, ISO timestamps and values containing colons must not silently turn into booleans/datetimes. Require quotes for strings when ambiguity exists, for example `value: "true"`.
90
+ - Reject non-finite numbers (`.nan`, `.inf`), octal/hex integer notation, unsupported numbers, non-string map keys, and invalid Unicode/control scalars. JSON Pointer field paths and dynamic values are treated as **data**, never expressions.
91
+ - YAML parsing MUST NOT bypass any Pydantic strict model, authorization, provenance, or execution budget. Never use `yaml.load(..., Loader=yaml.Loader)`, `unsafe_load`, pickle or `eval`.
92
+ - The server returns code-keyed errors from the single `errors/catalog.json`, optionally with safe 1-based `line`/`column` and a Pydantic query field path. It must not echo secret text fragments from malicious queries.
93
+
94
+ A normalized typed AST is the sole input to policy/planning/Polars; the compiler must have no dependency on YAML syntax and must not accept raw YAML nodes.
95
+
96
+ ## 2. Root contract
97
+
98
+ | Key | Type / default | Requirement |
99
+ |---|---|---|
100
+ | `version` | literal `analytics.query/v1` | required |
101
+ | `dataset` | object `{ "name": string }` | required; ownership checked server-side |
102
+ | `casts` | array of 0..8 cast specifications | optional, default [] |
103
+ | `time_range` | absolute or relative time object | optional unless window requested |
104
+ | `filters` | array of 0..20 predicate nodes; implicit AND | optional, default [] |
105
+ | `window` | null or tumbling/sliding | optional, default null; requires valid time range |
106
+ | `group_by` | 0..4 `{field: JSON pointer, as: alias}` | optional, default [] |
107
+ | `aggregations` | array of 1..20 typed aggregates | required |
108
+ | `having` | 0..10 post-aggregate predicates, implicit AND | optional, default [] |
109
+ | `order_by` | 0..4 sort elements | optional; implicit stable ordering when omitted |
110
+ | `limit` | int 1..500, default 100 | optional; result cap, not work cap |
111
+ | `options` | `{include_empty_windows?: false}` | optional; true only under restricted conditions |
112
+
113
+ The YAML root mapping is validated using `ConfigDict(extra="forbid", strict=True)` after restricted parsing. Unknown keys are errors. The DSL accepts neither textual expressions nor source-specific function names. Query files must use `.yaml` or `.yml` and contain exactly one YAML mapping document. YAML is never executed as code. `dataset.name` must resolve to a live authorized binding in the server-selected context. Names match [A-Za-z][A-Za-z0-9_-]{0,63}; no paths, context IDs or internal snapshot IDs are accepted. Unknown and foreign-context names both yield DATASET_NAME_UNKNOWN. Existing expired bindings yield DATASET_SNAPSHOT_STALE. All numeric limits above are **V1 default policy maxima**, further restricted by the server owner if configured.
114
+
115
+ ### 2.1 JSON Pointer fields
116
+
117
+ Use canonical RFC 6901 paths: `/name`, `/nested/value`, `/~0escaped`, `/foo~1bar` for key `foo/bar`. A key `""` uses `/` (empty token), subject to field policy. Root `/` must not ambiguously refer to entire record. Reject paths deeper than `max_depth`, invalid escape sequences, or non-scalar fields for unsupported operators. No dotted-path shorthand; no wildcards, JSONPath or array expansion. A path identifies the same logical field in every record, although that field may be missing or type-conflicted.
118
+
119
+ ### 2.2 Type contract
120
+
121
+ Supported queryable scalar types: boolean, integer (signed 64-bit if representable), float64 (finite), string (bounded), datetime (UTC), and null. JSON `object` and `array` may be discovered but are not groupable, comparable, castable to numeric, or aggregatable in V1. `mixed` discovered type requires an explicit and permitted cast or query fails with `DATA_FIELD_TYPE_CONFLICT`. Source field names/aliases must be authorized and visible. A cast is not the same as granting access to a field.
122
+
123
+ ### 2.3 Casts
124
+
125
+ ```yaml
126
+ field: /measure
127
+ to: float64
128
+ on_error: 'null'
129
+ ```
130
+
131
+ Allowed `to`: `float64`, `int64`, `string`, `datetime`; format mandatory for datetime: `rfc3339` or `epoch_s`, `epoch_ms`, `epoch_us` (only numeric when epoch chosen). `on_error`: `reject` (fail if any non-null/non-missing value cannot convert) or `null` (mark invalid conversion null, count `cast_errors`, warning). Do not silently convert booleans to numbers; verify overflow and non-finite values. `to:string` is for scalar values only and is not an arbitrary JSON serializer. Reject decimal precision-loss if a policy marks a field exact.
132
+
133
+ ## 3. Filter predicate AST — all V1 operators
134
+
135
+ A leaf has keys `field: /path`, `op: eq`, and `value: 123`; `filters` is implicit AND. Negation and composition are explicit disjoint typed variants. Enforce max AST depth 4, 32 total nodes, max 20 literals in an IN list, max scalar text length 1024. No executable expression syntax. The [matching extension](string-matching.md) adds bounded Rust regex, never Python callbacks.
136
+
137
+ | Operator | Shape | Semantics |
138
+ |---|---|---|
139
+ | `eq` | `{field,op,value}` | typed equality; null comparison yields unknown (not selected) |
140
+ | `ne` | same | typed inequality; **does not** include null/missing |
141
+ | `gt` | same | greater-than ordered scalar |
142
+ | `gte` | same | greater-or-equal |
143
+ | `lt` | same | less-than |
144
+ | `lte` | same | less-or-equal |
145
+ | `in` | `{field,op,values:[...]}` | membership, nonempty homogeneous bounded list |
146
+ | `not_in` | same | inverse membership on present non-null values |
147
+ | `between` | `{field,op,lower,upper}` | inclusive ordered bounds, `lower <= upper` |
148
+ | `is_null` | `{field,op}` | true if explicit JSON null **or missing** in normalized columns |
149
+ | `is_not_null` | `{field,op}` | true if present, non-null and successfully cast |
150
+ | `is_missing` | `{field,op}` | true only if the path is absent in original record |
151
+ | `is_present` | `{field,op}` | true if path exists, including explicit null |
152
+ | `contains` | `{field,op,value}` | literal case-sensitive substring only |
153
+ | `starts_with` | same | literal case-sensitive prefix only |
154
+ | `ends_with` | same | literal case-sensitive suffix only |
155
+ | `not_contains` | `{field,op,value,normalization?}` | literal substring complement; null remains unknown |
156
+ | `word` | same | Rust Unicode whole-word boundaries |
157
+ | `like`, `ilike` | same | anchored percent/underscore patterns with bounded escaping |
158
+ | `regex` | same | bounded Rust regex search; no lookaround/backreferences |
159
+ | `fuzzy` | `{field,op,value,algorithm,min_score,normalization?}` | inclusive RapidFuzz similarity over full normalized values |
160
+ | `match` | `{match:{fields,patterns,normalization?,mode:any}}` | three-valued ANY across authorized field/rule pairs, without multiplying records |
161
+ | `and` | `{op,conditions:[pred,...]}` | all conditions true; min 2 |
162
+ | `or` | same | any condition true; min 2 |
163
+ | `not` | `{op,condition:pred}` | logical complement with documented null behavior |
164
+
165
+ Notes:
166
+
167
+ - **26 predicate tags total:** 22 leaf predicates, `and`/`or`/`not`, and the disjoint multi-field `match` node. Boolean `false`, integer 0 and empty string are actual values, not missing.
168
+ - Null semantics use three-valued logic for typed comparisons; the top-level WHERE keeps only True; `count_if` treats unknown as False. `not` of unknown stays unknown, rather than turning null into a match.
169
+ - Existing literal operations retain their defaults. The matching extension supports explicit NFC/NFKC, trim and casefold, LIKE/ILIKE, Rust regex and exhaustive fuzzy similarity. Normalization changes matching only, never source/group values. Regex has its own syntax/case policy; see the [full matching contract](string-matching.md).
170
+ - `is_null` includes missing **by definition**. For distinctions, use `is_missing` and `is_present` explicitly.
171
+ - Operators must reject incompatible types, not return empty results on error. Explicit cast rules can resolve supported conflicts.
172
+ - HAVING uses a separate, smaller result-only typed predicate model; supports `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `between`, `is_null`, `is_not_null`, `and`, `or`, `not`. Its `field` is an output alias (not a source JSON Pointer). Max 10 total nodes.
173
+
174
+ ## 4. Aggregations — all V1 operators
175
+
176
+ All aggregations require unique safe `as` aliases of ASCII `[A-Za-z][A-Za-z0-9_]{0,63}` not equal to reserved outputs (`window_start`, `window_end`, `query_id`, etc.) nor group aliases. `field` is required only when specified below; no unused extra keys.
177
+
178
+ | Operator | Syntax fields | Exact semantics |
179
+ |---|---|---|
180
+ | `count` | `op,as` | input rows per group/window (including null-bearing records) |
181
+ | `count_non_null` | `op,field,as` | count present, non-null, successfully typed values |
182
+ | `null_count` | `op,field,as` | explicit JSON null plus invalid-to-null conversions, **not originally missing** |
183
+ | `missing_count` | `op,field,as` | count originally absent field path |
184
+ | `count_distinct` | `op,field,as` | exact unique non-null scalar values; bounded cardinality |
185
+ | `sum` | `op,field,as` | sum of finite numeric valid values; null when no valid samples |
186
+ | `avg` | `op,field,as` | arithmetic mean over valid numeric values; null if none |
187
+ | `min` | `op,field,as` | smallest valid ordered scalar; null if none |
188
+ | `max` | `op,field,as` | largest valid ordered scalar; null if none |
189
+ | `median` | `op,field,as` | 0.50 quantile of numeric values with linear interpolation |
190
+ | `p50` | `op,field,as` | exact quantile q=0.50, linear interpolation |
191
+ | `p90` | `op,field,as` | exact quantile q=0.90, linear interpolation |
192
+ | `p95` | `op,field,as` | exact quantile q=0.95, linear interpolation |
193
+ | `p99` | `op,field,as` | exact quantile q=0.99, linear interpolation |
194
+ | `stddev` | `op,field,as` | sample stdev, ddof=1, null if fewer than 2 valid values |
195
+ | `variance` | `op,field,as` | sample variance, ddof=1, null if fewer than 2 valid values |
196
+ | `count_if` | `op,where,as` | rows where predicate True; false/unknown not counted |
197
+ | `sum_if` | `op,field,where,as` | numeric sum over matching valid samples; null if no matches |
198
+ | `avg_if` | `op,field,where,as` | numeric mean over matching valid samples; null if none |
199
+ | `ratio_if` | `op,field,where,as` | True matches **among non-null valid values of field** / non-null valid count; null if denominator=0 |
200
+
201
+ **20 operators total.** For `ratio_if`, require `where` to reference the `field` itself or other approved scalar fields; clarify that null/missing field never enters the denominator and match count only includes denominator-eligible rows. Numeric `sum` must detect overflow or fail clearly rather than wrap. `median` and `p50` produce the same result by design. `count_distinct` exactness is restricted to finite cardinality. No `first`/`last` in V1: input order/event-time tie semantics need a separate later contract.
202
+
203
+ ### 4.1 Aggregation example with conditional counts
204
+
205
+ ```yaml
206
+ aggregations:
207
+ - op: count
208
+ as: events
209
+ - op: count_if
210
+ where:
211
+ field: /status
212
+ op: eq
213
+ value: failed
214
+ as: failures
215
+ - op: ratio_if
216
+ field: /latency
217
+ where:
218
+ field: /latency
219
+ op: gt
220
+ value: 100
221
+ as: high_latency_fraction
222
+ ```
223
+
224
+ This is a fragment, not a full root query. Percentages are *fractions*, range `[0,1]`, not multiplied by 100. A ratio based on missing observations must return supporting coverage metadata. Never turn count of bad observations into duration without sampling-cadence evidence.
225
+
226
+ ## 5. Time semantics and windows
227
+
228
+ ### 5.1 Time range (optional without window, mandatory with window)
229
+
230
+ Absolute:
231
+
232
+ ```yaml
233
+ field: /observed_at
234
+ start: '2026-11-06T00:00:00Z'
235
+ end: '2026-11-06T01:00:00Z'
236
+ ```
237
+
238
+ Relative:
239
+
240
+ ```yaml
241
+ field: /observed_at
242
+ last_minutes: 60
243
+ ```
244
+
245
+ Allow exactly one of `last_minutes` (1..1440) or `start`+`end`. Normalize explicit RFC3339 timestamps with timezone offsets to UTC. Resolve a relative interval against a server-injected UTC clock **once**, not the model's clock. `[start,end)` is applied before windowing; reject invalid or overly broad time bounds. Time field must be datetime or explicitly cast to datetime, with failed timestamp parses handled according to policy and counted. No implicit unit guessing, local timezone or DST-based calendar period in V1. If `time_range` is omitted, all admitted snapshot records are in scope (subject to row limits).
246
+
247
+ ### 5.2 Windows
248
+
249
+ No window: `"window": null` or key omitted; grouped aggregation over entire filtered data.
250
+
251
+ Tumbling:
252
+
253
+ ```yaml
254
+ type: tumbling
255
+ size: 5m
256
+ ```
257
+
258
+ Sliding (also called hopping):
259
+
260
+ ```yaml
261
+ type: sliding
262
+ size: 5m
263
+ step: 1m
264
+ ```
265
+
266
+ Allowed fixed intervals: `1s`, `5s`, `10s`, `30s`, `1m`, `5m`, `15m`, `30m`, `1h`. A trusted policy may reduce this allowlist. `step <= size`; overlap multiplier `size/step <= 12`, step divides size exactly. Windows aligned to UTC Unix epoch, closed left (`[start,end)`), labeled with **both** start and end. Partial boundary windows flagged in evidence. Require bounded number of generated windows, groups, and result bytes before execution. `group_by_dynamic` must sort the timestamp appropriately within groups as required by Polars version in use. Overlapping windows reuse records; do not sum them to calculate distinct source samples.
267
+
268
+ Polars does not automatically materialize empty windows. Default is sparse window output. `include_empty_windows=true` may be supported only with zero groups or a *small, explicitly constrained authorized list* of group keys and bounded grid. Otherwise fail `QUERY_EMPTY_WINDOW_UNSUPPORTED`, never silently omit promised rows. In an explicit empty window row, counts are 0 and numeric metric aggregations null, with `NO_DATA` evidence.
269
+
270
+ ### 5.3 Group by, HAVING, ordering
271
+
272
+ `group_by` defines output aliases for one to four scalar dimensions. Missing group keys are represented distinctly from explicit null in result grouping (e.g. distinct tagged sentinel encoded as JSON `null` with `group_key_status` metadata, or dual marker), and must be tested—never silently merge absent/null if a query requests their distinction. Reject arbitrary cardinality above policy limit. Group ordering deterministic across runs.
273
+
274
+ `having` applies **after** aggregation using output aliases only. `order_by` allows 0..4 alias/window keys, directions asc/desc, explicit `nulls: first|last` (default last), with deterministic tie-breaks. `limit` applies last. `truncated` only true when eligible output count exceeds returned count; `limit` doesn't justify unbounded work.
275
+
276
+ ## 6. Error and evidence requirements
277
+
278
+ Public query issues use `QUERY_*`, data shape issues `DATA_*`, record ingestion `INPUT_*`, authorization `ACCESS_*`, limits `RESOURCE_*`, backend `EXECUTION_*`. Every code is defined in `src/xp_libs_analytics/errors/catalog.json`; the API returns that **exact** message string, safe JSON path, retry flag and bounded `details`. Avoid echoing raw content. `analytics_error_info(code)` exposes the same catalog and cannot fabricate a new message.
279
+
280
+ `NO_DATA` means zero matching valid observations; this is not an error. `INSUFFICIENT_DATA` means data exists but a configured minimum evidence requirement was not met; metadata explains the basis. An unknown expected sampling cadence means coverage is `UNKNOWN`, not zero or 100%. Snapshot queries should identify which rows were admitted, excluded, malformed, missing, cast-invalid and aggregated, allowing totals to reconcile.
281
+
282
+ ## 7. YAML authoring / Pydantic schema compatibility policy
283
+
284
+ - Pydantic model names/types and `model_json_schema()` snapshots are committed and compared on PRs.
285
+ - `analytics.query/v1` YAML cannot silently change operator meaning, error code semantics, timestamp boundaries or serialization. Additive optional properties are reviewed; breaking changes require `v2` and migration docs.
286
+ - Use discriminated unions for `op` and `type`, `ConfigDict(extra='forbid', strict=True)`, explicit types for scalar values, and limits at model + semantic levels.
287
+ - Pydantic-derived JSON Schema documents the **post-YAML-parsing AST**, not the YAML tool wire argument; the skill and YAML examples teach agents the authoring format. JSON Schema supplied to LLMs assists planning, but real security rules stay server-side. Even a valid schema can't grant access to a dataset or reveal an unauthorized field.
288
+ - Validate every `.yaml` under `examples/queries/`: bounded strict YAML parser → `AnalyticsQuery.model_validate` → semantic validation against authorized named synthetic snapshots. Do not call `model_validate_json` for YAML. Invalid examples fail CI. Canonical runtime errors preserve YAML locations (line/column) and Pydantic field paths.
289
+
290
+ ## 8. Operator-to-Polars mapping guidance (not agent-facing)
291
+
292
+ - Filter leaves → `pl.col(colid)` comparisons, `.is_in`, `.is_null`, `.str.contains(..., literal=True)` if supported by pinned version or `.str.contains` with safely escaped literal, `.str.starts_with`, `.str.ends_with`.
293
+ - Missing predicates → separate source-presence bitmap/column per referenced field; do **not** map `is_missing` to `.is_null()`.
294
+ - Count → `pl.len()`; non-null → `.count()`; null vs missing → separate raw-null flags/presence; exact distinct → `.drop_nulls().n_unique()` with cardinality check.
295
+ - Quantiles → `.quantile(q, interpolation='linear')`; sample stdev/var → Polars equivalent with ddof=1, pinned version verified.
296
+ - Conditional counts → `when(predicate).then(1).otherwise(0).sum()` or equivalent Boolean `fill_null(False).cast(Int64).sum()`.
297
+ - Conditional mean/sum → null out invalid or nonmatching samples; ratio numerator/denominator must use the **same** non-null eligibility filter.
298
+ - Time grouping → lazy or eager `group_by_dynamic(index_column=..., every=..., period=..., group_by=..., closed='left', label='left', start_by='window')` with explicit UTC alignment and boundaries verified against reference oracle; APIs are version dependent; confirm exact pinned version.
299
+ - HAVING/sort → expressions using approved output alias map only. Keep internal ids opaque to avoid injection or name collisions.
300
+ - Prefer lazy execution and projection/filter pushdown before materialization where possible, but JSON records supplied in memory are already parsed; do not claim zero-copy for all formats.
301
+
302
+ The backend uses bounded native integer-range expansion for epoch-aligned window membership. Operator semantics are verified against an independent Python oracle; changes require versioned contract review.
303
+
304
+ ## String matching extension (0.4.0)
305
+
306
+ The [complete matching contract](string-matching.md) defines normalization, raw-value disclosure, bounded candidate/scoring work, diagnostics scope and all new errors. Source predicates are shared between filters and every conditional aggregate; HAVING remains aggregate-output aliases only. Matching does not infer entities. Snapshot registry and temporal semantics remain unchanged. analytics://string-matching/v1 exposes the same product reference through MCP.
@@ -0,0 +1,39 @@
1
+ # Agent Quick Reference — YAML Query DSL V1
2
+
3
+ **Authored query**: one YAML mapping document (not JSON SQL). **Input records**: JSON, NDJSON, Python mappings. **Result/error**: JSON. **MCP**: send query as `query_yaml: str` in `analytics_validate_query`, `analytics_explain_query`, and `analytics_query`.
4
+
5
+ ```yaml
6
+ version: analytics.query/v1
7
+ dataset:
8
+ name: example_data
9
+ filters:
10
+ - field: /status
11
+ op: eq
12
+ value: error
13
+ aggregations:
14
+ - op: count
15
+ as: errors
16
+ limit: 100
17
+ ```
18
+
19
+ `example_data` is a logical binding; discover available names with `analytics_list_datasets` and use `dataset.name`. Names are scoped to the server-selected execution context.
20
+
21
+ - `version` exactly `analytics.query/v1`; `dataset.name` opaque authorized string.
22
+ - `casts`: list with `field`, `to: float64|int64|string|datetime`, `on_error: reject|"null"`. For datetime include `format: rfc3339|epoch_s|epoch_ms|epoch_us`.
23
+ - `time_range`: `field: /ts` plus either `last_minutes: 60` or absolute `start` and `end` RFC3339 offset-aware strings. When windowed, a time range is required.
24
+ - `filters`: 0–20 predicate AST nodes (implicit AND). Leaf `field`, `op`, `value`; `in/not_in` use `values`; `between` uses `lower`/`upper`; null/missing checks omit values; `and/or` use `conditions`, `not` uses `condition`.
25
+ - 16 leaf operators: `eq ne gt gte lt lte in not_in between is_null is_not_null is_missing is_present contains starts_with ends_with`; logical 3: `and or not`.
26
+ - `window`: omitted/null or a mapping `type: tumbling, size: 5m` or `type: sliding, size: 5m, step: 1m`. Fixed UTC intervals allowed by server. Overlapping windows reuse source records.
27
+ - `group_by`: up to four `{field: /region, as: region}` mappings (YAML flow example); alias must be unique.
28
+ - `aggregations`: 1–20 operations; all aliases `as` unique. Operators: `count count_non_null null_count missing_count count_distinct sum avg min max median p50 p90 p95 p99 stddev variance count_if sum_if avg_if ratio_if`.
29
+ - `having`: result-alias predicates, up to 10; `order_by`: alias/window field, `direction: asc|desc`, `nulls: first|last`; `limit` 1–500 by default.
30
+ - `options`: `include_empty_windows: false` default, true only when bounded and policy allows.
31
+ - Full operator semantics, null behavior, quantile interpolation, window alignment, resources and authorization live in `docs/query-language.md`.
32
+
33
+ **YAML hazards:** no anchors/aliases/tags/merges/multi-doc. Use two-space indentation. Scalar `on`, `off`, `yes`, `no` remain strings; lowercase `true`, `false`, `null` are typed. Quote string literal `"null"`; quote strings containing colon-space or `#` when needed. Do not use `.nan` or `.inf`.
34
+
35
+ Status: `OK`, `NO_DATA`, `INSUFFICIENT_DATA`, `ERROR`. Always inspect source counts, field quality, resolved time range, truncation and null denominators.
36
+
37
+ See `../../examples/README.md`, `../../examples/queries/*.yaml`, `docs/yaml-security.md` and the canonical error catalog.
38
+
39
+ String matching extends the source predicate union with not_contains, word, like/ilike, regex, fuzzy and multi-field match. See analytics://string-matching/v1 and references/string-matching.md. Normalization is explicit, fuzzy selection exhaustive, diagnostics output-only, identity inference unsupported.