This document captures the implementation contract for the aimeta q module: the annotation standard, the runtime model, the JSON schema, and the public API surface.
- Make every kdb+ process self-describing for agents. Tables, columns, types, foreign-reference graphs, and (where the operator opts in) gateway functions with signatures, descriptions, and call examples.
- Zero-annotation baseline. Loading the module against an existing process must produce immediately useful metadata. Annotations are additive enrichment, never a precondition.
- One canonical form, one direction of flow. Annotations in source compile to
meta.json, which is loaded read-only at runtime. No mutation API; no live drift between source and served metadata. - Multiple consumer tiers, one schema. HTTP (
GET /meta), qIPC (.aimeta.get*), and thekx-metaCLI all see the samemeta.jsonshape.
- A live metadata editor. Annotations are source-of-truth.
- A query layer.
meta.jsondescribes capability; it does not execute it. - An access-control layer. Filtering by identity/role belongs in the gateway and the Trust Layer, not here.
.q source files with / @ annotations
│ aimeta compiler (built into .aimeta; runs on every init[] by default)
▼
.aimeta/meta.json ← canonical form, deterministic output
│ .aimeta.init[] (called by host script at startup)
▼
.aimeta.data ← read-only in-memory dict
├── GET / → home document (cold-start landing)
├── GET /.well-known/api-catalog → RFC 9727 Linkset (discovery)
├── GET /meta → streams raw JSON bytes (no re-encoding)
├── GET /openapi.json → OpenAPI 3.1 description of routes
├── GET /meta.schema.json → JSON Schema for /meta's body
└── .aimeta.get*[] → dict / table lookups (Tier 2 qIPC)
Direction is one-way. Source is canonical. JSON is a deterministic output; in-memory state is read-only. To change anything, edit annotations and recompile.
Compile on every boot, by default. init[] recompiles the source tree on every host start. srcDir defaults to the directory containing .z.f, resolved against the OS's notion of cwd (system "pwd" — not getenv \PWD``, which subprocess.Popen does not update). The compile-speed work in M6 established that compile is fast enough at realistic scales: ~80ms for the demo, sub-500ms for a 30-table tickerplant.
Opt out with --nocompile. Production hosts that ship pre-built metadata (e.g. compiled in CI, deployed alongside the q binary) launch with q host.q --nocompile. init[] then skips the compile step and reads .aimeta/meta.json directly. D2 is the worked example of this flow.
Degradation chain. A bad source tree must never crash a running host, and an unannotated tree should not produce a useless empty /meta. init[] walks the chain in order, never propagating an error past the chain:
- Compile succeeds AND emits tables or functions → write
.aimeta/meta.json→loadJson[]. Normal path. - Compile succeeds but emits zero tables AND zero functions → fall through to Tier-1 in-process introspection. The host's
tables[],meta t, and\f .nsproduce a structurally-useful doc that is served instead of the empty-arrays compile output. Stamped withprocess.compileStatus:"empty"so consumers can detect the degraded source. Typically means the source is unannotated; annotating fixes it. - Compile fails AND
.aimeta/meta.jsonexists → serve the prior file (known-good beats synthesis). Compile only writes on success, so a rule-violation rebuild leaves the prior artefact untouched. - Compile fails AND no prior file → Tier-1 synthesis as in (2), but stamped with
process.compileStatus:"failed"andprocess.warningscarrying the captured compile error. - Tier-1 synthesis itself errors →
loadJson[]on the missing path falls through to empty metadata, with a WARN line. Host stays up.
Footgun: init[] placement for unannotated hosts. If you add aimeta to an un-annotated file, call init[] at the end of the script, after your table and function declarations. The Tier-1 synthesis in step (2) introspects the live namespace — only symbols bound before init[] runs are visible. This won't matter once you add annotations, because the compile path in step (1) reads the source file directly; placement becomes conventional, not load-bearing.
Documented failure mode. Hosts loaded by a parent script (e.g. a test runner that does \l demo/host.q from a different directory) get the parent's directory as srcDir, not the demo's. The fallback chain catches it — compile finds nothing actionable, the on-disk file or tier-1 path takes over. An explicit srcDir override knob is not shipped; revisit if the failure mode bites in practice.
The module ships with four components, each in its own file, loaded by init.q:
| Component | File | Responsibility |
|---|---|---|
| metadata | metadata.q |
Runtime data dict, JSON loader, qIPC read API |
| compiler | compiler.q |
Annotation parser, validator, JSON renderer |
| rest | rest.q |
HTTP handlers for GET /, /.well-known/api-catalog, /meta, and the OpenAPI/Schema sidecars |
| discovery | discovery.q |
discover[] probe library backing the kx-meta CLI |
init.q exports a deliberately narrow surface:
| Export | Source | Purpose |
|---|---|---|
compile |
compiler.q |
Build meta.json from a directory of .q source files |
discover |
discovery.q |
Programmatic probe — also reachable via kx-meta discover |
init |
init.q |
Load meta.json; register /meta handler |
data |
metadata.q |
Read-only runtime dict — getter (data[]) |
getTables |
metadata.q |
qIPC: list of tables |
getTable |
metadata.q |
qIPC: one table by name |
getFunctions |
metadata.q |
qIPC: list of functions |
getFunction |
metadata.q |
qIPC: one function by name |
getReferences |
metadata.q |
qIPC: list of reference-vocabulary entries |
getReference |
metadata.q |
qIPC: one reference entry by semanticType |
Internal-only symbols (annotation parsing helpers, HTTP framing) stay private to their respective files. Argv parsing and subcommand dispatch live in scripts/kx-meta.q, outside the module.
The compiler recognises a closed set of tags. Unknown tags are warnings, not errors — annotation evolution must not break existing files.
| Tag | Applies to | Required? | Description |
|---|---|---|---|
@desc |
function, table | yes for @public |
Single-paragraph natural-language description |
@public |
function | opt-in | Marks a function as part of the published surface |
@private |
table | opt-out | Excludes a table from the published surface |
@param |
function | per-param | name {type} description — type in qdoc syntax |
@returns |
function | yes for @public |
{type} description — return type and shape |
@example |
function | recommended | Concrete, working call example. Treated as opaque text — never executed by the compiler |
@uses |
function | recommended | Space-separated table names this function reads from (e.g. @uses trade quote). May repeat across multiple lines; the union is the dependency set. Drives the publish graph and cross-process docs. |
@col |
table | per-col | name {type} description — overrides or augments native meta output |
@sampleRow |
table | optional, repeatable | One row per tag — comma-separated q literals (e.g. 2026.04.29D14:30:00,`AAPL,175.5,100). Cell count and per-cell type must match @col. See §"@sampleRow cell grammar". |
@reference |
table | opt-in | Argument is a vocabulary name. Marks the table as the canonical local resolver for any column with a matching @semanticType. See §"Reference resolution". |
@semanticType |
column | optional | Vocabulary tag (e.g. currency, instrument) for cross-table joins. Pairs with @reference to declare resolver tables. |
@foreignRef |
column | optional | tableName.columnName — declares a foreign-reference graph edge |
@cardinality |
column | optional | low, medium, high — informs query planning hints |
@attr |
column | optional, repeatable | Each value is one of s/u/p/g — the kdb+ column attribute (sorted/unique/parted/grouped). @attr:u marks the column as the @reference resolver key. Repeating on a @col line collects every value into the column's attributes list. See code.kx.com/q/ref/set-attribute. |
@label |
column | opt-in | Marks a column as a human-readable label for the row. Multiple @label allowed; first-declared is primary. Only meaningful on @reference tables. |
@tag |
function, table | optional | Free-form labels (e.g. experimental, deprecated) |
- Tables are public by default. Every table in
tables[]appears inmeta.json, with nativemetaas the fallback when no annotations exist.@privateopts a table out. - Functions opt in via
@public. Unannotated functions and helpers do not appear.@usesdeclares the table dependency graph for the function and is used to propagate cross-process documentation. @usesresolves transitively. A@publicfunction's@uses tablepulls that table into the published surface even if the table is in another process — the cross-process publish graph is walked at compile time.
Reference data (instruments, exchanges, currencies, calendars) is the connective tissue agents need to translate user-facing names into query terms and back. The spec models this with two cooperating tags:
@reference Xon a table declares it as the canonical local resolver for vocabularyX. The table must have a column with@attr:u(the resolver key).@semanticType Xon a column declares its values are drawn from vocabularyX. When a@reference Xtable exists in the samemeta.json, the column is implicitly joinable to it on the unique-key column.@labelon a column inside an@referencetable identifies the human-readable name for each row. Multiple@labelcolumns may be declared; the first wins for display, all are surfaced for lookup.
The compiler emits a denormalised top-level references[] index so consumers can enumerate reference vocabularies without scanning every table.
The @reference X ↔ @semanticType X link is separate from @foreignRef. @foreignRef declares a single explicit edge between two specific columns; @reference + @semanticType declares a vocabulary-level relationship that any column can participate in. Both may appear on the same column.
One line per sample row, comma-separated q literals. The compiler splits on commas, parses each cell with a literal-only grammar, and assembles a 98h table — value/parse are never called on annotation text.
Accepted — one literal per cell:
- Numeric:
100,-50,100i,25h,175.5,1.5f,2.5e,2.5e+3. - Boolean
1b,0101b; byte0xff,0x0102. - Symbol
`AAPL,`AAPL`MSFT,`; string"hello"(commas inside must be"..."-quoted; escapes\" \\ \n \t \r). - Temporal: date
2026.04.29, month2026.04m, minute14:30, second14:30:00, time14:30:00.123, timestamp2026.04.29D14:30:00, timespan0D14:30:00.000000000, datetime…T…(legacy). - Guid
1234abcd-1234-1234-1234-123456789abc. - Null/infinity for every type:
0N,0n,0Nh…0Nn,0w,0W,0Wh…0Wn, and their-variants.
Rejected — by character class, not heuristic: any ( ) [ ] { } ; @ $ \\ * / % & | = < > ! ~ ^ ? # ' _ outside a string/symbol; identifiers and .z.* (no name lookup); compound shapes (dicts, nested lists, lambdas, calls).
A cell that would be a dict, table, or other compound shape is omitted (the column stays absent from the sample) and documented via @desc. A future @sampleJson may cover full-row JSON if this proves too restrictive.
Determinism: keys must appear in stable order, no timestamps, no environment-dependent values. A clean recompile of unchanged source must produce a byte-identical file.
A worked example covering every tag in this spec lives at tests/fixtures/worked/host.q (annotated source) and tests/fixtures/worked/meta.json (the compiled output it should produce). The compile tests exercise the same fixture.
schemaVersion (top-level, currently 2) is a single integer marking the shape of meta.json. It changes only on breaking edits to the schema; consumers use it to fail loudly when reading a file they don't understand.
| Version | Change |
|---|---|
| 2 | Removed column field unique and the @unique tag. Added column field attributes (string list) and the @attr tag covering s/u/p/g. @attr:u replaces @unique for @reference resolver-key declaration. |
| 1 | Initial schema. |
Bump when an edit would silently misinterpret an old file or break a current consumer:
- A field is renamed (e.g.
references[].key → keyColumn). - A field is removed, or its type changes (string → object, scalar → array).
- A required field becomes optional, or vice versa.
- The structure of a section changes (e.g.
tables[]becoming a keyed object).
Don't bump for additive changes:
- A new optional field on an existing object — old consumers ignore it.
- A new optional tag in §"Tag reference" that produces a new optional field.
- New entries in an enum where consumers already pass values through.
Pre-1.0 caveat: while no external consumers exist, breaking edits may land without a bump if every in-tree consumer is updated in the same MR. The keyColumn rename was such a change. Once the schema is consumed outside this repo, that exception ends.
A consumer reading meta.json must inspect schemaVersion before trusting any field:
schemaVersion == known: read normally.schemaVersion < known: the consumer may read it if it chooses to maintain backwards compat. Backwards compatibility is a per-consumer decision, not a spec requirement.schemaVersion > known: fail loudly. Print the file's version, the consumer's max-known version, and a pointer to upgrade. Do not best-effort parse — fields may have moved.
metadata.q (the in-process loader) and scripts/kx-meta.q (the CLI) both follow this contract. New reader libraries should declare a MAX_SCHEMA_VERSION constant alongside their parser.
A schema bump lands in a single MR that:
- Updates
schemaVersionin compiler.q. - Updates the schema example and §"meta.json schema" prose.
- Updates every fixture and golden under tests/.
- Updates
MAX_SCHEMA_VERSIONin every reader library shipped from this repo. - Adds a CHANGELOG entry naming the version, the breaking change, and the upgrade path for downstream readers.
Distinct from schemaVersion. compilerVersion is a semver string identifying the build of the compiler that produced a given meta.json. It tracks the implementation; schemaVersion tracks the wire shape. The two move independently — most compiler releases will not bump schemaVersion.
- Source of truth: the
compilerVersionconstant in compiler.q, alongsideschemaVersion. Re-exported frominit.q's export dict so the CLI can read it. - Stamped: into every
meta.jsonas a top-level field next toschemaVersion. - Surfaced: via
kx-meta version, which emits{"compilerVersion":"X.Y.Z","schemaVersion":N}. - Bump cadence: when the compiler's output or behavior changes meaningfully — parser, validator, or JSON renderer changes that produce a different
meta.jsonfor the same source; new validation rules; etc. Decoupled from the project release version (which lives inCHANGELOG.md): a release that doesn't touch the compiler leavescompilerVersionalone, so existing checked-inmeta.jsonartifacts stay byte-identical. Bumping is a one-line edit incompiler.qplus a golden refresh (UPDATE_GOLDENS=1 q test.q); no MR-wide reader-library coordination is required.
Consumers reading meta.json should not branch on compilerVersion — it is informational, useful for bug reports and reproducibility, not a compatibility gate. Compatibility decisions go through schemaVersion.
Walks srcDir for .q files, parses annotations, validates against the schema, and writes srcDir/.aimeta/meta.json. Returns the written path on success. Validation errors block the write and signal with all rules collected; warnings print to stderr but don't block.
Validation entrypoint without the write. Returns `errors`warnings!(strings; strings) keyed by severity. Used by kx-meta compile --check for CI gating.
Compiles the host's annotations on every boot by default (override with the --nocompile launch flag), loads the resulting .aimeta/meta.json into the runtime data dict, registers the GET /meta HTTP handler, and binds the read API at .aimeta.* (init, reload, setlvl, data, getTables/getTable, getFunctions/getFunction, getReferences/getReference) so qIPC clients can call h ".aimeta.getTable[\t]"` without the host re-binding the export dict by hand. Idempotent — safe to call again after a recompile. Degrades gracefully through the four-step chain described in §"Runtime model".
Returns the read-only runtime dict. Shape mirrors meta.json. Mutation is not supported; bypassing the read-only contract is a bug. Exposed as a no-arg getter rather than a bare binding: q destructuring at use time snapshots dict values, so a plain data entry would freeze callers at the empty dict captured before init[] runs.
qIPC read API for Tier 2 consumers. Same data as data, scoped lookups.
qIPC read API over the computed references[] index. getReferences[] returns the full list; getReference[\currency]returns the single entry whosesemanticType` matches, or an empty dict if none.
The CLI entrypoint. Probes the target process from Tier 3 down to Tier 1 (HTTP GET /meta → qIPC .aimeta.get* → native tables[] / meta / \f), returning a meta.json-shaped dict with a tier field indicating which tier resolved.
GET /meta returns 200 with Content-Type: application/json and an ETag: header. The ETag is the md5 hex of rawJson, recomputed on every loadJson[]/reload[]. Clients that send If-None-Match: <etag> get 304 Not Modified (no body) when the ETag matches; otherwise the full JSON. The hash is opaque to clients — they only round-trip it.
POST /meta returns 405 Method Not Allowed with Allow: GET. Other paths return 404 Not Found unless a pre-existing .z.ph/.z.pp claims them — register[] wraps any handler the host installed before module init, so non-/meta traffic delegates through. PUT/DELETE/OPTIONS to /meta are not dispatched to user callbacks by q's HTTP layer; q returns its default 501 Not Implemented. This is acceptable behavior; cleaning it up would require either patching q's HTTP layer or running behind a proxy.
Body emission is single-shot via plain string concatenation. Typical .aimeta payloads are 1–100KB; chunked Transfer-Encoding is not implemented and not planned for sub-MB payloads.
The module serves four static documents alongside /meta so the route is discoverable to cold clients without out-of-band knowledge:
| Path | Body | Purpose |
|---|---|---|
/ |
JSON home document | First-hop landing page for cold clients that probe the root. Lists every discovery URL the host serves. |
/.well-known/api-catalog |
RFC 9727 Linkset (application/linkset+json) |
IETF standards-track discovery entry. Anchored at / with link relations pointing at the OpenAPI document, JSON Schema, and /meta. |
/openapi.json |
OpenAPI 3.1 document | Describes every HTTP route the module serves, so agent frameworks and Swagger UI can bind to a known entrypoint. |
/meta.schema.json |
JSON Schema (draft 2020-12) | Validates the body returned by /meta. The OpenAPI document $refs it for the /meta 200 response. |
The home document has the shape {"service":"aimeta","schemaVersion":N,"links":{"meta":...,"openapi":...,"schema":...,"apiCatalog":...}} — schemaVersion is sourced from meta.schema.json at module load and tracks any future bump automatically. The Linkset uses the relations service-desc (OpenAPI), describedby (JSON Schema), and item (the meta payload), all anchored at /.
All four sidecars use the same framing as /meta (200 + ETag; 304 on If-None-Match match; 405 on POST). Content-Type is application/json for the home document and the OpenAPI/Schema, application/linkset+json for the api-catalog. Their bytes never change at runtime, so ETags are computed once at module load.
Source of truth for the OpenAPI doc and JSON Schema lives in aimeta/openapi.json and aimeta/meta.schema.json, checked into the repo. rest.q reads them directly from disk at module-load time using .Q.rp \:::file.json(the kdb-x convention for resolving paths against the loading module's directory). The home document and Linkset are constructed in q at module load — no on-disk source. When the schema bumps, updateinfo.versioninopenapi.jsonandproperties.schemaVersion.constinmeta.schema.jsontogether withcompiler.q's schemaVersion—openapiVersionLockedToCompiler enforces the lockstep, and the home document inherits it transitively (homeDocSchemaVersionLockedToSchema`).
| Tier | What's available | Functions surface | Tables surface |
|---|---|---|---|
| 3 — deluxe | .aimeta + HTTP |
GET /meta — full JSON: signatures, descriptions, examples |
Schemas, semantic types, foreign refs |
| 2 — enhanced | .aimeta + qIPC |
.aimeta.getFunctions[], .getFunction[f] |
.aimeta.getTables[], .getTable[t] |
| 1 — introspect | any kdb+ + qIPC | \f namespace + value .namespace.fn for arity (best-effort) |
tables[], meta, namespace scan |
The kx-meta CLI must work against any kdb+ process — including legacy processes with no .aimeta installed. At Tier 1, the output is structurally useful but semantically sparse: column names and types, but no descriptions, examples, or function annotations. Per-function arity is best-effort: if value .namespace.fn errors on a particular binding (locked-down host, unbound global), we omit arity for that function and continue. The CLI embeds a "tier" field so consumers can calibrate expectations.
The compiler enforces these rules; violations are errors collected and surfaced together.
- Every
@publicfunction has@descand@returns. - Every
@paramdeclared on a function corresponds to a real argument. @usesreferences resolve to a known table — either local (intables[]) or declared elsewhere in the source tree.- Each
@sampleRowshould have one cell per@col, each a q literal whose type matches the column'skdbType. A wrong cell count, a type mismatch, or a non-literal cell produces a warning and drops that table's sample — the rest of the table still compiles (sample data is illustrative, not load-bearing). @exampleis treated as opaque text; the compiler does not parse, type-check, or execute it. (Treating it as opaque is a deliberate decision — examples may include illustrative shorthand or non-runnable forms.)@foreignRefistableName.columnNamesyntax; both must exist somewhere in the source tree (warning if not, not error).@reference Xrequires the table to have a column with@attr:u— without a key, the table cannot resolve.- Two tables declaring
@reference Xfor the sameXis an error — the resolver must be unambiguous. @reference Xwhose vocabularyXis not used by any@semanticTypein the source tree produces a warning (probably typo, but isolated reference tables are legitimate).@labelon a column whose table has no@referenceproduces a warning — nothing consumes it.@labelon a non-symbol/non-string column produces a warning — labels are expected to be human-readable text.- Unknown tags produce warnings; duplicate tags on the same target produce warnings unless explicitly allowed (e.g. multiple
@example, multiple@label). @attr:Xvalue must be one ofs/u/p/g— anything else is an error. Multiple@attrmodifiers on a single@colline are valid; each value is checked individually. Conflicting kdb+-impossible combinations (e.g.s + p) are not flagged — the schema models declared intent and is permissive about the runtime constraint.
Decisions parked beyond the preview release.
@sampleRowsensitivity policy. Real data in source files raises governance questions. Either restrict@sampleRowto synthetic illustrative data only or add a CI check that flags suspicious values (real CUSIPs, real PII shapes).
Calls made during the M3 build that resolve specific Open Questions and lock conventions for downstream phases. The compiler lives in aimeta/compiler.q.
The compiler delegates annotation parsing to the kdb-x kx.ax.qdoc
module, but uses its internal pipeline (.z.m.qd.lib.genTagBlocks
via .z.m.qd.pre.file.toArtifacts) rather than the public surface.
The public (use \kx.ax.qdoc)`qddict exposesdoc(rendered markdown),getFiles, and getTags` — none returns the structured
per-item tagBlocks aimeta needs. We accept the version-coupling
risk: ax internal-API drift is patchable.
qdoc's preprocessor only surfaces items declaring @kind function
or @kind data (tables use @kind data). Without @kind, the item
is silently dropped.
qdoc auto-detects name from the binding line for some patterns but
fails for multi-line lambda bodies. The compiler reads names from
@name only, ignoring qdoc's name and module columns. By
convention, @name carries the binding-name-as-written: leaf for
top-level (@name trade) and fully-qualified for namespaced bindings
(@name .gw.vwap).
Section-divider comments above unrelated bindings can produce phantom
rows from qdoc with @kind but no @name. The compiler filters
those at projection time.
qdoc strips the tagType of unknown tags (returning empty `` ), preserving the line content but losing the tag identity. The compiler registers each aimeta tag in qdoc's tagmap` as an identity mapping,
so every `tagType` survives the pipeline. The full set lives in
`compiler.q`'s `META_TAGS` constant.
For @col columns the compiler emits a single-char kdbType
(e.g. "s" for symbol). The mapping evaluates `<typeName>$()
and indexes .Q.t for the char — q's own type table is the source
of truth. Two qdoc names that aren't valid q cast verbs are aliased:
bool → boolean, string → char. Unknown types pass through
verbatim for the validator to flag.
@param and @returns keep the qdoc string verbatim
("symbol[]", "table") — q functions are dynamically typed and
have no kdb-char form.
The original plan called for sorted-key JSON output; the
implementation achieves byte-determinism through q's
insertion-order-preserving dicts. projectTable and projectFunction
build their dicts in a fixed order matching the SPEC schema,
.j.j walks them in that order, and no timestamps or environment
reads enter the output. Recompile of unchanged source is
byte-identical, which is the underlying intent. The on-disk key
order is the SPEC-documented order, not alphabetical.
compile[srcDir] walks srcDir recursively (qdoc's default). The
SPEC's open question on whether to skip tests/ is left unresolved;
projects that need to exclude can do so via qdoc's exclude setting
when aimeta exposes one. For now, point compile at a directory
that contains only intended source.
compile stamps process.name from the PROCESS_NAME module
constant (default "host"). Hosts can rebind before calling compile.
host and port are SPEC-marked as "populated only when running"
and the compiler does not stamp them — those are runtime concerns
handled by the loader, not compile-time facts.
aimeta's net-new tags (@uses, @col, @sampleRow, @reference,
@semanticType, @foreignRef, @cardinality, @attr, @label,
@tag) keep plain names rather than carrying an @aimeta: namespace
prefix. Audit at decision time: none of them appears in ax/qdoc's
tag table (.z.m.qd.data.TAGS), and the shared tags (@desc,
@public, @private, @param, @returns, @example, @kind,
@name) carry consistent or refining semantics — no parser-level
or semantic conflict exists.
Parser-level survival of aimeta-only tags is handled by registering
each one in qdoc's tagmap as an identity mapping — see "Identity
tagmap for aimeta-specific tags" above.
If a future ax release introduces a colliding tag, we coordinate at that point. The cost of a prefix on every annotation in every annotated codebase outweighs the hypothetical conflict.
The supported tag set lives in compiler.q's SUPPORTED_TAGS
constant — a single q symbol list. At 19 distinct tags (16 declared
plus kind/name/overview inherited from qdoc) the inline form
remains readable on one screen and reviewable in a single MR. No
per-tag documentation files yet; the table at §"Tag reference"
above is the canonical user-facing list.
Migration trigger: once SUPPORTED_TAGS would exceed ~20 entries,
move to one markdown file per tag under docs/tags/ with frontmatter
(applies-to, required, semantics, examples) and a build step that
compiles them into the constant. The threshold is a guideline — adding
the 21st tag is the point at which "split this into a registry" stops
feeling premature.
{ "schemaVersion": 2, "compilerVersion": "0.1.0", // semver; build of the compiler that produced this file "process": { "name": "gateway", // optional — informational "host": "...", // populated only when running "port": 5013 // populated only when running }, "tables": [ { "name": "trade", "private": false, "desc": "...", // optional "reference": null, // optional — vocabulary name if this is an @reference table "labels": [], // optional — ordered label-column names; populated when reference != null "columns": [ { "name": "sym", "kdbType": "s", "desc": "...", // optional "semanticType": "instrument", // optional "foreignRef": "instrument.sym",// optional "cardinality": "high", // optional "attributes": ["g"], // optional — list of "s"/"u"/"p"/"g"; field omitted when none "label": false // optional — true if @label } // ... ], "sampleData": [...], // optional, q-table-shaped "tags": [] // optional } // ... ], "references": [ // computed top-level index { "semanticType": "instrument", // vocabulary name "table": "instrument", // resolver table name "keyColumn": "sym", // @attr:u column on the resolver "label": "name", // primary @label column (null if none) "labels": ["name", "longName"] // all @label columns, primary first } // ... ], "functions": [ { "name": ".gw.vwap", "desc": "Time-bucketed VWAP for one or more instruments", "params": [ {"name": "syms", "type": "symbol[]", "desc": "..."}, {"name": "dt", "type": "date", "desc": "..."}, {"name": "bucket", "type": "timespan", "desc": "..."} ], "returns": {"type": "table", "desc": "..."}, "examples": [".gw.vwap[`AAPL`MSFT; .z.d; 0D00:05:00]"], "uses": ["trade", "quote"], // multiple tables — space-separated in source, array here "tags": [] } // ... ] }