@malloy-publisher/server 0.6.0 → 0.7.0
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.
- package/dist/app/api-doc.yaml +54 -22
- package/dist/app/assets/{EnvironmentPage-C4IrQrKG.js → EnvironmentPage-ufeWfqnK.js} +1 -1
- package/dist/app/assets/{HomePage-DDyGPzmK.js → HomePage-CN_FgmtD.js} +1 -1
- package/dist/app/assets/{LightMode-BpHRmI5M.js → LightMode-BFbC4W45.js} +1 -1
- package/dist/app/assets/{MainPage-BHleF-Kb.js → MainPage-BcCyzakx.js} +1 -1
- package/dist/app/assets/{ModelPage-Cgw-SbZi.js → ModelPage-wHHnxoih.js} +2 -2
- package/dist/app/assets/{PackagePage-Cy0c37D0.js → PackagePage-DwdO-2dp.js} +1 -1
- package/dist/app/assets/{RenderedResult-DSgqZUiN.es-CL0FEank.js → RenderedResult-CYyhQekT.es-DBHF8EwO.js} +2 -2
- package/dist/app/assets/{RouteError-DBMc9ccU.js → RouteError-Blt6a2Za.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-ByJlsmNu.js → ThemeEditorPage-Cb-2YbfG.js} +1 -1
- package/dist/app/assets/{ToggleButtonGroup-BHEVw4Ti.js → ToggleButtonGroup-BuhURn8v.js} +1 -1
- package/dist/app/assets/{index-c3i6ScGn.js → index-BELjGKhI.js} +1 -1
- package/dist/app/assets/{index-rWWmF7hn.js → index-BJyLY8XQ.js} +1 -1
- package/dist/app/assets/{index-Dyxfz1yO.js → index-DCizS2BO.js} +1 -1
- package/dist/app/assets/{index-C5BtJ1GX.js → index-Dz4y_2PE.js} +4 -4
- package/dist/app/assets/{index-BdwkgpbO.js → index-kt4oTEGz.js} +1 -1
- package/dist/app/assets/{index.es-VI5lesX5.js → index.es-u5PRRTzT.js} +4 -4
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +54 -5
- package/dist/server.mjs +383 -55
- package/dist/{sshcrypto-xqan60jb.node → sshcrypto-cqb52edz.node} +0 -0
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -175849,7 +175849,7 @@ import os from "os";
|
|
|
175849
175849
|
function normalizeModelPath(p) {
|
|
175850
175850
|
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
175851
175851
|
}
|
|
175852
|
-
var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
|
|
175852
|
+
var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", INDEX_MODEL_NAME = "index.malloy", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
|
|
175853
175853
|
var init_constants = __esm(() => {
|
|
175854
175854
|
TEMP_DIR_PATH = os.tmpdir();
|
|
175855
175855
|
});
|
|
@@ -233150,7 +233150,7 @@ var require_utils92 = __commonJS((exports, module) => {
|
|
|
233150
233150
|
|
|
233151
233151
|
// ../../node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
|
|
233152
233152
|
var require_sshcrypto = __commonJS((exports, module) => {
|
|
233153
|
-
module.exports = __require("./sshcrypto-
|
|
233153
|
+
module.exports = __require("./sshcrypto-cqb52edz.node");
|
|
233154
233154
|
});
|
|
233155
233155
|
|
|
233156
233156
|
// ../../node_modules/ssh2/lib/protocol/crypto/poly1305.js
|
|
@@ -274436,9 +274436,11 @@ function extractRunTargetSourceName(query) {
|
|
|
274436
274436
|
}
|
|
274437
274437
|
function buildSourceAliasMap(query) {
|
|
274438
274438
|
const aliasOf = new Map;
|
|
274439
|
-
const
|
|
274439
|
+
const text = stripMalloyCommentsAndLiterals(query);
|
|
274440
|
+
const ident = String.raw`(?:\x60([^\x60]+)\x60|([\p{L}\p{N}_]+))`;
|
|
274441
|
+
const declRe = new RegExp(String.raw`source\s*:\s*${ident}(?:\s*\([^)]*\))?\s+is\s*\(?\s*${ident}`, "gu");
|
|
274440
274442
|
let match;
|
|
274441
|
-
while ((match = declRe.exec(
|
|
274443
|
+
while ((match = declRe.exec(text)) !== null) {
|
|
274442
274444
|
aliasOf.set(match[1] ?? match[2], match[3] ?? match[4]);
|
|
274443
274445
|
}
|
|
274444
274446
|
return aliasOf;
|
|
@@ -274489,8 +274491,9 @@ function stripMalloyCommentsAndLiterals(text) {
|
|
|
274489
274491
|
}
|
|
274490
274492
|
return out.join("");
|
|
274491
274493
|
}
|
|
274492
|
-
function buildDerivationBaseMap(
|
|
274494
|
+
function buildDerivationBaseMap(query) {
|
|
274493
274495
|
const basesOf = new Map;
|
|
274496
|
+
const text = stripMalloyCommentsAndLiterals(query);
|
|
274494
274497
|
const ident = String.raw`(?:\x60([^\x60]+)\x60|([\p{L}\p{N}_]+))`;
|
|
274495
274498
|
const declRe = new RegExp(String.raw`(?:source|query)\s*:\s*${ident}(?:\s*\([^)]*\))?\s+is\s*\(?\s*${ident}`, "gu");
|
|
274496
274499
|
let match;
|
|
@@ -276112,17 +276115,15 @@ var init_model = __esm(() => {
|
|
|
276112
276115
|
return this.admittedByPackage(name, this.queryBoundary.packageCuratedSources);
|
|
276113
276116
|
}
|
|
276114
276117
|
isCuratedQuery(name) {
|
|
276115
|
-
if ((this.getQueries() ?? []).some((q) => q.name === name))
|
|
276118
|
+
if (!this.isNotebook() && (this.getQueries() ?? []).some((q) => q.name === name))
|
|
276116
276119
|
return true;
|
|
276117
276120
|
return this.admittedByPackage(name, this.queryBoundary.packageCuratedQueries);
|
|
276118
276121
|
}
|
|
276119
276122
|
assertQueryBoundaryEarly(sourceName, queryName, query) {
|
|
276120
|
-
if (this.modelPath.endsWith(NOTEBOOK_FILE_SUFFIX))
|
|
276121
|
-
return "cleared";
|
|
276122
276123
|
const { mode, exploresDeclared, isQueryEntryPoint } = this.queryBoundary;
|
|
276123
276124
|
if (mode === "all" || !exploresDeclared)
|
|
276124
276125
|
return "cleared";
|
|
276125
|
-
if (!isQueryEntryPoint) {
|
|
276126
|
+
if (!isQueryEntryPoint && !this.isNotebook()) {
|
|
276126
276127
|
throw new NotQueryableError(`No queryable model "${this.modelPath}".`);
|
|
276127
276128
|
}
|
|
276128
276129
|
if (queryName && !query) {
|
|
@@ -276140,18 +276141,16 @@ var init_model = __esm(() => {
|
|
|
276140
276141
|
if (query) {
|
|
276141
276142
|
const target = extractRunTargetSourceName(query);
|
|
276142
276143
|
if (target && !this.isCuratedSource(target) && !this.derivesFromCurated(target, query) && this.sources?.some((s) => s.name === target)) {
|
|
276143
|
-
throw new NotQueryableError(
|
|
276144
|
+
throw new NotQueryableError("Query target is not queryable.");
|
|
276144
276145
|
}
|
|
276145
276146
|
}
|
|
276146
276147
|
return "deferred";
|
|
276147
276148
|
}
|
|
276148
276149
|
assertQueryBoundaryCompiled(compiledSource, query) {
|
|
276149
|
-
if (this.modelPath.endsWith(NOTEBOOK_FILE_SUFFIX))
|
|
276150
|
-
return;
|
|
276151
276150
|
const { mode, exploresDeclared, isQueryEntryPoint } = this.queryBoundary;
|
|
276152
276151
|
if (mode === "all" || !exploresDeclared)
|
|
276153
276152
|
return;
|
|
276154
|
-
if (!isQueryEntryPoint) {
|
|
276153
|
+
if (!isQueryEntryPoint && !this.isNotebook()) {
|
|
276155
276154
|
throw new NotQueryableError(`No queryable model "${this.modelPath}".`);
|
|
276156
276155
|
}
|
|
276157
276156
|
if (compiledSource) {
|
|
@@ -276168,7 +276167,12 @@ var init_model = __esm(() => {
|
|
|
276168
276167
|
return;
|
|
276169
276168
|
this.assertQueryBoundaryCompiled(await this.resolveAuthorizeSourceFromRunnable(runnable), query);
|
|
276170
276169
|
}
|
|
276170
|
+
isNotebook() {
|
|
276171
|
+
return this.modelPath.endsWith(NOTEBOOK_FILE_SUFFIX);
|
|
276172
|
+
}
|
|
276171
276173
|
ownCuratedSourceNames() {
|
|
276174
|
+
if (this.isNotebook())
|
|
276175
|
+
return new Set;
|
|
276172
276176
|
return new Set((this.getSources() ?? []).map((s) => s.name).filter((n) => n !== undefined));
|
|
276173
276177
|
}
|
|
276174
276178
|
derivesFromCurated(name, query) {
|
|
@@ -302208,7 +302212,7 @@ class QueryController {
|
|
|
302208
302212
|
const p = await environment.getPackage(packageName, false);
|
|
302209
302213
|
const model = p.getModel(modelPath);
|
|
302210
302214
|
if (!model) {
|
|
302211
|
-
throw new ModelNotFoundError(
|
|
302215
|
+
throw new ModelNotFoundError(`No queryable model "${modelPath}".`);
|
|
302212
302216
|
} else {
|
|
302213
302217
|
const {
|
|
302214
302218
|
result,
|
|
@@ -310326,6 +310330,23 @@ class Package {
|
|
|
310326
310330
|
const explores = this.packageMetadata.explores;
|
|
310327
310331
|
return !!(explores && explores.length > 0);
|
|
310328
310332
|
}
|
|
310333
|
+
surfaceWidenedWarning;
|
|
310334
|
+
noteSurfaceChangeFrom(previousSurface) {
|
|
310335
|
+
this.surfaceWidenedWarning = undefined;
|
|
310336
|
+
if (!previousSurface || previousSurface.length === 0 || this.packageMetadata.explores !== undefined) {
|
|
310337
|
+
return;
|
|
310338
|
+
}
|
|
310339
|
+
const message = `This package published "${previousSurface.join('", "')}" before the last ` + `reload and publishes no surface now, so every model in it is listed ` + `and queryable by name again, including the sources that surface was ` + `withholding. A surface file that was deleted or renamed is the usual ` + `cause. If that was intended, nothing to do. If not, restore the file ` + `(under its original name -- the name IS the surface), or declare an ` + `"explores" in publisher.json naming what this package should ` + `publish. To keep the package open deliberately, write ` + `"explores": [], which says so and stops this notice.`;
|
|
310340
|
+
this.surfaceWidenedWarning = message;
|
|
310341
|
+
logger.warn(`Package ${this.packageName} no longer publishes a surface`, {
|
|
310342
|
+
packageName: this.packageName,
|
|
310343
|
+
detail: message
|
|
310344
|
+
});
|
|
310345
|
+
}
|
|
310346
|
+
surfaceIsIndexModel() {
|
|
310347
|
+
const explores = this.packageMetadata.explores;
|
|
310348
|
+
return this.models.has(INDEX_MODEL_NAME) && explores?.length === 1 && explores[0] === INDEX_MODEL_NAME;
|
|
310349
|
+
}
|
|
310329
310350
|
exploreSet() {
|
|
310330
310351
|
const explores = this.packageMetadata.explores;
|
|
310331
310352
|
return explores && explores.length > 0 ? new Set(explores) : null;
|
|
@@ -310665,6 +310686,8 @@ class Package {
|
|
|
310665
310686
|
...this.droppedPersistWarnings(),
|
|
310666
310687
|
...this.persistRefusalWarnings(),
|
|
310667
310688
|
...this.emptyDiscoveryWarnings(),
|
|
310689
|
+
...this.brokenSurfaceWarnings(),
|
|
310690
|
+
...this.surfaceWidenedWarning !== undefined ? [{ message: this.surfaceWidenedWarning }] : [],
|
|
310668
310691
|
...this.persistenceCollisionWarnings().map((message) => ({ message })),
|
|
310669
310692
|
...this.preaggregateAccessWarnings(),
|
|
310670
310693
|
...incrementalPolicyAdvisories(this.incrementalPolicySources),
|
|
@@ -310825,7 +310848,7 @@ class Package {
|
|
|
310825
310848
|
if (entry.endsWith(NOTEBOOK_FILE_SUFFIX)) {
|
|
310826
310849
|
problems.push({
|
|
310827
310850
|
entry,
|
|
310828
|
-
reason: `notebooks are always
|
|
310851
|
+
reason: `notebooks are always listed and cannot be explores. ` + `Fix: remove it, and list a ${MODEL_FILE_SUFFIX} model file instead.`
|
|
310829
310852
|
});
|
|
310830
310853
|
} else if (!malloyModels.has(entry)) {
|
|
310831
310854
|
problems.push({
|
|
@@ -310996,13 +311019,39 @@ class Package {
|
|
|
310996
311019
|
if (model.hasEmptyDiscoverySurface()) {
|
|
310997
311020
|
warnings.push({
|
|
310998
311021
|
model: modelPath,
|
|
310999
|
-
message: `Model "${modelPath}" is
|
|
311022
|
+
message: `Model "${modelPath}" is on this package's discovery ` + `surface but exposes nothing: its export closure surfaces ` + `no sources or named queries (typically an import-only ` + `file, or an export {} that filters everything out). Add ` + `e.g. 'export { source_name }' to surface sources on this ` + `model, or ` + (this.surfaceIsIndexModel() ? `delete the file AND any "explores" entry naming it. ` + `Deleting the file alone returns the package to ` + `listing every model only when no "explores" was ` + `written; where one was, it would be left naming a ` + `model that no longer exists and the package would ` + `list nothing.` : `remove it from explores.`)
|
|
311000
311023
|
});
|
|
311001
311024
|
}
|
|
311002
311025
|
}
|
|
311003
311026
|
return warnings;
|
|
311004
311027
|
}
|
|
311028
|
+
brokenSurfaceWarnings() {
|
|
311029
|
+
const exploreSet = this.exploreSet();
|
|
311030
|
+
if (!exploreSet || exploreSet.size === 0)
|
|
311031
|
+
return [];
|
|
311032
|
+
const surface = Array.from(this.models.entries()).filter(([modelPath]) => modelPath.endsWith(MODEL_FILE_SUFFIX) && exploreSet.has(modelPath));
|
|
311033
|
+
if (surface.length === 0)
|
|
311034
|
+
return [];
|
|
311035
|
+
if (!surface.every(([, model]) => !!model.getCompilationError())) {
|
|
311036
|
+
return [];
|
|
311037
|
+
}
|
|
311038
|
+
const collateral = Array.from(this.models.entries()).filter(([modelPath, model]) => modelPath.endsWith(MODEL_FILE_SUFFIX) && !exploreSet.has(modelPath) && !model.getCompilationError()).length;
|
|
311039
|
+
const [firstPath] = surface[0];
|
|
311040
|
+
const subject = surface.length === 1 ? `Model "${firstPath}" is this package's whole discovery surface ` + `and failed to compile` : `Every model on this package's discovery surface ` + `(${surface.map(([p]) => `"${p}"`).join(", ")}) failed to ` + `compile`;
|
|
311041
|
+
return [
|
|
311042
|
+
{
|
|
311043
|
+
model: firstPath,
|
|
311044
|
+
message: `${subject}, so the package exposes nothing: every model in ` + `it, including the ${collateral} that compiled, is now refused ` + `by name with a 404 that reads as "does not exist". Fix the ` + `compile error${surface.length === 1 ? "" : "s"} to restore ` + `them -- ` + surface.map(([modelPath, model]) => `${modelPath}: ` + `${model.getCompilationError()?.message ?? "unknown error"}`).join("; ")
|
|
311045
|
+
}
|
|
311046
|
+
];
|
|
311047
|
+
}
|
|
311005
311048
|
logEmptyDiscoveryWarnings() {
|
|
311049
|
+
for (const warning of this.brokenSurfaceWarnings()) {
|
|
311050
|
+
logger.warn(`Package ${this.packageName} has a broken discovery surface`, {
|
|
311051
|
+
packageName: this.packageName,
|
|
311052
|
+
detail: warning.message
|
|
311053
|
+
});
|
|
311054
|
+
}
|
|
311006
311055
|
for (const warning of this.emptyDiscoveryWarnings()) {
|
|
311007
311056
|
logger.warn(`Package ${this.packageName} has a blank-looking model`, {
|
|
311008
311057
|
packageName: this.packageName,
|
|
@@ -311120,7 +311169,9 @@ class Package {
|
|
|
311120
311169
|
await this.pushPreaggregateServeModels();
|
|
311121
311170
|
this.renderTagWarnings = renderTagWarnings;
|
|
311122
311171
|
this.manifestWarnings = outcome.packageMetadata.manifestWarnings ?? [];
|
|
311172
|
+
const previousSurface = this.packageMetadata.explores;
|
|
311123
311173
|
this.packageMetadata.explores = outcome.packageMetadata.explores;
|
|
311174
|
+
this.noteSurfaceChangeFrom(previousSurface);
|
|
311124
311175
|
this.packageMetadata.queryableSources = outcome.packageMetadata.queryableSources;
|
|
311125
311176
|
this.packageMetadata.manifestLocation = outcome.packageMetadata.manifestLocation ?? null;
|
|
311126
311177
|
this.applyDiscoveryPolicyToModels();
|
|
@@ -311282,6 +311333,29 @@ class Package {
|
|
|
311282
311333
|
});
|
|
311283
311334
|
}
|
|
311284
311335
|
}
|
|
311336
|
+
lintTilesAgainstSurface(modelPath, manifest) {
|
|
311337
|
+
const model = this.models.get(modelPath);
|
|
311338
|
+
if (!model || !manifest.tiles)
|
|
311339
|
+
return [];
|
|
311340
|
+
const listedModel = (this.packageMetadata.explores ?? []).find((entry) => entry.endsWith(MODEL_FILE_SUFFIX) && !isDashboardModelPath(entry));
|
|
311341
|
+
const importInto = listedModel ? `a listed file such as "${listedModel}"` : `a file you list there`;
|
|
311342
|
+
const findings = [];
|
|
311343
|
+
for (const tile of manifest.tiles) {
|
|
311344
|
+
try {
|
|
311345
|
+
model.assertQueryBoundaryEarly(undefined, undefined, `run: ${tile.query}`);
|
|
311346
|
+
} catch (error) {
|
|
311347
|
+
if (!(error instanceof NotQueryableError))
|
|
311348
|
+
throw error;
|
|
311349
|
+
findings.push({
|
|
311350
|
+
model: modelPath,
|
|
311351
|
+
subject: manifest.name,
|
|
311352
|
+
message: `tile "${tile.query}" reads a source this package's surface ` + `does not publish, so the tile answers 404 once served, ` + `although the file compiles. Listing a file publishes what ` + `it declares, not what it imports. Fix: add the file that ` + `declares the source to 'explores', keeping the entries ` + `already there, or import it into ${importInto} and add ` + `it to that file's export { … }.`,
|
|
311353
|
+
severity: "error"
|
|
311354
|
+
});
|
|
311355
|
+
}
|
|
311356
|
+
}
|
|
311357
|
+
return findings;
|
|
311358
|
+
}
|
|
311285
311359
|
async lintDashboards(factsByPath, allFacts, heldBack, unconventionalSlugs, knownSlugs, droppedByError) {
|
|
311286
311360
|
const warnings = [];
|
|
311287
311361
|
const withheldDrills = new Map;
|
|
@@ -311315,10 +311389,11 @@ class Package {
|
|
|
311315
311389
|
});
|
|
311316
311390
|
}
|
|
311317
311391
|
}
|
|
311392
|
+
const remedy = this.surfaceIsIndexModel() ? `This package's surface is "${INDEX_MODEL_NAME}", and a ` + `dashboard file cannot be exported from it -- dashboards are ` + `files, not sources. To serve this one, declare (or extend) ` + `an 'explores' in publisher.json listing both ` + `"${INDEX_MODEL_NAME}" and "${modelPath}"; an explicit key ` + `overrides the convention. Or set ` + `queryableSources: "all" to keep the curated surface for ` + `discovery only.` : `Add it to 'explores', or set queryableSources: "all" to ` + `keep the curated surface for discovery only.`;
|
|
311318
311393
|
warnings.push({
|
|
311319
311394
|
model: modelPath,
|
|
311320
311395
|
subject: name,
|
|
311321
|
-
message: `is a dashboard, but "${modelPath}" is not
|
|
311396
|
+
message: `is a dashboard, but "${modelPath}" is not part of this ` + `package's discovery surface and this package sets ` + `queryableSources: "declared", so its query would be ` + `refused. It is not served. ${remedy} Listing it is not ` + `always sufficient on its own: the queryable sources are ` + `the union of every listed file's export closure, so a tile ` + `reading a source that only an UNLISTED file exports is ` + `still refused. List that file too, or re-export the source ` + `from one already listed.`,
|
|
311322
311397
|
severity: "warn"
|
|
311323
311398
|
});
|
|
311324
311399
|
}
|
|
@@ -311328,6 +311403,11 @@ class Package {
|
|
|
311328
311403
|
for (const finding of findings) {
|
|
311329
311404
|
warnings.push({ model: modelPath, ...finding });
|
|
311330
311405
|
}
|
|
311406
|
+
if (manifest) {
|
|
311407
|
+
for (const finding of this.lintTilesAgainstSurface(modelPath, manifest)) {
|
|
311408
|
+
warnings.push(finding);
|
|
311409
|
+
}
|
|
311410
|
+
}
|
|
311331
311411
|
}
|
|
311332
311412
|
const drillFacts = Array.from(allFacts.values());
|
|
311333
311413
|
for (const finding of withheldDrills.values()) {
|
|
@@ -312202,6 +312282,7 @@ ${source}` : source ?? "";
|
|
|
312202
312282
|
await this.rebindServeBindingsFromLocalStore(_package);
|
|
312203
312283
|
if (existingPackage !== undefined && reload) {
|
|
312204
312284
|
this.retireConnectionGeneration(`package ${packageName}`, () => existingPackage.getMalloyConfig().shutdown("close"));
|
|
312285
|
+
_package.noteSurfaceChangeFrom(existingPackage.getPackageMetadata().explores);
|
|
312205
312286
|
}
|
|
312206
312287
|
this.packages.set(packageName, _package);
|
|
312207
312288
|
this.setPackageStatus(packageName, "serving" /* SERVING */);
|
|
@@ -312505,11 +312586,12 @@ ${source}` : source ?? "";
|
|
|
312505
312586
|
const materializationBase = metadata.materialization !== undefined ? { ...metadata.materialization, ...preservedQueryMetadata } : onDiskMaterialization !== undefined ? { ...onDiskMaterialization } : undefined;
|
|
312506
312587
|
const materializationBlock = resolvedScope !== undefined ? { ...materializationBase ?? {}, scope: resolvedScope } : materializationBase;
|
|
312507
312588
|
const resolvedQueryMetadata = metadata.queryMetadata ?? materializationBlock?.queryMetadata ?? existingManifest.queryMetadata;
|
|
312589
|
+
const echoesDerivedSurface = existingManifest.explores === undefined && Array.isArray(metadata.explores) && metadata.explores.length === 1 && metadata.explores[0] === INDEX_MODEL_NAME;
|
|
312508
312590
|
const updatedManifest = {
|
|
312509
312591
|
...existingManifest,
|
|
312510
312592
|
name: metadata.name,
|
|
312511
312593
|
description: metadata.description,
|
|
312512
|
-
...metadata.explores !== undefined ? { explores: metadata.explores } : {},
|
|
312594
|
+
...metadata.explores !== undefined && !echoesDerivedSurface ? { explores: metadata.explores } : {},
|
|
312513
312595
|
...metadata.queryableSources !== undefined ? { queryableSources: metadata.queryableSources } : {},
|
|
312514
312596
|
...metadata.manifestLocation !== undefined ? { manifestLocation: metadata.manifestLocation } : {},
|
|
312515
312597
|
...resolvedScope !== undefined ? { scope: resolvedScope } : {},
|
|
@@ -319207,6 +319289,21 @@ If the agent could not reasonably have known to ask, that is a model gap.
|
|
|
319207
319289
|
| \`GUIDANCE-NOT-RETRIEVED\` | entities came back, governing guidance did not | model: put guidance on the entities agents search for |
|
|
319208
319290
|
| \`GUIDANCE-DECLINED\` | guidance was retrieved and judged inapplicable | model: state the business default, not a caveat |
|
|
319209
319291
|
|
|
319292
|
+
**Before diagnosing a \`NOT-RETURNED\`, check the call actually returned
|
|
319293
|
+
nothing.** The \`tool_call\` event carries \`retrieval_mode\` and \`rankedSummary\`.
|
|
319294
|
+
A \`rankedSummary\` of \`null\` means the response could not be read, not that it
|
|
319295
|
+
was empty -- a body too large for the model's context is written to a file, and
|
|
319296
|
+
the run records no summary for it. There is nothing to diagnose there: say the
|
|
319297
|
+
call was not measured.
|
|
319298
|
+
|
|
319299
|
+
And **never attribute a miss to the embedding index unless \`retrieval_mode\`
|
|
319300
|
+
says \`lexical\`.** The mode is recorded per call precisely so this is checkable.
|
|
319301
|
+
A diagnosis once explained a phantom empty result with "the index was not ready
|
|
319302
|
+
yet" on a run whose index was ready before the first question and whose
|
|
319303
|
+
response did hold results; the empty list was a parsing bug in the harness. An
|
|
319304
|
+
unverifiable cause that sounds right is worse than \`needs_human\`, because it
|
|
319305
|
+
closes the finding.
|
|
319306
|
+
|
|
319210
319307
|
A missing join is coverage, not an agent-call miss. The model has to volunteer
|
|
319211
319308
|
relationships. A declared join is not a retrieval entity; do not look for it in
|
|
319212
319309
|
\`get_context\` results.
|
|
@@ -320252,6 +320349,27 @@ Output, exactly this shape:
|
|
|
320252
320349
|
reading that produced it, that is \`no_match\`. Use \`near_match\` only when you
|
|
320253
320350
|
can name the rubric clause that makes the difference defensible.
|
|
320254
320351
|
|
|
320352
|
+
**A rubric clause cannot make a wrong VALUE defensible, and a clause that
|
|
320353
|
+
tries is a defect in the rubric rather than a licence to you.** \`near_match\`
|
|
320354
|
+
turns on the answer being right under a reading the QUESTION allows -- a tie
|
|
320355
|
+
broken the other way, a grain the question left open, a basis the question
|
|
320356
|
+
never fixed. It does not turn on the answer being transparent about how it
|
|
320357
|
+
got a figure the question did not ask for. Those two look alike in a rubric
|
|
320358
|
+
and are opposites in a report: one is a number a reader can act on, the
|
|
320359
|
+
other is a number a reader would act on wrongly. Naming the method makes a
|
|
320360
|
+
wrong figure DIAGNOSABLE, which is worth having, and it is not partial
|
|
320361
|
+
credit.
|
|
320362
|
+
|
|
320363
|
+
The test, before you write \`near_match\` on a case with a value: would a
|
|
320364
|
+
reader who acted on this figure be wrong? If yes, it is \`no_match\` however
|
|
320365
|
+
plainly the answer explained itself, and however the rubric is worded. Say
|
|
320366
|
+
in \`why\` that you are overriding a rubric clause, so the clause gets fixed.
|
|
320367
|
+
This rule exists because a set shipped one: a question asked for sales over
|
|
320368
|
+
the company's season, the answer gave the meteorological window 25% lower
|
|
320369
|
+
and said which window it used, and a clause granting \`near_match\` for a
|
|
320370
|
+
stated window kept a materially wrong answer out of the pass rate
|
|
320371
|
+
entirely -- the arm reported 100%.
|
|
320372
|
+
|
|
320255
320373
|
It is a third outcome because as a pass it was a large share of the measured
|
|
320256
320374
|
noise: the same unchanged answer reads \`match\` in one run and \`near_match\`
|
|
320257
320375
|
in the next, and the pass rate moves although nothing did. A verdict whose
|
|
@@ -320915,11 +321033,19 @@ the run measure something other than what it names:
|
|
|
320915
321033
|
first time somebody tidies up; the set, the run directory and the write-up
|
|
320916
321034
|
belong in git beside the model.
|
|
320917
321035
|
|
|
320918
|
-
2. The server must be up
|
|
320919
|
-
|
|
320920
|
-
|
|
320921
|
-
|
|
320922
|
-
|
|
321036
|
+
2. The server must be up. Where your host offers retrieval tracing, turn it on
|
|
321037
|
+
and confirm a trace lookup answers, so a call's ranked results can be
|
|
321038
|
+
recovered afterwards.
|
|
321039
|
+
|
|
321040
|
+
**Open-source Publisher has none.** \`PUBLISHER_MCP_TRACE=retrieval\` is set by
|
|
321041
|
+
\`serve.py\` and read by nothing: the trace store was written and never merged,
|
|
321042
|
+
so there is no trace tool and \`traceId\` is null on every local attempt. This
|
|
321043
|
+
does not block a scored run, because attribution never depended on it:
|
|
321044
|
+
\`rankedSummary\` is copied onto the \`tool_call\` event at capture, precisely so
|
|
321045
|
+
the evidence survives without a store. So do not refuse a local run for want
|
|
321046
|
+
of tracing -- an earlier version of this rule did, and it refused every local
|
|
321047
|
+
run there has ever been. Refuse one whose \`tool_call\` events carry no
|
|
321048
|
+
\`rankedSummary\`, which is what attribution actually reads.
|
|
320923
321049
|
|
|
320924
321050
|
3. Health-check: your host's status check until it reports serving, and inspect
|
|
320925
321051
|
\`loadErrors\`. A dead database that still answers HTTP is an environment
|
|
@@ -321087,6 +321213,13 @@ conductor. Do not:
|
|
|
321087
321213
|
|
|
321088
321214
|
- The model is the only thing improve edits. No question text, qids, or
|
|
321089
321215
|
expected values in any name, doc, or comment.
|
|
321216
|
+
- **You are measuring a model, not reviewing this harness.** Read a script when
|
|
321217
|
+
a number you have to report cannot be explained otherwise, and stop there. Do
|
|
321218
|
+
not audit the scripts, propose fixes to them, or hand back tooling critique
|
|
321219
|
+
in place of a result -- a run that ends in harness feedback has not answered
|
|
321220
|
+
the question it was asked. A defect that changed a number gets one sentence
|
|
321221
|
+
in the report; a defect that changed nothing gets one line in chat, once.
|
|
321222
|
+
Fixing it is a different task, and the user starts it.
|
|
321090
321223
|
- When the environment misbehaves, stop. Never diagnose a sick system. The
|
|
321091
321224
|
harness is part of the environment: a known-broken measurement does not
|
|
321092
321225
|
become quotable by being finished. Measured against this directive, an arm
|
|
@@ -322047,6 +322180,30 @@ one exception is anything that qualifies THIS run's number -- a truncated
|
|
|
322047
322180
|
attempt, a contaminated one, an unestablished answer key -- which the "Eval
|
|
322048
322181
|
failures" section exists for.
|
|
322049
322182
|
|
|
322183
|
+
**Do not audit the harness. You were asked to measure a model.** This is the
|
|
322184
|
+
most common way this job goes wrong, and it does not look like going wrong: a
|
|
322185
|
+
run turns up something odd in a script, the odd thing is genuinely a bug, and
|
|
322186
|
+
the reply comes back as a critique of the tooling with the model's score
|
|
322187
|
+
somewhere underneath. The reader asked what their model scored. Answer that.
|
|
322188
|
+
|
|
322189
|
+
So, unless the user asked you to work on the harness:
|
|
322190
|
+
|
|
322191
|
+
- Do not read harness source to satisfy your own curiosity about a number.
|
|
322192
|
+
Read it when a number you must report cannot be explained any other way, and
|
|
322193
|
+
stop when it can.
|
|
322194
|
+
- Do not propose harness fixes, refactors, flags or "while I was in there"
|
|
322195
|
+
improvements. Not in the report, not in the chat reply.
|
|
322196
|
+
- When a harness defect DID change this run's number, the report gets one
|
|
322197
|
+
sentence: what the number should be and why. Not the mechanism, not the
|
|
322198
|
+
file, not the fix.
|
|
322199
|
+
- Keep a defect that changed nothing out of the report entirely. Mention it
|
|
322200
|
+
once in chat, in a line, and let the user decide whether they want it
|
|
322201
|
+
chased.
|
|
322202
|
+
|
|
322203
|
+
A harness bug you found and did not chase is not a loose end. It is the job
|
|
322204
|
+
being done. If the user wants it fixed they will say so, and then it is a
|
|
322205
|
+
different task with its own turn.
|
|
322206
|
+
|
|
322050
322207
|
## Step 1: build the artifacts, before writing a word
|
|
322051
322208
|
|
|
322052
322209
|
\`\`\`bash
|
|
@@ -322085,6 +322242,29 @@ not in \`~/Downloads\`, both of which have lost a findings document before.
|
|
|
322085
322242
|
from the console.** A figure retyped from scrollback is a figure nobody can
|
|
322086
322243
|
check, and the console rounds.
|
|
322087
322244
|
|
|
322245
|
+
### Keep it under a page and a half
|
|
322246
|
+
|
|
322247
|
+
**Budget: about 80 lines, and 120 is the ceiling for a run with several
|
|
322248
|
+
distinct failures.** Reports have shipped at 200 and the length is not
|
|
322249
|
+
thoroughness -- it is the artifact links restated as prose, the cascade
|
|
322250
|
+
explained twice, and a paragraph apologising for a figure nobody disputed.
|
|
322251
|
+
A reader who wants the detail opens the case matrix, which is why step 1 builds
|
|
322252
|
+
it. What cannot be recovered from the artifacts is your judgement: what broke,
|
|
322253
|
+
why, and what to do. Spend the lines there.
|
|
322254
|
+
|
|
322255
|
+
What earns its place: the headline, one row per case, one short entry per
|
|
322256
|
+
failure, the retrieval numbers, and the next steps. What does not: restating a
|
|
322257
|
+
number you already gave, explaining what a cascade is before showing it,
|
|
322258
|
+
defending a decision nobody questioned, or any section whose content is that
|
|
322259
|
+
nothing happened -- except "Eval failures", where "None." is the point.
|
|
322260
|
+
|
|
322261
|
+
**Being brief is not being terse with the vocabulary.** The reader does not
|
|
322262
|
+
know what \`near_match\`, a cluster, recall or a holdout is, so the first time
|
|
322263
|
+
one appears, say what it means in the same sentence -- "\`near_match\`, which is
|
|
322264
|
+
excluded from the pass rate" -- and then use it. Cutting the explanations is
|
|
322265
|
+
how a short report becomes an unreadable one; cutting the restatements is how
|
|
322266
|
+
it becomes a good one.
|
|
322267
|
+
|
|
322088
322268
|
### The template
|
|
322089
322269
|
|
|
322090
322270
|
\`\`\`markdown
|
|
@@ -322098,7 +322278,7 @@ check, and the console rounds.
|
|
|
322098
322278
|
- Target \`<env>/<package>\`, model \`<modelPath>\`, pinned \`<modelSha or targetVersion>\`
|
|
322099
322279
|
- Answerer \`<model>\`, judge \`<model>\`, cap \`<maxTurns>\` turns
|
|
322100
322280
|
- Steps run: scrape/run, eval<, diagnose><, improve>
|
|
322101
|
-
- Cost $X answerer + $Y judge
|
|
322281
|
+
- Cost $X answerer + $Y judge, N turns median / N p90, N entities delivered per answer
|
|
322102
322282
|
|
|
322103
322283
|
## The result
|
|
322104
322284
|
|
|
@@ -322109,9 +322289,15 @@ check, and the console rounds.
|
|
|
322109
322289
|
|
|
322110
322290
|
[case matrix](<url>) - [aggregate tables](<url>)
|
|
322111
322291
|
|
|
322112
|
-
##
|
|
322292
|
+
## Coverage, retrieval, accuracy, cost
|
|
322113
322293
|
|
|
322114
|
-
|
|
322294
|
+
**Coverage N of M** -- how many questions the model can express an answer to at
|
|
322295
|
+
all. **Entity recall N%** -- one number, first, before the cascade. It is the share
|
|
322296
|
+
of the entities an answer needed that retrieval actually handed the agent, and
|
|
322297
|
+
it is the headline of this section for the same reason the pass rate is the
|
|
322298
|
+
headline of the last one. Then the cascade: Covered? -> Retrieved? -> Correct?,
|
|
322299
|
+
with the per-arm numbers under each. Say in one clause what recall counts, then
|
|
322300
|
+
give the number.
|
|
322115
322301
|
|
|
322116
322302
|
## Model failures
|
|
322117
322303
|
|
|
@@ -322140,10 +322326,10 @@ these in order and put every one that fires into the list, with its command:
|
|
|
322140
322326
|
|
|
322141
322327
|
| If the run shows | Then the next step is |
|
|
322142
322328
|
|---|---|
|
|
322143
|
-
| \`coverage: unmeasured\` | run
|
|
322329
|
+
| \`coverage: unmeasured\` | the run was given \`--no-coverage\`, or the measurement failed and said so. It is measured by default, so say in "Eval failures" that this score cannot tell a model gap from a bad answer, and re-run without the flag |
|
|
322144
322330
|
| \`goldenCheck: skipped\` or the set names no \`truthPackage\` | build one with \`init_truth_package.py\`; until then the goldens were derived through the model under test and certify themselves |
|
|
322145
322331
|
| any golden still \`provisional\` | re-derive and \`verify_goldens.py --promote\` |
|
|
322146
|
-
| a stale entity name warning | fix \`expectedEntities\`; it scores as a retrieval miss on every run until you do |
|
|
322332
|
+
| a stale entity name warning | fix \`expectedEntities\`; it scores as a retrieval miss on every run until you do. A next step, not a section: it goes in this list and nowhere else in the report |
|
|
322147
322333
|
| a passing case with recall below 1.0 | check whether \`required\` over-specifies one path |
|
|
322148
322334
|
| \`truncated\` non-empty | re-run those cases at a higher cap with \`--from\` |
|
|
322149
322335
|
| diagnose did not run | run it, or say the failures have no owner yet |
|
|
@@ -322186,8 +322372,23 @@ wrong answer. One entry each: what the answer said, what was right, and the
|
|
|
322186
322372
|
mechanism in one sentence. Do not write a cluster id here; write what it got
|
|
322187
322373
|
wrong.
|
|
322188
322374
|
|
|
322189
|
-
**Eval failures** are
|
|
322190
|
-
|
|
322375
|
+
**Eval failures** are the things that stopped the run measuring the model, and
|
|
322376
|
+
**only** those. The test is one question: *did this cost a verdict, or make one
|
|
322377
|
+
untrustworthy?* If no, it does not appear in the report at any length.
|
|
322378
|
+
|
|
322379
|
+
That rules out most of what is tempting to put here, and all of it has been put
|
|
322380
|
+
here on a real run: a \`-dirty\` model pin, a stale-entity-name warning that cost
|
|
322381
|
+
no case, a defect you hit in the harness and worked around, a setup step that
|
|
322382
|
+
took two tries, anything you would open with "worth knowing, though it changes
|
|
322383
|
+
no verdict". A reader wants to know what their model scored. Harness defects are
|
|
322384
|
+
real and belong in a harness issue, filed against the harness -- that is the
|
|
322385
|
+
skill's opening rule, and this section is where it gets broken.
|
|
322386
|
+
|
|
322387
|
+
The section is usually two words. "**None.**" is a complete and good answer, and
|
|
322388
|
+
a reader who sees it learns exactly what they need to. Do not pad it into a
|
|
322389
|
+
paragraph explaining the absence.
|
|
322390
|
+
|
|
322391
|
+
Report every one that DID occur, with its count and its qids, and say plainly
|
|
322191
322392
|
that these are NOT evidence about the model:
|
|
322192
322393
|
|
|
322193
322394
|
| What happened | How it reads in the ledger | Who fixes it |
|
|
@@ -322206,11 +322407,32 @@ harness prints \`INCOMPLETE\` and withholds the percentage; the report does the
|
|
|
322206
322407
|
same. Give the counts and the re-run command instead of a number with a caveat,
|
|
322207
322408
|
because the number is what gets repeated and the caveat is what gets dropped.
|
|
322208
322409
|
|
|
322209
|
-
##
|
|
322410
|
+
## The four measurements, and why a report carries all of them
|
|
322210
322411
|
|
|
322211
|
-
A pass rate says an answer was wrong. It does not say
|
|
322212
|
-
|
|
322213
|
-
|
|
322412
|
+
A pass rate says an answer was wrong. It does not say where, and on its own it
|
|
322413
|
+
makes every failure look like the model's. A run measures four things, in this
|
|
322414
|
+
order, and each one tells you whether the next is even a fair question:
|
|
322415
|
+
|
|
322416
|
+
| | Measures | A failure here means |
|
|
322417
|
+
|---|---|---|
|
|
322418
|
+
| **a. Coverage** | can the data and the model express an answer at all | nothing downstream was winnable. Fix the model |
|
|
322419
|
+
| **b. Retrieval** | did \`get_context\` hand the agent the entities it needed | the model has it and the agent never saw it: the docs, or the search wording |
|
|
322420
|
+
| **c. Accuracy** | did the agent get the answer right | it had what it needed and still missed: the agent, or a doc that misleads |
|
|
322421
|
+
| **d. Cost** | dollars, turns, wall-clock, entities per answer | it works and cannot be afforded, which is its own kind of not working |
|
|
322422
|
+
|
|
322423
|
+
**Coverage first, and a report without it is incomplete.** A model that cannot
|
|
322424
|
+
express an answer can never succeed at that question, so a pass rate quoted
|
|
322425
|
+
without coverage cannot distinguish a bad model from an unanswerable set.
|
|
322426
|
+
\`run_baseline.py\` measures it by default; if a run skipped it, say so in
|
|
322427
|
+
"Eval failures" and treat the score as provisional.
|
|
322428
|
+
|
|
322429
|
+
**Cost is a result, not an aside.** Report dollars, the turn and call counts,
|
|
322430
|
+
and the entity payload per answer. An agent that answers correctly in 40 turns
|
|
322431
|
+
and 75 delivered entities per question is a different product from one that
|
|
322432
|
+
does it in 8 and 5, and only the report says which you have.
|
|
322433
|
+
|
|
322434
|
+
Report them all, because a report that omits them makes every failure look
|
|
322435
|
+
like the model's:
|
|
322214
322436
|
|
|
322215
322437
|
**Copy the cascade the run printed. Do not re-derive it.** \`cascade_lines()\`
|
|
322216
322438
|
in \`run_baseline.py\` already renders every rung, and it carries one field a
|
|
@@ -322426,6 +322648,7 @@ This table is a catalogue of what exists, not of what is loaded. A host that ins
|
|
|
322426
322648
|
| \`skill:malloy-notebooks\` | Building Malloy notebooks (.malloynb) |
|
|
322427
322649
|
| \`skill:malloy-analysis-report\` | Combining validated queries into a notebook report or dashboard |
|
|
322428
322650
|
| \`skill:malloy-analysis-pitfalls\` | Checking a query and its results before presenting an answer |
|
|
322651
|
+
| \`skill:malloy-source-unreachable\` | A source is missing from discovery, or a query was refused with a 404 or 403 |
|
|
322429
322652
|
| \`malloy-notebook-chat\` | The chat is bound to a notebook or saved report; answer from its cells. Ships in \`analysis\`. |
|
|
322430
322653
|
| \`skill:malloy-phrase-detection\` | Turning a plain-English question into search targets for the context tool |
|
|
322431
322654
|
|
|
@@ -322482,7 +322705,7 @@ Call \`search_malloy_docs\` with your question. Use \`skill:malloy-patterns\` to
|
|
|
322482
322705
|
},
|
|
322483
322706
|
{
|
|
322484
322707
|
name: "malloy-analysis",
|
|
322485
|
-
description: "
|
|
322708
|
+
description: "Read before your first get_context call on any business or data question. The required workflow, not background: which source, which rows, how to verify. Skipping it causes confident wrong answers.",
|
|
322486
322709
|
body: `# Malloy analysis workflow
|
|
322487
322710
|
|
|
322488
322711
|
> **Tool names** are written bare here - \`get_context\`, \`execute_query\`, \`search_malloy_docs\`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
|
|
@@ -322891,6 +323114,13 @@ Scanned at a glance is a dashboard; read top to bottom is a notebook.
|
|
|
322891
323114
|
|
|
322892
323115
|
## Build sequence
|
|
322893
323116
|
|
|
323117
|
+
0. **CHECK THE PACKAGE CAN SERVE A DASHBOARD AT ALL.** If its root holds an \`index.malloy\`, that
|
|
323118
|
+
file is the package's surface, and a dashboard's file is not something it can \`export\`, so the
|
|
323119
|
+
dashboard you are about to write will compile and then be withheld. Every package made by the
|
|
323120
|
+
scaffolder has that file, so this is the common case, not the corner one. Serving dashboards
|
|
323121
|
+
there means declaring an \`explores\` in \`publisher.json\` naming \`index.malloy\` and every dashboard
|
|
323122
|
+
file, which overrides the convention. Settle that first: it is a different curation shape for the
|
|
323123
|
+
package, not a line to add at the end. See "Read the lint" for the warning it produces otherwise.
|
|
322894
323124
|
1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package:
|
|
322895
323125
|
\`get_context\` if you have it, otherwise the REST model endpoint or the \`.malloy\` files.
|
|
322896
323126
|
Never guess a name. A guessed field in a query fails the whole package load, not just that one
|
|
@@ -323301,8 +323531,8 @@ Package warnings after a reload are the dashboard's test suite. Fix all of them:
|
|
|
323301
323531
|
Findings carry a \`severity\`, but \`warn\` is the ordinary default and tells you nothing about how bad
|
|
323302
323532
|
one is. Read the text, not the severity and not the count. One message is worth recognising because
|
|
323303
323533
|
it changes what the rest of the list means: **"Dashboard lint stopped early, so this list is
|
|
323304
|
-
incomplete"**. A dashboard withheld from
|
|
323305
|
-
withheld file is not a clean bill of health.
|
|
323534
|
+
incomplete"**. A dashboard withheld from the package's surface also loses its own findings, so a
|
|
323535
|
+
short list for a withheld file is not a clean bill of health.
|
|
323306
323536
|
|
|
323307
323537
|
**Read the status the reload itself returns, not the listing.** One dashboard that fails to compile
|
|
323308
323538
|
fails the whole package load, and the reload answers **424** with the compile error. A package that
|
|
@@ -323317,11 +323547,22 @@ at all appears there too, without \`stale\`, and is absent from the listing enti
|
|
|
323317
323547
|
|
|
323318
323548
|
If the reload is 200 and the others are listed but yours is not, discovery skipped the file instead,
|
|
323319
323549
|
usually a missing or misspelled \`# artifact\` tag, which is the same mechanism that deliberately skips
|
|
323320
|
-
an untagged shared include. There is a second cause
|
|
323321
|
-
|
|
323322
|
-
|
|
323323
|
-
|
|
323324
|
-
|
|
323550
|
+
an untagged shared include. There is a second cause whenever the package has a curated surface: a
|
|
323551
|
+
dashboard whose file is off it is withheld rather than served, and the warning says so and names the
|
|
323552
|
+
fix. A package has a surface in two ways, and the second is easy to miss because no manifest field
|
|
323553
|
+
records it:
|
|
323554
|
+
|
|
323555
|
+
- the package's \`publisher.json\` carries an \`explores\` list, and the dashboard's file is not on it;
|
|
323556
|
+
- the package root holds an **\`index.malloy\`**, which IS the surface. This bites in both
|
|
323557
|
+
directions, and a dashboard's file is not something an \`index.malloy\` exports either way: adding
|
|
323558
|
+
that file to a package that has dashboards withholds every one of them, and adding a dashboard to
|
|
323559
|
+
a package that already has one (every scaffolded package does) means it is never served. The fix
|
|
323560
|
+
is the same in both directions, and it is not an edit to \`index.malloy\`: declare an \`explores\` in
|
|
323561
|
+
\`publisher.json\` naming \`index.malloy\` and every dashboard file.
|
|
323562
|
+
|
|
323563
|
+
Either way the surface is what matters, not the \`queryableSources\` setting, which is \`declared\` by
|
|
323564
|
+
default; a package with neither withholds nothing. Where there is a surface, a \`suggest\` source has
|
|
323565
|
+
to be queryable as well as resolvable, so it needs to be reachable through it too.
|
|
323325
323566
|
|
|
323326
323567
|
**A clean reload is not proof the tags are right.** The checks above read names and resolve them; the
|
|
323327
323568
|
separate warning for a tag that does not _parse_ is syntax only: it carries no
|
|
@@ -325687,7 +325928,7 @@ source: first_touch is conn.table('events') -> {
|
|
|
325687
325928
|
{
|
|
325688
325929
|
name: "malloy-modeling",
|
|
325689
325930
|
description: "Build semantic models with Malloy for the Malloy Publisher. Read this skill whenever the user asks about modeling data or specifically mentions Malloy.",
|
|
325690
|
-
body: "# STOP - READ BEFORE WRITING ANY MALLOY CODE\n\n> **AI AGENTS: You MUST review this file before writing Malloy code.** Cross-skill references below use logical `skill:` names; load the referenced skill before acting. Before writing code, also read the gotcha skills: `skill:malloy-gotchas-modeling`, `skill:malloy-gotchas-queries`, and `skill:malloy-gotchas-rendering`.\n\n## Pre-Flight Checklist\n\n1. **Discover first**: ground yourself before writing ANY code, with the tool that matches what you are modelling.\n - Modelling data **already in a package**: `get_context` returns that package's sources, views, and fields (with their docs).\n - Modelling **a database with no package yet**: `get_context` has nothing to return, so use `search_database_schema` instead. It walks the connection's schemas and tables, ranks them against a plain-English description, and gives you each table's columns plus the `source:` line to start from. Take those names verbatim into step 5.\n Never guess field names either way.\n2. **Search docs proactively**: call `search_malloy_docs` BEFORE writing unfamiliar patterns (window functions, query-based sources, pipelines). Don't guess. Malloy syntax is specific and SQL intuition is often wrong.\n3. **Use `skill:malloy-patterns`** to discover available doc topics (YoY, cohorts, rendering, window functions).\n4. **Check diagnostics** after writing: fix the FIRST error first, errors cascade.\n5. **Read the gotcha skills**: `skill:malloy-gotchas-modeling`, `skill:malloy-gotchas-queries`, and `skill:malloy-gotchas-rendering` prevent the most common mistakes.\n\n**Quick syntax reminders:**\n1. **Backtick reserved words:** `` `Date` ``, `` `Hour` ``, `` `Timestamp` ``, `` `Type` ``, `` `number` ``, `` `source` ``\n2. **Use `having:` for aggregate filters**: not `where:` on measures\n3. **Alias joined fields in `group_by`** if using them in `order_by`\n4. **`count()` counts rows; `count(x)` counts distinct values of `x`**: `count(distinct x)` is deprecated, write `count(x)`\n5. **One tag per line**: `# label=\"Revenue\"` and `# currency` on separate lines\n6. **No fixed scale on measures**: use `# currency` not `# currency=usd0m`\n7. **Cast strings for aggregates:** `avg(score::number)` not `avg(score)`\n8. **Boolean columns:** use `= true` not `= 'true'` (no quotes!)\n9. **Read data files in place:** `.csv`, `.parquet`, `.json`, `.ndjson`, and `.xlsx` all work as-is through `duckdb.table('data/file.ext')`. Never convert a file to another format first, and never read one with python or jq to \"have a look\" first: query it. For `.xlsx`, check the row count before trusting it: a workbook with a title row or a blank spacer reads short and reports no error. (Per-format quirks: `skill:malloy-gotchas-modeling`)\n\n## Planning and `modeling-notes.md`\n\nIf the IDE has a native plan mode, use it for the high-level approach: do data exploration during planning, then present a concrete plan for user approval before writing any files.\n\n`modeling-notes.md` is an expected output of the workflow, not an optional extra. Start it at step 2 (Propose Scope) and grow it as you work: it persists alongside the model, and its value is as the thing the user argues with at step 3, before source files exist; written after the build it can only document decisions already baked in. Record findings and problems as they are found during discovery (`skill:malloy-discover`), and every unconfirmed decision as an open item. Only when there is no writable workspace do the notes live in the conversation instead.\n\nKeep it compact, with these sections:\n\n```markdown\n# Modeling notes - <package>\n## Scope what was confirmed, what the model is FOR, skip list with reasons\n## Grain and keys proven by query, not by column name\n## Coverage coverage cliffs; columns excluded for nullity\n## Decisions each with its evidence\n## Open decisions ASSUMPTIONS, NOT CONFIRMED: every threshold or definition the user\n has not settled, one entry each, mirrored by a hedge in its #(doc)\n## Validation reconciliation checks performed, and their results\n```\n\n## 8-Step Modeling Workflow\n\nThe agent orchestrates all steps. Steps marked **(user)** pause for input. Each step has a dedicated skill with full instructions. Read each step's skill **before starting that step**, including the decision skills for steps 1–4 (`skill:malloy-discover`, `skill:malloy-scope`, `skill:malloy-define`). They govern what the model says; skipping them to reach the build skills is how unreviewed business logic ships.\n\n**A field is not complete until it has its definition, `#(doc)` tag, and rendering tags, and any threshold or business convention in it is user-confirmed, distribution-derived, or explicitly flagged in its `#(doc)`** (see `skill:malloy-document` § Mark conventions as conventions). Documentation is part of defining a field, not a separate activity. Read `skill:malloy-document` for full documentation standards (doc string writing, tag ordering).\n\n```\nDISCOVER → SCOPE → SOURCES → DEFINITIONS → BUILD BASE → BUILD JOINED → REVIEW → CURATE\n (silent) (user) (user) (user) (agent) (agent) (user) (user)\n```\n\n| Step | Skill | What Happens |\n|------|-------|-------------|\n| 1. Discover | `skill:malloy-discover` | Read the model and data; scan sources, fields, distributions; detect prior art. With no package yet, start from `search_database_schema` to find the tables in the connection |\n| 2. Propose Scope | `skill:malloy-scope` | Present findings, user selects focus |\n| 3. Propose Sources | `skill:malloy-define` | Propose source plan, user confirms architecture |\n| 4. Propose Definitions | `skill:malloy-define` | Propose fields per base source, user confirms logic |\n| 5. Build Base Sources | `skill:malloy-model` | Write fully documented base source files (one per table), check diagnostics. Read `skill:malloy-document` for doc standards. |\n| 6. Build Joined Sources | `skill:malloy-model` | Write fully documented joined source files, validate. Read `skill:malloy-document` for doc standards. |\n| 7. Review | (none) | Present the review checklist below; user confirms or corrects |\n| 8. Curate | `skill:malloy-model` | Propose access controls (`explores`, `queryableSources`, access modifiers); always propose, the user decides whether to apply |\n\n### The pauses are the point\n\nThese are governed semantic models: the business decisions in them must be confirmed by a human subject-matter expert, and the **(user)** steps exist to collect that confirmation. They are real stops, not progress reports. A model can be complete, compiling, and fully documented and still be wrong everywhere it guessed; a capable agent can build the whole thing without pausing once, which is exactly the failure mode this workflow exists to prevent.\n\nWhen a decision goes unanswered (the user explicitly declines to decide, or nobody is there to ask), do not silently proceed as if it were settled. Take your best-supported position, label it an assumption in the field's own `#(doc)` (see `skill:malloy-document` § Mark conventions as conventions), record it under \"Open decisions\" in `modeling-notes.md`, and raise it again at Review. An unlabeled assumption is indistinguishable from a confirmed fact, and misleads everyone downstream.\n\n### Step 7 Review is a checklist, not a summary\n\nPresent these to the user, with answers:\n\n- **Which definitions did the user actually confirm?** List them; everything else is an assumption.\n- **Which thresholds and bucket boundaries did you choose?** For each: the evidence (distribution query, metadata, prior art) and the `#(doc)` hedge that marks it.\n- **Which questions were left unanswered?** Each must already carry a labeled assumption and an \"Open decisions\" entry.\n- **Does the headline metric have more than one defensible definition?** If yes, that is a blocking question: put the candidate definitions to the user with their counts side by side, not in a footnote.\n\nThe user confirming this checklist is what makes the model governed. A summary of what you built is not a checkpoint.\n\nPublishing is out of scope for open-source v1. Self-hosters move a finished model into a served package via git and the host's publish path; see `skill:malloy-publish` for the local-to-served handoff.\n\n**Two paths to a model: both produce the same fully documented result:**\n- **Schema-first:** \"Model my data\" → 8-step workflow above using the relevant skills\n- **Analysis-first:** a data question arrives before any model exists → `skill:malloy-model-as-you-go`. It answers the question with `skill:malloy-analysis`, then codifies what the answer assumed into the model, one question at a time, confirming binding decisions first. The model exists by the end; there is no separate formalize step.\n- **Open-ended exploration** with no intent to keep anything: `skill:malloy-analyze`. If it turns into something worth keeping, formalize via `skill:malloy-model` (`reference/analysis-to-model.md`).\n\n## Agent Behavior\n\n**Research before asking.** Present proposals with evidence. Never ask open-ended questions: propose with data and let the user confirm.\n\n**Use business language.** Say \"I simplified the column name\" not \"reserved word replaced.\" Don't expose Malloy internals unless the user asks.\n\n**Describe what you're doing, not which step you're on.** The user doesn't have the skill files open. Say \"I'll propose which tables to include and how they relate\" not \"Steps 3 and 4.\" Say \"Now I'll write the source files\" not \"Moving to Step 5.\" Explain the purpose of each phase in plain language before doing it.\n\n**Present choices as A/B/C.** When asking the user to choose, use lettered options with one-line descriptions. Mark your recommendation.\n\n**Complete all workflow steps.** Once modeling begins, complete through Review and propose Curate. A field without documentation is not finished. If you lose track, re-read the model and your notes. Suggest notebooks at the end.\n\n## Route by Intent\n\n| User says... | Route to |\n|-------------|----------|\n| \"Model my data\", \"create a model\" | 8-step workflow (`skill:malloy-discover`) |\n| \"Model from LookML\" | 8-step with prior art via `skill:malloy-lookml-review` |\n| \"Explore this data\", \"what's interesting?\", \"show me the top X\" | `skill:malloy-analyze` (EDA) |\n| \"Build a dashboard\", \"create views\" on existing model | `skill:malloy-analyze` (views), plus `skill:malloy-charts` or `skill:malloy-notebooks` as needed |\n| \"Build a model but not sure what metrics\" | `skill:malloy-model-as-you-go`: answer their first real question, codify what it assumed, repeat |\n\n**If the user's first message is a data question** (not \"build me a model\"), route to `skill:malloy-model-as-you-go`. It answers with `skill:malloy-analysis` and grows the model from what each answer assumed, so there is nothing to formalize afterwards.\n\n## Additional Support Skills\n\nThese supplemental skills may also be loaded as needed:\n\n- **`skill:malloy`**: Index of Malloy skills and routing guide\n- **`skill:malloy-debug`**: Fix compile errors and interpret diagnostics\n\n## Publisher MCP Tools\n\nEnsure the Publisher MCP tools are configured before modeling. No server yet? `skill:malloy-getting-started` covers setup, including the one-command scaffolder (`npm create @malloy-publisher/malloy-package@latest <name>`) and why local authoring needs `--watch-env <env>`: start the server without it and your saved edits are never read.\n\n| Tool | Purpose |\n|------|---------|\n| `get_context` | Ground yourself in a package: its sources, views, and fields |\n| `execute_query` | Run ad-hoc queries for validation |\n| `compile_model` | Compile-check a change and get diagnostics back without running a query |\n| `reload_package` | Recompile a package from disk so a saved edit becomes queryable by name |\n| `search_malloy_docs` | Search Malloy docs (call BEFORE unfamiliar patterns) |\n| `search_database_schema` | Find the tables in a database connection by plain-English description, when modelling data that is not in a package yet. Returns each table's columns and the `source:` line to start from. Names and types only: no row value is returned |\n\nNever guess field names. Ground yourself with `get_context` to see the sources and fields a package defines.\n\n### The edit-and-run loop\n\nPublisher compiles each configured package at boot and serves that cached model, so a source or view you add afterwards is not queryable by name until you reload the package. The loop is:\n\n1. **Validate** the change with `compile_model`, picking the scope that matches what you are doing:\n - Adding a new definition or query: the default (`scope: \"append\"`) compiles your text in the model's namespace. Note its diagnostic positions land in the model-plus-your-text concatenation.\n - **Editing an existing definition: `scope: \"file\"`**, with the whole edited file as `source`. It compiles your text AS the file (append would collide with \"Cannot redefine\"), and diagnostics land at the true line numbers of your text.\n - Before saving a change other files import: `scope: \"package\"` with the edited file as `source` runs reload's worker compiler over every `.malloy` and `.malloynb` file against your edit, so a rename that breaks an importer surfaces now instead of at reload. Each diagnostic carries `model`, the file it points at; files hidden from discovery can appear. If `modelPath` does not exactly match an existing file, a warning says the source was treated as new.\n2. **Save** it to the package's model file.\n3. **Reload** with `reload_package`.\n4. **Run** the new view with `execute_query`.\n\nA reload that fails to compile is safe: your files are left alone and the previously compiled model keeps serving, with the compile errors returned to you. Compile first anyway for faster feedback, and a `scope: \"package\"` dry-run with no `source` uses reload's compiler and file selection (imports across files, every `.malloy` and `.malloynb` file as saved) without touching the served model. Keep the source of truth outside `publisher_data/`, which is not version-controlled and is wiped by a `--init` restart. If these tools are missing, the Publisher you are connected to predates them; fall back to validating with a throwaway `execute_query`. An older Publisher that has `compile_model` but rejects `scope` supports only the append behavior.\n\n## SQL-to-Malloy Quick Reference\n\n| SQL | Malloy |\n|-----|--------|\n| `COUNT(*)` | `count()` |\n| `COUNT(DISTINCT x)` | `count(x)` |\n| `NOW()` | `now` |\n| `CASE WHEN...END` | `pick...when...else` |\n| `col IN ('a','b')` | `col ? 'a' \\| 'b'` |\n| `COALESCE(a,b)` | `a ?? b` |\n| `CAST(x AS type)` | `x::type` |\n| `DATEDIFF(day, a, b)` | `days(a to b)` |\n| `CONCAT(a, b)` or `a \\|\\| b` | `concat(a, b)` |\n| `TIMESTAMP_DIFF(a, b, SECOND)` | `seconds(b to a)` |\n\n## Critical Rules\n\n1. **All keywords require colons**: `source:`, `dimension:`, `measure:`, `view:`\n2. **Use `is` not `as`**: `dimension: name is expression`\n3. **Arrow operator required**: `run: source -> { operations }`\n4. **Specify join type**: `join_one:`, `join_many:`, `join_cross:`\n5. **Safe division**: `revenue / nullif(count, 0)`\n6. **Group definitions under one keyword**: `measure:` then indent fields beneath\n\n## Common Anti-Patterns\n\n```\nWRONG: source flights is ... RIGHT: source: flights is ...\nWRONG: dimension: x as y RIGHT: dimension: y is x\nWRONG: count(*) RIGHT: count()\nWRONG: count(distinct x) RIGHT: count(x)\nWRONG: revenue / order_count RIGHT: revenue / nullif(order_count, 0)\nWRONG: run: src { ... } RIGHT: run: src -> { ... }\n```\n\n## Reserved Words: Scan Schema First\n\n**Malloy has many reserved words. When in doubt, backtick it.** Most likely to appear as column names:\n\n```\ndate, time, day, month, year, quarter, week, hour, minute, second,\nnumber, string, boolean, type, table, source, index, count, sum, avg, min, max,\ntrue, false, null, is, on, with, all, from, by, in, to, for, select, order_by,\ntop, bottom, desc, asc, row, range, current, window, rank\n```\n\n- `number`: only the bare word needs backticking; `account_number` is fine\n- `source`: reserved; use a different alias like `traffic_source`\n- `string`, `boolean`, `true`, `false`: backtick any column with these exact names\n\n## Gotcha Skills: Read Before Writing Code\n\nThe following skills contain detailed WRONG/RIGHT patterns that prevent the most common Malloy errors. **Read them before writing code:**\n\n- **`skill:malloy-gotchas-modeling`**: Reserved words, NULL checks, date functions, type casts, rename pitfalls, query-based source gotchas, `conn.sql()` anti-pattern\n- **`skill:malloy-gotchas-queries`**: Chart constraints, aggregate filters, joined field aliasing, time truncation vs extraction\n- **`skill:malloy-gotchas-rendering`**: Tag syntax, scale rules, sparkline setup, big_value patterns"
|
|
325931
|
+
body: "# STOP - READ BEFORE WRITING ANY MALLOY CODE\n\n> **AI AGENTS: You MUST review this file before writing Malloy code.** Cross-skill references below use logical `skill:` names; load the referenced skill before acting. Before writing code, also read the gotcha skills: `skill:malloy-gotchas-modeling`, `skill:malloy-gotchas-queries`, and `skill:malloy-gotchas-rendering`.\n\n## Pre-Flight Checklist\n\n1. **Discover first**: ground yourself before writing ANY code, with the tool that matches what you are modelling.\n - Modelling data **already in a package**: `get_context` returns that package's sources, views, and fields (with their docs).\n - Modelling **a database with no package yet**: `get_context` has nothing to return, so use `search_database_schema` instead. It walks the connection's schemas and tables, ranks them against a plain-English description, and gives you each table's columns plus the `source:` line to start from. Take those names verbatim into step 5.\n Never guess field names either way.\n2. **Search docs proactively**: call `search_malloy_docs` BEFORE writing unfamiliar patterns (window functions, query-based sources, pipelines). Don't guess. Malloy syntax is specific and SQL intuition is often wrong.\n3. **Use `skill:malloy-patterns`** to discover available doc topics (YoY, cohorts, rendering, window functions).\n4. **Check diagnostics** after writing: fix the FIRST error first, errors cascade.\n5. **Read the gotcha skills**: `skill:malloy-gotchas-modeling`, `skill:malloy-gotchas-queries`, and `skill:malloy-gotchas-rendering` prevent the most common mistakes.\n\n**Quick syntax reminders:**\n1. **Backtick reserved words:** `` `Date` ``, `` `Hour` ``, `` `Timestamp` ``, `` `Type` ``, `` `number` ``, `` `source` ``\n2. **Use `having:` for aggregate filters**: not `where:` on measures\n3. **Alias joined fields in `group_by`** if using them in `order_by`\n4. **`count()` counts rows; `count(x)` counts distinct values of `x`**: `count(distinct x)` is deprecated, write `count(x)`\n5. **One tag per line**: `# label=\"Revenue\"` and `# currency` on separate lines\n6. **No fixed scale on measures**: use `# currency` not `# currency=usd0m`\n7. **Cast strings for aggregates:** `avg(score::number)` not `avg(score)`\n8. **Boolean columns:** use `= true` not `= 'true'` (no quotes!)\n9. **Read data files in place:** `.csv`, `.parquet`, `.json`, `.ndjson`, and `.xlsx` all work as-is through `duckdb.table('data/file.ext')`. Never convert a file to another format first, and never read one with python or jq to \"have a look\" first: query it. For `.xlsx`, check the row count before trusting it: a workbook with a title row or a blank spacer reads short and reports no error. (Per-format quirks: `skill:malloy-gotchas-modeling`)\n\n## Planning and `modeling-notes.md`\n\nIf the IDE has a native plan mode, use it for the high-level approach: do data exploration during planning, then present a concrete plan for user approval before writing any files.\n\n`modeling-notes.md` is an expected output of the workflow, not an optional extra. Start it at step 2 (Propose Scope) and grow it as you work: it persists alongside the model, and its value is as the thing the user argues with at step 3, before source files exist; written after the build it can only document decisions already baked in. Record findings and problems as they are found during discovery (`skill:malloy-discover`), and every unconfirmed decision as an open item. Only when there is no writable workspace do the notes live in the conversation instead.\n\nKeep it compact, with these sections:\n\n```markdown\n# Modeling notes - <package>\n## Scope what was confirmed, what the model is FOR, skip list with reasons\n## Grain and keys proven by query, not by column name\n## Coverage coverage cliffs; columns excluded for nullity\n## Decisions each with its evidence\n## Open decisions ASSUMPTIONS, NOT CONFIRMED: every threshold or definition the user\n has not settled, one entry each, mirrored by a hedge in its #(doc)\n## Validation reconciliation checks performed, and their results\n```\n\n## 8-Step Modeling Workflow\n\nThe agent orchestrates all steps. Steps marked **(user)** pause for input. Each step has a dedicated skill with full instructions. Read each step's skill **before starting that step**, including the decision skills for steps 1–4 (`skill:malloy-discover`, `skill:malloy-scope`, `skill:malloy-define`). They govern what the model says; skipping them to reach the build skills is how unreviewed business logic ships.\n\n**A field is not complete until it has its definition, `#(doc)` tag, and rendering tags, and any threshold or business convention in it is user-confirmed, distribution-derived, or explicitly flagged in its `#(doc)`** (see `skill:malloy-document` § Mark conventions as conventions). Documentation is part of defining a field, not a separate activity. Read `skill:malloy-document` for full documentation standards (doc string writing, tag ordering).\n\n```\nDISCOVER → SCOPE → SOURCES → DEFINITIONS → BUILD BASE → BUILD JOINED → REVIEW → CURATE\n (silent) (user) (user) (user) (agent) (agent) (user) (user)\n```\n\n| Step | Skill | What Happens |\n|------|-------|-------------|\n| 1. Discover | `skill:malloy-discover` | Read the model and data; scan sources, fields, distributions; detect prior art. With no package yet, start from `search_database_schema` to find the tables in the connection |\n| 2. Propose Scope | `skill:malloy-scope` | Present findings, user selects focus |\n| 3. Propose Sources | `skill:malloy-define` | Propose source plan, user confirms architecture |\n| 4. Propose Definitions | `skill:malloy-define` | Propose fields per base source, user confirms logic |\n| 5. Build Base Sources | `skill:malloy-model` | Write fully documented base source files (one per table), check diagnostics. Read `skill:malloy-document` for doc standards. |\n| 6. Build Joined Sources | `skill:malloy-model` | Write fully documented joined source files, validate. Read `skill:malloy-document` for doc standards. |\n| 7. Review | (none) | Present the review checklist below; user confirms or corrects |\n| 8. Curate | `skill:malloy-model` | Propose the published surface (an `index.malloy` with `export { ... }`) and access controls (access modifiers, gates); always propose, the user decides whether to apply |\n\n### The pauses are the point\n\nThese are governed semantic models: the business decisions in them must be confirmed by a human subject-matter expert, and the **(user)** steps exist to collect that confirmation. They are real stops, not progress reports. A model can be complete, compiling, and fully documented and still be wrong everywhere it guessed; a capable agent can build the whole thing without pausing once, which is exactly the failure mode this workflow exists to prevent.\n\nWhen a decision goes unanswered (the user explicitly declines to decide, or nobody is there to ask), do not silently proceed as if it were settled. Take your best-supported position, label it an assumption in the field's own `#(doc)` (see `skill:malloy-document` § Mark conventions as conventions), record it under \"Open decisions\" in `modeling-notes.md`, and raise it again at Review. An unlabeled assumption is indistinguishable from a confirmed fact, and misleads everyone downstream.\n\n### Step 7 Review is a checklist, not a summary\n\nPresent these to the user, with answers:\n\n- **Which definitions did the user actually confirm?** List them; everything else is an assumption.\n- **Which thresholds and bucket boundaries did you choose?** For each: the evidence (distribution query, metadata, prior art) and the `#(doc)` hedge that marks it.\n- **Which questions were left unanswered?** Each must already carry a labeled assumption and an \"Open decisions\" entry.\n- **Does the headline metric have more than one defensible definition?** If yes, that is a blocking question: put the candidate definitions to the user with their counts side by side, not in a footnote.\n\nThe user confirming this checklist is what makes the model governed. A summary of what you built is not a checkpoint.\n\nPublishing is out of scope for open-source v1. Self-hosters move a finished model into a served package via git and the host's publish path; see `skill:malloy-publish` for the local-to-served handoff.\n\n**Two paths to a model: both produce the same fully documented result:**\n- **Schema-first:** \"Model my data\" → 8-step workflow above using the relevant skills\n- **Analysis-first:** a data question arrives before any model exists → `skill:malloy-model-as-you-go`. It answers the question with `skill:malloy-analysis`, then codifies what the answer assumed into the model, one question at a time, confirming binding decisions first. The model exists by the end; there is no separate formalize step.\n- **Open-ended exploration** with no intent to keep anything: `skill:malloy-analyze`. If it turns into something worth keeping, formalize via `skill:malloy-model` (`reference/analysis-to-model.md`).\n\n## Agent Behavior\n\n**Research before asking.** Present proposals with evidence. Never ask open-ended questions: propose with data and let the user confirm.\n\n**Use business language.** Say \"I simplified the column name\" not \"reserved word replaced.\" Don't expose Malloy internals unless the user asks.\n\n**Describe what you're doing, not which step you're on.** The user doesn't have the skill files open. Say \"I'll propose which tables to include and how they relate\" not \"Steps 3 and 4.\" Say \"Now I'll write the source files\" not \"Moving to Step 5.\" Explain the purpose of each phase in plain language before doing it.\n\n**Present choices as A/B/C.** When asking the user to choose, use lettered options with one-line descriptions. Mark your recommendation.\n\n**Complete all workflow steps.** Once modeling begins, complete through Review and propose Curate. A field without documentation is not finished. If you lose track, re-read the model and your notes. Suggest notebooks at the end.\n\n## Route by Intent\n\n| User says... | Route to |\n|-------------|----------|\n| \"Model my data\", \"create a model\" | 8-step workflow (`skill:malloy-discover`) |\n| \"Model from LookML\" | 8-step with prior art via `skill:malloy-lookml-review` |\n| \"Explore this data\", \"what's interesting?\", \"show me the top X\" | `skill:malloy-analyze` (EDA) |\n| \"Build a dashboard\", \"create views\" on existing model | `skill:malloy-analyze` (views), plus `skill:malloy-charts` or `skill:malloy-notebooks` as needed |\n| \"Build a model but not sure what metrics\" | `skill:malloy-model-as-you-go`: answer their first real question, codify what it assumed, repeat |\n\n**If the user's first message is a data question** (not \"build me a model\"), route to `skill:malloy-model-as-you-go`. It answers with `skill:malloy-analysis` and grows the model from what each answer assumed, so there is nothing to formalize afterwards.\n\n## Additional Support Skills\n\nThese supplemental skills may also be loaded as needed:\n\n- **`skill:malloy`**: Index of Malloy skills and routing guide\n- **`skill:malloy-debug`**: Fix compile errors and interpret diagnostics\n\n## Publisher MCP Tools\n\nEnsure the Publisher MCP tools are configured before modeling. No server yet? `skill:malloy-getting-started` covers setup, including the one-command scaffolder (`npm create @malloy-publisher/malloy-package@latest <name>`) and why local authoring needs `--watch-env <env>`: start the server without it and your saved edits are never read.\n\n| Tool | Purpose |\n|------|---------|\n| `get_context` | Ground yourself in a package: its sources, views, and fields |\n| `execute_query` | Run ad-hoc queries for validation |\n| `compile_model` | Compile-check a change and get diagnostics back without running a query |\n| `reload_package` | Recompile a package from disk so a saved edit becomes queryable by name |\n| `search_malloy_docs` | Search Malloy docs (call BEFORE unfamiliar patterns) |\n| `search_database_schema` | Find the tables in a database connection by plain-English description, when modelling data that is not in a package yet. Returns each table's columns and the `source:` line to start from. Names and types only: no row value is returned |\n\nNever guess field names. Ground yourself with `get_context` to see the sources and fields a package defines.\n\n### The edit-and-run loop\n\nPublisher compiles each configured package at boot and serves that cached model, so a source or view you add afterwards is not queryable by name until you reload the package. The loop is:\n\n1. **Validate** the change with `compile_model`, picking the scope that matches what you are doing:\n - Adding a new definition or query: the default (`scope: \"append\"`) compiles your text in the model's namespace. Note its diagnostic positions land in the model-plus-your-text concatenation.\n - **Editing an existing definition: `scope: \"file\"`**, with the whole edited file as `source`. It compiles your text AS the file (append would collide with \"Cannot redefine\"), and diagnostics land at the true line numbers of your text.\n - Before saving a change other files import: `scope: \"package\"` with the edited file as `source` runs reload's worker compiler over every `.malloy` and `.malloynb` file against your edit, so a rename that breaks an importer surfaces now instead of at reload. Each diagnostic carries `model`, the file it points at; files hidden from discovery can appear. If `modelPath` does not exactly match an existing file, a warning says the source was treated as new.\n2. **Save** it to the package's model file.\n3. **Reload** with `reload_package`.\n4. **Run** the new view with `execute_query`.\n\nA reload that fails to compile is safe: your files are left alone and the previously compiled model keeps serving, with the compile errors returned to you. Compile first anyway for faster feedback, and a `scope: \"package\"` dry-run with no `source` uses reload's compiler and file selection (imports across files, every `.malloy` and `.malloynb` file as saved) without touching the served model. Keep the source of truth outside `publisher_data/`, which is not version-controlled and is wiped by a `--init` restart. If these tools are missing, the Publisher you are connected to predates them; fall back to validating with a throwaway `execute_query`. An older Publisher that has `compile_model` but rejects `scope` supports only the append behavior.\n\n## SQL-to-Malloy Quick Reference\n\n| SQL | Malloy |\n|-----|--------|\n| `COUNT(*)` | `count()` |\n| `COUNT(DISTINCT x)` | `count(x)` |\n| `NOW()` | `now` |\n| `CASE WHEN...END` | `pick...when...else` |\n| `col IN ('a','b')` | `col ? 'a' \\| 'b'` |\n| `COALESCE(a,b)` | `a ?? b` |\n| `CAST(x AS type)` | `x::type` |\n| `DATEDIFF(day, a, b)` | `days(a to b)` |\n| `CONCAT(a, b)` or `a \\|\\| b` | `concat(a, b)` |\n| `TIMESTAMP_DIFF(a, b, SECOND)` | `seconds(b to a)` |\n\n## Critical Rules\n\n1. **All keywords require colons**: `source:`, `dimension:`, `measure:`, `view:`\n2. **Use `is` not `as`**: `dimension: name is expression`\n3. **Arrow operator required**: `run: source -> { operations }`\n4. **Specify join type**: `join_one:`, `join_many:`, `join_cross:`\n5. **Safe division**: `revenue / nullif(count, 0)`\n6. **Group definitions under one keyword**: `measure:` then indent fields beneath\n\n## Common Anti-Patterns\n\n```\nWRONG: source flights is ... RIGHT: source: flights is ...\nWRONG: dimension: x as y RIGHT: dimension: y is x\nWRONG: count(*) RIGHT: count()\nWRONG: count(distinct x) RIGHT: count(x)\nWRONG: revenue / order_count RIGHT: revenue / nullif(order_count, 0)\nWRONG: run: src { ... } RIGHT: run: src -> { ... }\n```\n\n## Reserved Words: Scan Schema First\n\n**Malloy has many reserved words. When in doubt, backtick it.** Most likely to appear as column names:\n\n```\ndate, time, day, month, year, quarter, week, hour, minute, second,\nnumber, string, boolean, type, table, source, index, count, sum, avg, min, max,\ntrue, false, null, is, on, with, all, from, by, in, to, for, select, order_by,\ntop, bottom, desc, asc, row, range, current, window, rank\n```\n\n- `number`: only the bare word needs backticking; `account_number` is fine\n- `source`: reserved; use a different alias like `traffic_source`\n- `string`, `boolean`, `true`, `false`: backtick any column with these exact names\n\n## Gotcha Skills: Read Before Writing Code\n\nThe following skills contain detailed WRONG/RIGHT patterns that prevent the most common Malloy errors. **Read them before writing code:**\n\n- **`skill:malloy-gotchas-modeling`**: Reserved words, NULL checks, date functions, type casts, rename pitfalls, query-based source gotchas, `conn.sql()` anti-pattern\n- **`skill:malloy-gotchas-queries`**: Chart constraints, aggregate filters, joined field aliasing, time truncation vs extraction\n- **`skill:malloy-gotchas-rendering`**: Tag syntax, scale rules, sparkline setup, big_value patterns"
|
|
325691
325932
|
},
|
|
325692
325933
|
{
|
|
325693
325934
|
name: "malloy-notebook-chat",
|
|
@@ -325721,7 +325962,7 @@ Steps to follow when the user asks a question:
|
|
|
325721
325962
|
{
|
|
325722
325963
|
name: "malloy-phrase-detection",
|
|
325723
325964
|
description: "How to phrase search_text on a get_context call so retrieval returns the fields you need instead of a truncated catalog. Covers target-type classification and decomposition patterns.",
|
|
325724
|
-
body: "# Search Target Construction for `get_context`\n\nThe `get_context` tool description defines each field and what a call returns. This skill focuses on the parts you won't get right by default: classifying concepts into target types and splitting ambiguous phrases.\n\n> **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.\n\n**Scope of this skill:** the patterns below build `dimension` / `measure` / `view` targets. Phrasing for `source` targets is covered at the end.\n\n**A note on matching:** `get_context` searches over the model (sources, fields, views, and their descriptions), not the distinct categorical *values* stored in the data. To find which literal values a categorical dimension holds, target the dimension, then query its distinct values with `execute_query` (see the patterns below).\n\n## Always send `search_text`\n\n**Do not enumerate.** Omitting `search_text` lists a catalog rather than searching it. Knowing the package narrows *where* to look; it does not substitute for saying *what* you need: if you know the package, that is a reason to scope, not a reason to skip `search_text`. Enumerated listings are capped per source and per entity type, and with no relevance signal the cap drops the fields your question is about while keeping join-path noise.\n\nA bare listing has two legitimate uses. The first is answering \"what data is here?\" when the user has named no subject at all. The second is reading a specific entity you already have the exact name of: scope to its source, set `entity_name`, and pass `search_text: null`, which returns that entity's docstring and Malloy code without spending a search. Every other call carries `search_text` on every target.\n\n## Authoring `search_text` for entity targets\n\nWrite `search_text` as a brief semantic **description** of what you're looking for, not an echo of the user's word. This applies even when you already know the entity name from a prior result: still describe it, don't just repeat the name. That is a rule about how to *phrase* a search, and it does not conflict with the exact-entity lookup above: if you want that one entity's code and docstring rather than a ranked set, pin it with `entity_name` and skip the search entirely. Sending its name back as `search_text` is the move this rule forbids, because it searches for a name instead of either describing a concept or asking for the entity.\n\nOne target per concept is enough: the tool handles phrasing variants internally. Don't pile up dimension targets that point at the same field. Use multiple targets only when they describe genuinely distinct concepts (see \"Non-obvious decomposition patterns\" below).\n\n## Target-type decision guide\n\n- **`dimension`**: categorical attribute to group, filter, or join on. Also used for time and numeric fields.\n - \"region\" becomes `\"the geographic region\"`\n- **`measure`**: aggregation metric (count, sum, average, rate).\n - \"total revenue\" becomes `\"the total revenue or sales amount\"`\n- **`view`**: pre-built analysis. Include one whenever the question sounds like a canned report (summary, breakdown, top-N, trend).\n - \"sales summary\" becomes `\"a summary of sales metrics\"`\n- **`source`**: data domain, for a question that names a subject area rather than fields (phrasing below).\n\n**Resolving categorical values (no value-search target in v1).** When the user names a literal value like \"premium\" or \"New York City\", target the *dimension* it lives on (`\"the subscription tier\"`, `\"the city where the subscriber lives\"`). Then confirm the exact stored string by querying that dimension's distinct values with `execute_query` before you filter on it. The data may store `\"Premium\"`, `\"PREMIUM\"`, `\"NYC\"`, or `\"New York\"`, and only the data tells you which.\n\n## Non-obvious decomposition patterns\n\nThese are the rules you won't apply correctly by default:\n\n1. **Adjective + noun, split.** \"active users\" becomes two dimension targets: one for the attribute (`\"the status of the user account\"`) and one for the noun (`\"the user or account holder\"`). Resolve the modifier (\"active\") to the exact stored value by querying the status dimension's distinct values with `execute_query`.\n2. **Ambiguous concept, cover both types.** \"rating\", \"duration\", and the like could be either a dimension or a measure: create one target of each type.\n3. **Time references are dimensions.** \"last year\" becomes a dimension target for the relevant date field (`\"the date the event occurred\"`).\n4. **Numeric ranges are dimensions.** \"aged 50\", \"revenue over $1M\" become dimension targets; the comparison is applied in the query, not matched as text.\n5. **Categorical strings that look numeric are still dimensions.** \"18-30\", \"<5 days\", \"tier 2\" are stored as literal strings on a dimension. Target that dimension, then confirm the exact string with `execute_query`.\n6. **\"Top N\" without a named measure, add a ranking measure.** \"top 6 products\" becomes a measure for the ranking concept (`\"the performance metric for a product\"`) plus a dimension for the entity. If the measure is explicit (\"top products by total sales\"), use it directly and skip the generic ranking measure.\n7. **Multiple values for one concept, one dimension target.** Several values (\"premium and basic\") still map to a single dimension target for the parent field; enumerate the exact stored values with `execute_query`.\n8. **The quantity asked for is a measure, even when nothing names it.** \"How many titles were released in 2019?\" is asking for a count, but no noun in it is the count: the visible phrases are the subject (\"titles\") and the filter (\"2019\"), and mapping only those yields a `source` and a `dimension` target with no way to answer. Always add a measure target for the quantity itself (`\"the number of titles\"`). This holds for every interrogative that IS the aggregation -- \"how many\", \"how much\", \"how often\" -- and it is the pattern most easily lost when a filter or grouping is the loud part of the sentence. Measured: an answerer got this right on \"How many titles are in the dataset?\" and dropped it on \"How many titles were released in 2019?\", where the year took the attention.\n9. **A population qualifier is a target, and so is the one the question omits.** Words like \"real\", \"actual\", \"genuine\", \"live\" or \"production\" are not filler: they name rows the model marks for exclusion. Target the flag itself (`\"the flag marking synthetic, test or monitoring traffic\"`), not just the noun they modify. Add one such target even when the question carries no qualifier at all, because a table of events, requests, sessions or logs usually holds test, internal or cancelled rows and nothing in the wording will say so. Resolve it to the model's own flag rather than inventing a filter on an id or a name; a hand-rolled exclusion and the documented one rarely select the same rows.\n\n## Worked example\n\n**User:** \"Customer churn in NYC over the last year for premium and basic subscribers\"\n\nThe targets for this question:\n\n| target_type | search_text |\n|---|---|\n| `measure` | `\"the rate at which customers leave the service\"` |\n| `dimension` | `\"the city where the subscriber lives\"` |\n| `dimension` | `\"the date the subscription was canceled\"` |\n| `dimension` | `\"the tier of the subscription\"` |\n| `view` | `\"subscriber churn or retention analysis\"` |\n\nKey moves: time (\"last year\") becomes a dimension on the cancellation date; \"NYC\" and \"premium/basic subscribers\" do not get their own value targets, they resolve to the city and tier dimensions. One `view` target is included to surface any canned churn analysis.\n\nThe response returns the source these fields live on (here `subscriptions`) with the matched fields on its card. Then run `execute_query` to read the city and tier dimensions' distinct values and confirm the exact strings to filter on (\"New York City\" vs \"NYC\", \"premium\" vs \"Premium\").\n\n## Authoring `search_text` for `source` targets\n\nAim for **3-8 words that name the entity and its business process**. Don't include filter values, time ranges, or aggregations: those belong in entity targets.\n\n| Too vague | Over-specific (entity-shaped) | Good |\n|---|---|---|\n| `\"orders\"` | `\"total order revenue by customer last year\"` | `\"customer order history and line items\"` |\n| `\"customer data\"` | `\"premium subscribers who churned in NYC\"` | `\"subscriber accounts and churn\"` |\n| `\"metrics\"` | `\"monthly revenue variance by account\"` | `\"sales pipeline and revenue forecasts\"` |\n\nHeuristics:\n\n1. **Translate, don't echo.** \"How did sales go last month?\" becomes `\"sales order revenue\"`, not `\"sales last month\"`.\n2. **Differentiate by data shape or business process, not by the user's industry, brand, or product category.** Prefer `\"order fulfillment and shipping\"` over `\"ecommerce data\"` when multiple commerce-ish packages exist. Do NOT add words like `\"eyewear\"`, `\"subscription box\"`, `\"Acme Corp\"`, or the user's specific vertical/brand. Source summaries describe data structure, not the customer's vertical, so those words add noise and can hurt matching.\n3. **Retry with alternative phrasings** if the right source isn't in the results before concluding it's missing."
|
|
325965
|
+
body: "# Search Target Construction for `get_context`\n\nThe `get_context` tool description defines each field and what a call returns. This skill focuses on the parts you won't get right by default: classifying concepts into target types and splitting ambiguous phrases.\n\n> **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.\n\n**Scope of this skill:** the patterns below build `dimension` / `measure` / `view` / `dimensional_value` targets. Phrasing for `source` targets is covered at the end.\n\n**A note on matching:** `get_context` searches over the model (sources, fields, views, and their descriptions), and `dimensional_value` targets search the categorical *values* stored in the data. Some servers don't index values; check the tool's description. On one that doesn't, find a literal value by targeting its dimension, then querying its distinct values with `execute_query` (see \"Where value search isn't available\" below).\n\n## Always send `search_text`\n\n**Do not enumerate.** Omitting `search_text` lists a catalog rather than searching it. Knowing the package narrows *where* to look; it does not substitute for saying *what* you need: if you know the package, that is a reason to scope, not a reason to skip `search_text`. Enumerated listings are capped per source and per entity type, and with no relevance signal the cap drops the fields your question is about while keeping join-path noise.\n\nA bare listing has two legitimate uses. The first is answering \"what data is here?\" when the user has named no subject at all. The second is reading a specific entity you already have the exact name of: scope to its source, set `entity_name`, and pass `search_text: null`, which returns that entity's docstring and Malloy code without spending a search. Every other call carries `search_text` on every target.\n\n## Authoring `search_text` for entity targets\n\nWrite `search_text` as a brief semantic **description** of what you're looking for, not an echo of the user's word. This applies even when you already know the entity name from a prior result: still describe it, don't just repeat the name. That is a rule about how to *phrase* a search, and it does not conflict with the exact-entity lookup above: if you want that one entity's code and docstring rather than a ranked set, pin it with `entity_name` and skip the search entirely. Sending its name back as `search_text` is the move this rule forbids, because it searches for a name instead of either describing a concept or asking for the entity.\n\nOne target per concept is enough: the tool handles phrasing variants internally. Don't pile up dimension targets that point at the same field. Use multiple targets only when they describe genuinely distinct concepts (see \"Non-obvious decomposition patterns\" below).\n\n## Target-type decision guide\n\n- **`dimension`**: categorical attribute to group, filter, or join on. Also used for time and numeric fields.\n - \"region\" becomes `\"the geographic region\"`\n- **`measure`**: aggregation metric (count, sum, average, rate).\n - \"total revenue\" becomes `\"the total revenue or sales amount\"`\n- **`view`**: pre-built analysis. Include one whenever the question sounds like a canned report (summary, breakdown, top-N, trend).\n - \"sales summary\" becomes `\"a summary of sales metrics\"`\n- **`dimensional_value`**: a literal value the user named, stored in some dimension. `search_text` is the value itself, the one target where echoing the user's word is right.\n - \"CyberArk\" stays `\"CyberArk\"`\n - Best practice is to scope the call to the source that holds the value. An unscoped value search covers every indexed dimension in scope, so it is slow on a large package. If you truly don't know the source, you can leave the scope off, but expect a slow call.\n- **`source`**: data domain, for a question that names a subject area rather than fields (phrasing below).\n\n**Resolving categorical values.** When the user names a literal value like \"premium\" or \"New York City\", send a `dimensional_value` target for it, scoped to its source once you know the source. Filter on the exact string it returns: the data may store `\"Premium\"`, `\"PREMIUM\"`, `\"NYC\"`, or `\"New York\"`, and only the data tells you which.\n\n**Where value search isn't available.** Some servers have no value index and return nothing for a `dimensional_value` target, and on others a particular dimension's values may not be indexed. Then target the *dimension* the value lives on (`\"the subscription tier\"`, `\"the city where the subscriber lives\"`), and confirm the exact stored string by querying that dimension's distinct values with `execute_query` before you filter on it.\n\n## Non-obvious decomposition patterns\n\nThese are the rules you won't apply correctly by default:\n\n1. **Adjective + noun, split.** \"active users\" becomes two dimension targets: one for the attribute (`\"the status of the user account\"`) and one for the noun (`\"the user or account holder\"`). Resolve the modifier (\"active\") to the exact stored value with a `dimensional_value` target, or the status dimension's distinct values where values aren't indexed.\n2. **Ambiguous concept, cover both types.** \"rating\", \"duration\", and the like could be either a dimension or a measure: create one target of each type.\n3. **Time references are dimensions.** \"last year\" becomes a dimension target for the relevant date field (`\"the date the event occurred\"`).\n4. **Numeric ranges are dimensions.** \"aged 50\", \"revenue over $1M\" become dimension targets; the comparison is applied in the query, not matched as text.\n5. **Categorical strings that look numeric are still dimensions.** \"18-30\", \"<5 days\", \"tier 2\" are stored as literal strings on a dimension. Target that dimension, then confirm the exact string with a `dimensional_value` target, or `execute_query` where values aren't indexed.\n6. **\"Top N\" without a named measure, add a ranking measure.** \"top 6 products\" becomes a measure for the ranking concept (`\"the performance metric for a product\"`) plus a dimension for the entity. If the measure is explicit (\"top products by total sales\"), use it directly and skip the generic ranking measure.\n7. **Multiple values for one concept, one dimension target.** Several values (\"premium and basic\") still map to a single dimension target for the parent field; confirm the exact stored values with `dimensional_value` targets, or `execute_query` where values aren't indexed.\n8. **The quantity asked for is a measure, even when nothing names it.** \"How many titles were released in 2019?\" is asking for a count, but no noun in it is the count: the visible phrases are the subject (\"titles\") and the filter (\"2019\"), and mapping only those yields a `source` and a `dimension` target with no way to answer. Always add a measure target for the quantity itself (`\"the number of titles\"`). This holds for every interrogative that IS the aggregation -- \"how many\", \"how much\", \"how often\" -- and it is the pattern most easily lost when a filter or grouping is the loud part of the sentence. Measured: an answerer got this right on \"How many titles are in the dataset?\" and dropped it on \"How many titles were released in 2019?\", where the year took the attention.\n9. **A population qualifier is a target, and so is the one the question omits.** Words like \"real\", \"actual\", \"genuine\", \"live\" or \"production\" are not filler: they name rows the model marks for exclusion. Target the flag itself (`\"the flag marking synthetic, test or monitoring traffic\"`), not just the noun they modify. Add one such target even when the question carries no qualifier at all, because a table of events, requests, sessions or logs usually holds test, internal or cancelled rows and nothing in the wording will say so. Resolve it to the model's own flag rather than inventing a filter on an id or a name; a hand-rolled exclusion and the documented one rarely select the same rows.\n\n## Worked example\n\n**User:** \"Customer churn in NYC over the last year for premium and basic subscribers\"\n\nThe targets for this question:\n\n| target_type | search_text |\n|---|---|\n| `measure` | `\"the rate at which customers leave the service\"` |\n| `dimension` | `\"the city where the subscriber lives\"` |\n| `dimension` | `\"the date the subscription was canceled\"` |\n| `dimension` | `\"the tier of the subscription\"` |\n| `view` | `\"subscriber churn or retention analysis\"` |\n\nKey moves: time (\"last year\") becomes a dimension on the cancellation date; \"NYC\" and \"premium/basic subscribers\" resolve to the city and tier dimensions in this first call, because the source is not known yet and an unscoped value search is slow. One `view` target is included to surface any canned churn analysis.\n\nThe response returns the source these fields live on (here `subscriptions`) with the matched fields on its card. Then send a second call scoped to `subscriptions`, with `dimensional_value` targets for `\"NYC\"`, `\"premium\"` and `\"basic\"`, to get the exact strings to filter on (\"New York City\" vs \"NYC\", \"premium\" vs \"Premium\"). Where values aren't indexed, run `execute_query` on the city and tier dimensions' distinct values instead.\n\n## Authoring `search_text` for `source` targets\n\nAim for **3-8 words that name the entity and its business process**. Don't include filter values, time ranges, or aggregations: those belong in entity targets.\n\n| Too vague | Over-specific (entity-shaped) | Good |\n|---|---|---|\n| `\"orders\"` | `\"total order revenue by customer last year\"` | `\"customer order history and line items\"` |\n| `\"customer data\"` | `\"premium subscribers who churned in NYC\"` | `\"subscriber accounts and churn\"` |\n| `\"metrics\"` | `\"monthly revenue variance by account\"` | `\"sales pipeline and revenue forecasts\"` |\n\nHeuristics:\n\n1. **Translate, don't echo.** \"How did sales go last month?\" becomes `\"sales order revenue\"`, not `\"sales last month\"`.\n2. **Differentiate by data shape or business process, not by the user's industry, brand, or product category.** Prefer `\"order fulfillment and shipping\"` over `\"ecommerce data\"` when multiple commerce-ish packages exist. Do NOT add words like `\"eyewear\"`, `\"subscription box\"`, `\"Acme Corp\"`, or the user's specific vertical/brand. Source summaries describe data structure, not the customer's vertical, so those words add noise and can hurt matching.\n3. **Retry with alternative phrasings** if the right source isn't in the results before concluding it's missing."
|
|
325725
325966
|
},
|
|
325726
325967
|
{
|
|
325727
325968
|
name: "malloy-publish",
|
|
@@ -325774,28 +326015,47 @@ If it doesn't exist, create one. Suggest a package name based on the model conte
|
|
|
325774
326015
|
|
|
325775
326016
|
### Curating discovery & the query boundary (optional)
|
|
325776
326017
|
|
|
325777
|
-
|
|
326018
|
+
A package with no \`index.malloy\` and no \`explores\` exposes **everything**: every model is listed and every source is directly queryable.
|
|
326019
|
+
|
|
326020
|
+
To curate, add an **\`index.malloy\`** at the package root. Publisher reads it as the package's published surface, so no manifest field is involved:
|
|
326021
|
+
|
|
326022
|
+
\`\`\`malloy
|
|
326023
|
+
// index.malloy
|
|
326024
|
+
import "order_analysis.malloy"
|
|
326025
|
+
import "staging.malloy"
|
|
326026
|
+
|
|
326027
|
+
export { orders, customers }
|
|
326028
|
+
\`\`\`
|
|
326029
|
+
|
|
326030
|
+
What it exports is what agents discover **and** what may be queried. Everything else still compiles, and other models can import, join and extend it, but a direct query against it is refused with a 404 - indistinguishable from a source that does not exist. Reach for this when you have raw/staging/scaffolding sources that exist to build a curated entry point and you don't want agents landing on, or querying, them directly.
|
|
325778
326031
|
|
|
325779
|
-
|
|
326032
|
+
**Address queries to the surface.** Once a package has an \`index.malloy\`, \`.../models/staging.malloy/query\` is no longer a query entry point, *even for a source that file declares itself*. Use \`.../models/index.malloy/query\`. If you are debugging a refusal rather than authoring, \`skill:malloy-source-unreachable\` covers the three ways a source can be out of reach and how to tell them apart.
|
|
326033
|
+
|
|
326034
|
+
**A surface can be layered.** An \`index.malloy\` may front a file that fronts another. A source re-exported through a chain of files stays queryable through the surface at any depth, because admission follows the declaration rather than the path taken to it.
|
|
326035
|
+
|
|
326036
|
+
**Curation hides a landing point, not a column.** A published source may \`join\` an unpublished one, and a query grouping by a joined field returns that field's values normally. If a column must not be readable, do not join it into something you publish; gate it with \`#(authorize)\` instead.
|
|
326037
|
+
|
|
326038
|
+
**Leaving a source out does not put it out of reach, but there is a condition.** \`export { ... }\` also decides what an *importing* file may see, which is Malloy's rule rather than Publisher's. A file that declares no \`export\` hands an importer everything it declares, so an unpublished source stays importable and joinable from the file that declares it. A file that *does* declare one hands over exactly that list: importing a file whose \`export\` omits a source and then naming it fails to compile with \`Reference to undefined object\`. Put an \`export\` on a mid-layer file only when you mean to narrow what its importers can build on, not just what Publisher lists.
|
|
326039
|
+
|
|
326040
|
+
**About \`export { … }\`:** the surface filters which *files* are listed; \`export { … }\` (a Malloy statement) filters which *sources within a file* are exposed, and the two compose. You usually don't write it in a leaf model: a file with **no** \`export\` exposes all of its own top-level sources. It must appear after the definitions it names. See [Malloy: Imports & Exports](https://docs.malloydata.dev/documentation/language/imports).
|
|
326041
|
+
|
|
326042
|
+
### The older manifest fields
|
|
326043
|
+
|
|
326044
|
+
Both still work and are not going away in this release. Both are deprecated where \`index.malloy\` replaces them, and a package using them that way gets a load-time warning naming the replacement. The two uses it cannot replace, an \`explores\` naming several files and \`queryableSources: "all"\`, stay supported without a warning. Do not add either key to a new package that does not need one of those two.
|
|
325780
326045
|
|
|
325781
326046
|
\`\`\`json
|
|
325782
326047
|
{
|
|
325783
326048
|
"name": "ecommerce",
|
|
325784
|
-
"version": "0.0.1",
|
|
325785
|
-
"description": "Orders, customers, and revenue analysis",
|
|
325786
326049
|
"explores": ["order_analysis.malloy", "customer_health.malloy"],
|
|
325787
|
-
"queryableSources": "
|
|
326050
|
+
"queryableSources": "all"
|
|
325788
326051
|
}
|
|
325789
326052
|
\`\`\`
|
|
325790
326053
|
|
|
325791
|
-
- **\`explores\`** (\`string[]\`) -
|
|
325792
|
-
- **\`queryableSources\`** (\`"declared"\` | \`"all"\`, default \`"declared"\`) - the query boundary.
|
|
325793
|
-
|
|
325794
|
-
**About \`export { … }\`:** \`explores\` filters which *files* are listed; \`export { … }\` (a Malloy statement) filters which *sources within a file* are exposed, the two compose. You usually don't write it: a file with **no** \`export\` exposes all of its own top-level sources. Add \`export { orders, customers }\` to a file to expose only those and keep imported/scaffolding helpers out of discovery (it must appear after the definitions it names). See [Malloy: Imports & Exports](https://docs.malloydata.dev/documentation/language/imports).
|
|
326054
|
+
- **\`explores\`** (\`string[]\`) - model file paths relative to the package root, naming the surface. Reach for it for the one thing \`index.malloy\` cannot express: a surface spanning **several** files. An explicit \`explores\` always wins over the convention, and a package with both an \`index.malloy\` and an \`explores\` that omits it carries a warning saying so. An entry that doesn't resolve to a real \`.malloy\` file surfaces in \`exploresWarnings\`; publishing a package that has any is rejected, so fix the path before publishing. An explicit \`"explores": []\` means "do not curate" and suppresses the convention.
|
|
326055
|
+
- **\`queryableSources\`** (\`"declared"\` | \`"all"\`, default \`"declared"\`) - the query boundary. \`"declared"\` is already the default, so the key changes nothing. **\`"all"\` is the exception, and \`index.malloy\` does not replace it:** it curates listings while leaving every source queryable by name, and a surface derived from an \`index.malloy\` always enforces the boundary. If you want listings-only curation, keep both keys.
|
|
325795
326056
|
|
|
325796
|
-
**Why curate here:** declaring \`explores\` routes agents to the well-documented curated sources instead of raw tables, and \`queryableSources: "declared"\` keeps them from reaching the hidden sources by name. The two axes compose: list a file in \`explores\` for its models to be discoverable, and \`export\` a source within that file for it to be a landing point.
|
|
325797
326057
|
|
|
325798
|
-
> **Not access control.**
|
|
326058
|
+
> **Not access control.** The surface gates the query surface (the query endpoints, REST and MCP alike), not compile and not raw file retrieval by exact path: \`/compile\` and \`compile_model\` are deliberately exempt, because compile is the authoring loop and the boundary is discovery curation. It doesn't restrict *who* may query, only *what* is queryable by name. Queryable sources are the union of every listed file's \`export {}\` closure, whichever listed model path a query addresses them through. To gate access by caller-supplied identity/role, use \`#(authorize)\` on the source (and \`#(access_filter)\` to scope rows), see \`skill:malloy-model\` § Access Control and \`docs/authorize.md\`. Discovery curation and these gates are independent layers.
|
|
325799
326059
|
|
|
325800
326060
|
The manifest also carries a \`scope\` field (\`"package"\` | \`"version"\`, default \`"package"\`) controlling whether persisted/materialized artifacts are shared across published versions or owned by a single version, and a \`materialization\` field configuring that persistence policy (a cron \`schedule\` or a \`freshness\` window). Both are unrelated to discovery curation; there is no per-source \`sharing\` or \`schedule\` field, that was retired in favor of the single package-level \`scope\` and \`materialization\`.
|
|
325801
326061
|
|
|
@@ -326798,6 +327058,74 @@ Then hand off to modeling: use your modeling workflow to turn the confirmed scop
|
|
|
326798
327058
|
## Done
|
|
326799
327059
|
|
|
326800
327060
|
Scope confirmed, in the conversation and in \`modeling-notes.md\`: tables in scope, analytical focus, model kind and what it rules out, and what's deferred. Continue with your modeling workflow.`
|
|
327061
|
+
},
|
|
327062
|
+
{
|
|
327063
|
+
name: "malloy-source-unreachable",
|
|
327064
|
+
description: `Work out why a source is missing from get_context, or why a query against it was refused with a 404 or a 403, and what to do next. Use when a source you expect is absent from discovery, when execute_query answers "No queryable source", when a query that used to work stops working, or when deciding whether a package's curation is hiding something you need.`,
|
|
327065
|
+
body: `# A source you cannot reach
|
|
327066
|
+
|
|
327067
|
+
> **Tool names** are written bare here - \`get_context\`, \`execute_query\`, \`get_status\`, \`compile_model\`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
|
|
327068
|
+
|
|
327069
|
+
Most packages publish a curated surface, so a source being absent or refused is usually the package working as its author intended, not a fault. Read the refusal before working around it, because the three things that can refuse you need three different responses and they are told apart by the status code.
|
|
327070
|
+
|
|
327071
|
+
## The three answers
|
|
327072
|
+
|
|
327073
|
+
| what you got | what it means | what to do |
|
|
327074
|
+
| --- | --- | --- |
|
|
327075
|
+
| **404**, "No queryable source", "No queryable model" or "Query target is not queryable" | the source is not on the package's published surface, or does not exist. The two are deliberately indistinguishable so that a refusal cannot be used to probe for hidden names | query something on the surface, or ask the package's author to publish it |
|
|
327076
|
+
| **403**, "Access denied" | the source exists and is on the surface, but an \`#(authorize)\` gate did not admit you | supply the givens the gate reads, or accept that this caller may not read it |
|
|
327077
|
+
| **200** with zero rows | you were admitted, and an \`#(access_filter)\` narrowed the rows to none of them | this is a real answer. Report it as "no matching rows", never as an error |
|
|
327078
|
+
|
|
327079
|
+
A 403 names a source, so it tells you the source exists. A 404 tells you nothing at all. That asymmetry is deliberate: a source that is both hidden AND gated answers **404**, so you can never use a 403 to discover that a hidden name is real.
|
|
327080
|
+
|
|
327081
|
+
## Addressing the surface, which is the most common 404
|
|
327082
|
+
|
|
327083
|
+
A query names a model file. When a package curates its surface, only files on that surface are valid entry points, **even for a source that file declares itself**:
|
|
327084
|
+
|
|
327085
|
+
\`\`\`
|
|
327086
|
+
POST .../packages/sales/models/index.malloy/query {"sourceName": "orders"} -> 200
|
|
327087
|
+
POST .../packages/sales/models/orders.malloy/query {"sourceName": "orders"} -> 404
|
|
327088
|
+
\`\`\`
|
|
327089
|
+
|
|
327090
|
+
Same source, same package. \`orders.malloy\` is off the surface, so it is not an entry point.
|
|
327091
|
+
|
|
327092
|
+
**Use the \`model_path\` that \`get_context\` gave you, verbatim.** Its \`source_info.resource_id\` holds \`environment\`, \`package\`, \`model_path\` and \`source\`, and those are exactly \`execute_query\`'s \`environmentName\`, \`packageName\`, \`modelPath\` and \`sourceName\`. On a curated package the \`model_path\` is the surface file, not the file that declares the source. Substituting the declaring file because it looks more correct is how this 404 happens.
|
|
327093
|
+
|
|
327094
|
+
## When the source is not in \`get_context\` at all
|
|
327095
|
+
|
|
327096
|
+
Work down this list. The first three are far more common than the last.
|
|
327097
|
+
|
|
327098
|
+
1. **It is off the surface.** The package publishes a curated set and this source is not in it. Nothing you can do from the query side; it is a package edit.
|
|
327099
|
+
2. **The package failed to load, or is serving a stale model.** Call \`get_status\`. A package that never loaded is absent from listings entirely, which reads exactly like "does not exist". A package whose last reload failed to compile is listed and answering, from the model it compiled *before* that save, and carries \`stale: true\`. Neither is visible from a listing alone.
|
|
327100
|
+
3. **You are talking to a different server than you think.** Call \`list_packages\` and check the environment and package names are the ones you expect. A stale \`.mcp.json\` outlives the server that wrote it, and another Publisher may hold that port.
|
|
327101
|
+
4. **Your search phrasing missed it.** Retry with a bare target (no \`search_text\`) to enumerate rather than rank. If it appears there, the source exists and was a ranking miss.
|
|
327102
|
+
|
|
327103
|
+
## What curation does not do
|
|
327104
|
+
|
|
327105
|
+
- **It does not hide fields.** A published source may \`join\` an unpublished one, and a query grouping by a joined field returns that field's values normally. Hiding a source removes it as a landing point; it does not redact columns a published source pulls in.
|
|
327106
|
+
- **It does not gate \`/compile\`.** \`compile_model\` is exempt, because compile is the authoring loop. A hidden source can still be compile-checked, and that is intended. The exception is a hidden source that is also gated, which answers 404 at compile too.
|
|
327107
|
+
- **It is not access control.** Curation answers "what is queryable by name", not "who may query it". Identity is \`#(authorize)\` and \`#(access_filter)\`. A source is not protected by being hidden.
|
|
327108
|
+
|
|
327109
|
+
## Reaching an unpublished source from your own model
|
|
327110
|
+
|
|
327111
|
+
If you are writing Malloy rather than just querying, an unpublished source is still usable, with one condition that trips people.
|
|
327112
|
+
|
|
327113
|
+
\`export { ... }\` decides what an **importing file** can see, which is Malloy's rule and not Publisher's:
|
|
327114
|
+
|
|
327115
|
+
- a file that declares **no** \`export\` hands an importer everything it declares, so import the file that declares the source and join it normally;
|
|
327116
|
+
- a file that **does** declare one hands over exactly that list. Importing a file whose \`export\` omits the source and then naming it fails to compile with \`Reference to undefined object\`.
|
|
327117
|
+
|
|
327118
|
+
So an unpublished source is reachable through a file that exports it, or that exports nothing, and not through a file whose \`export\` leaves it out. Re-export chains are fine at any depth: a source re-exported through several files stays queryable through the surface, because admission follows the declaration rather than the path taken to it.
|
|
327119
|
+
|
|
327120
|
+
## Before you report a source as missing
|
|
327121
|
+
|
|
327122
|
+
Confirm all three, in this order. Each one has been mistaken for "the data is not there".
|
|
327123
|
+
|
|
327124
|
+
1. \`get_status\` shows the package loaded and not \`stale\`.
|
|
327125
|
+
2. \`list_packages\` names the environment and package you meant.
|
|
327126
|
+
3. An enumerating \`get_context\` call (a target with no \`search_text\`) does not list it.
|
|
327127
|
+
|
|
327128
|
+
Only then is it genuinely not published, and the answer to the user is that the package does not expose it, not that the data does not exist.`
|
|
326801
327129
|
}
|
|
326802
327130
|
]
|
|
326803
327131
|
};
|