Skip to content

feat(server): default a package's discovery surface to its index.malloy - #988

Closed
mlennie wants to merge 12 commits into
mainfrom
monty/index-malloy-discovery
Closed

mlennie wants to merge 12 commits into
mainfrom
monty/index-malloy-discovery

Conversation

@mlennie

@mlennie mlennie commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What this does

A package whose root holds an index.malloy now takes its discovery surface from that file, with no explores key in publisher.json. Listings return that file and whatever it exports; the other models become building blocks. An explicit explores still wins, and when a package has both and they disagree, the explicit key is used and the package carries a load-time warning rather than the server guessing. create-malloy-package now scaffolds an index.malloy, so a new package is curated from its first boot.

This is the convention described under Open questions in docs/malloyyo-dashboards-design.md, and it matches what Malloyyo does: its project entry point is the same hardcoded filename, and export { ... } inside it selects what consumers may query.

Where this departs from the design note, and why

The note also defaults queryableSources to "declared". I did not do that, and the reason is that it would not have been additive.

queryableSources is already "declared" by default. The query boundary is inert today only because explores is undeclared, and Package.exploresDeclared() was the single predicate driving both the listing curation and the boundary. So defaulting explores on its own switches the boundary on. That boundary denies with a 404 that is deliberately indistinguishable from "does not exist" (errors.ts, to avoid an enumeration oracle). A package that merely happened to contain a file with that name would have lost query access to every other source by name, with no config edit and no error the author could act on. Malloyyo can be absolute here because every project is greenfield and --develop is its escape hatch; Publisher has packages in the wild and no mode split.

So the convention curates discovery only. exploresDeclared() is split in two: the listing predicate counts a surface from either source, and a new boundaryDeclared() counts only an explores the author wrote. The rule stays teachable: writing an index.malloy curates your listings, and declaring explores is what gates access. Adding an index.malloy can hide a source from listings; it can never make one unreachable.

This is not a preference. Reverting that split fails four tests in query_boundary.spec.ts, and two of them are pre-existing backward-compatibility tests, one of which is the injection test. That is the evidence the split was required rather than chosen.

Happy to change this if you would rather match the note exactly, but it should be a deliberate call, because the failure mode is silent.

Deprecation is accept-with-warning, not removal

Removing explores / queryableSources would be a breaking REST and config change across api-doc.yaml and its four generators, for no gain over defaulting. Instead the keys keep working and earn a warning where the convention now covers them, following the precedent resolvePackageScope already sets in the same file for the deprecated root-level scope: a pure resolver returning { value, warnings }, riding the existing manifestWarnings channel.

The warning fires only when a package has an index.malloy and declares the legacy keys. A blanket warning on every explores user would fire on our own examples/governed-analytics, which curates a two-file surface the convention cannot express. That is a warning with no available fix, which is how deprecation warnings get ignored. Three cases, mutually exclusive: the explicit set omits the index file (disagreement), the explicit set names it (now redundant, safe to delete), or the convention is driving and a declared queryableSources is therefore inert and says so.

Implementation notes worth a reviewer's eye

  • The origin flag is a constructor parameter, not a post-construction assignment. Package's constructor runs both policy pushes, so assigning it afterwards the way manifestWarnings is assigned would compute the boundary as if the surface were explicit, for exactly the packages this protects.
  • It lives on the Package, not in packageMetadata, because setPackageMetadata replaces that object wholesale. A name-only PATCH would otherwise promote a convention-derived surface into a declared one and start returning 404s. The PATCH path preserves it, clears it when the body carries its own explores, and restores it on the reject path.
  • File listing now runs before the manifest read in the load worker, since the convention is a fact about the tree. Both still sit in the setup region excluded from the compile timing.
  • Only a root-level index.malloy counts. A nested reports/index.malloy is an ordinary model; the convention only helps if there is one place to look.
  • An explicit explores: [] counts as explicit and does not trigger the convention. It reads as a deliberate "do not curate", and today it means everything is listed.
  • The design note's "publisher.json is absent" branch is unreachable in Publisher: the manifest is mandatory, so only the "has no explores" half applies.

Verification

Gate: typecheck clean, unit 2312/0, integration 250/0. prettier:check reports the six standing failures, all confirmed git-ignored generated files that CI never sees. Regenerating skills_bundle.json was needed because the skill text changed.

The Malloy re-export syntax in the scaffolded template was compiled through the real worker rather than assumed.

Verified live against a freshly built bundle, not just tests. A package with an index.malloy and no explores lists only that file and its export closure, and a source hidden by it still answers 200 when queried by name. The same package with an explicit explores that omits the index file lists the declared set, 404s the hidden source, and carries the disagreement warning. A package scaffolded by create-malloy-package and served on a running server lists index.malloy, exposes its one exported source, and queries successfully.

Coordination

This touches no listing path. It edits applyQueryBoundaryToModels and the two predicates only, so discoverDashboards and the other exploreSet() call sites are untouched and the slice 6 work on dashboard curation is unaffected. Worth knowing there: exploreSet() can now return a convention-derived set, so gating dashboards on it will gate them on an index.malloy surface too, which is almost certainly what you want.

@mlennie
mlennie force-pushed the monty/index-malloy-discovery branch 2 times, most recently from 8c55ef5 to 43aef36 Compare August 13, 2026 18:53
mlennie added 12 commits August 13, 2026 13:25
A package whose root holds an index.malloy now gets its discovery surface
from that file, with no publisher.json "explores" key. An explicit explores
still wins, and when a package has both and they disagree the explicit key
is used and the package carries a load-time warning rather than the server
guessing. The scaffolder emits an index.malloy so new packages are curated
from their first boot.

The convention curates discovery only. It deliberately does not enable the
query boundary, which is where this departs from the design note in
docs/malloyyo-dashboards-design.md. That note also defaults queryableSources
to "declared", but "declared" is already the default and the boundary is
inert today only because explores is undeclared, so defaulting explores by
itself would have switched it on. The boundary denies with a 404 that cannot
be told apart from "does not exist", so a package that merely happened to
contain a file with that name would have lost query access to every other
source, with no config edit and no actionable error. Declaring explores stays
the explicit opt-in; adding an index.malloy can hide a source from listings
but never makes one unreachable.

Two pre-existing backward-compatibility tests in query_boundary.spec.ts fail
without that split, which is the evidence it was needed rather than a
preference.

Package.exploresDeclared is therefore split in two: the listing predicate
counts either source, the boundary predicate counts only an author-written
explores. The origin flag lives on the Package rather than in packageMetadata,
because setPackageMetadata replaces that object wholesale and a name-only
PATCH would otherwise promote a convention-derived surface into a declared
one and start returning 404s.

Verified live on a built bundle: a package with an index.malloy and no
explores lists only that file and its exports, and a source hidden by it
still answers 200 by name; the same package with an explicit explores that
omits the index file lists the declared set, 404s the hidden source, and
carries the disagreement warning.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
Nine findings from an adversarial review pass. The two that would have
reached a user:

Scaffolding a package named `index` destroyed its own starter model. The
model file is `<source>.malloy`, so for that name it IS index.malloy, and
the surface template was written over it. The result imported itself and
exported a source that no longer existed, so the user's first run produced
a package that could not compile. The surface write is now skipped when the
model already is the index, compared case-insensitively so macOS and Windows
(which would have clobbered it for `Index` too) behave like Linux rather than
only the platforms that would have corrupted it.

A GET-then-PATCH round trip armed the query boundary. `exploresFromConvention`
is deliberately not on the wire, so a client that reads a package, edits its
description and sends the whole object back re-sent the convention's own
`explores` having declared nothing, which flipped the surface to declared and
started 404ing every other source. A body that echoes exactly the current
surface now leaves the origin alone; only a body naming a different surface is
a declaration. The rule is a pure exported helper so it is testable and sits
beside the other explores rules.

The rest:

- A non-array `explores` (the usual typo of the array form) fell through to
  the convention silently. It now warns; models no longer vanish unexplained.
- The redundancy warning told authors of a multi-entry `explores` that they
  could delete the key for the same surface. That would have dropped every
  other entry. It now fires only for a surface of exactly index.malloy.
- The inert-`queryableSources` warning gave `"all"` a remedy that would not
  have produced a boundary. Each value now gets advice that works.
- Those warnings survived the PATCH that made them false. setPackageMetadata
  drops them when the origin changes.
- setPackageMetadata's origin argument was optional and defaulted to
  preserving, so a future caller declaring an explicit surface could have left
  the boundary off. Now required; that immediately caught two call sites.
- resolveExplores claimed every package in the wild keeps today's behavior.
  Untrue for one already carrying a root index.malloy, which is the intended
  feature but not what the comment said. Corrected, and docs gained an
  upgrade note.
- One em-dash, which the house rule forbids.

Declined: a PATCH omitting `description` deletes it. Real, on an unchanged
line, and pre-existing, so it belongs in its own change rather than widening
this one.

Both scaffolder e2e tests that asserted `sales.malloy` appears in listings now
assert the surface instead, and a new one proves `export { sales }` carries the
views through, so a scaffolded package is not empty in the UI. The index-name
regression test was mutation-tested.

Gate: typecheck 0, unit 2323/0, integration 250/0, scaffolder 385/0 including
the e2e against a rebuilt bundle.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
Pass 3: an adversarial regression lens on pass 2's own fixes, plus a second
security lens. Both independently found the same blocker, which a live run
had already turned up: pass 2's round-trip fix was in-memory only.

`resolvePatchedExploresOrigin` kept the origin as convention, but the same
request still persisted the resolved `explores` to publisher.json. So the
boundary stayed off in the running process and armed at the next load. Proven
live: the identical query returned 200, then 404 after a reload, with no
configuration change in between. That is worse than the bug pass 2 set out to
fix, because the arming is decoupled from the request that caused it, and
under `--watch-env` the manifest write itself triggers the reload seconds
later. A convention-derived surface is now simply never written.

The remaining five, all found by the review lenses:

- Declaring `explores` equal to the convention surface via the API is
  indistinguishable from a client echoing back what it read. It is still
  treated as the echo, because guessing the other way revokes query access,
  but the package now says so instead of silently ignoring the declaration
  and leaving the author believing queries are gated. It also names the route
  that does work.
- A non-string entry in the `explores` array was coerced with `String(e)`,
  which was scope creep in pass 2 and strictly worse than what it replaced: a
  surface naming "null" matches no model, so curation switched on over an
  empty set and the package listed nothing, with no warning. A bad element is
  now treated like a malformed key, which leaves the package uncurated.
- A rejected PATCH permanently dropped the convention warnings, because
  setPackageMetadata's filter is destructive and runs again on the way back.
  The warnings are snapshotted and restored with the metadata.
- Scaffolding `Index` produced only `Index.malloy`, which the server's exact
  match never treats as the convention, so the package was uncurated while
  the generated AGENTS.md told the agent that file was its published surface.
  On Linux that was a regression. A package named `index` in any casing now
  gets the canonical lowercase filename and the model simply is the surface.
- The upgrade note in docs/discovery-and-access.md recommended declaring
  `explores` to "keep the old behavior". That arms the query boundary, so it
  is a strictly bigger change than the one it was helping the reader avoid.
  Renaming the file is the only remedy that restores the previous behavior.

Also: one new e2e test from pass 2 asserted a GET by exact path, which the
boundary never gates, so it passed either way. It runs a query now.

The two fixes nothing pinned (the warning matcher, and the drop on origin
change) have tests. Every behavioral fix here was mutation-tested.

Gate: typecheck 0, unit 2326/0, integration 254/0, scaffolder 385/0, prettier
clean apart from the six standing git-ignored generated files. The new
integration spec drives a real server, because the blocker lived in the seam
between the in-memory flag, the manifest write, and the next load, which no
unit test sees together.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
Pass 4: two fresh lenses over the finished change, one reading the code in its
final state and one reading the text as instructions a user or agent will act
on. Between them they found NO defects in the behaviour. Every finding was a
place where the words were wrong about it, which is the class this feature has
now produced in four consecutive rounds.

The through-line: a surface from `index.malloy` and a declared `explores` are
not interchangeable. The first curates listings; the second also enforces the
404 query boundary, because `queryableSources` defaults to `"declared"`. Six
separate pieces of text glossed over that, and each one pointed a reader at a
change to their query surface they had not asked for.

- `api-doc.yaml` still described `explores` as declaration-only, with "when
  absent or empty, every model is listed". A client reading the spec would
  conclude a response carrying `explores` means the author declared it and the
  boundary is in force. Both halves are now stated: the value can be derived,
  and the boundary needs a declared one. Same stale premise fixed on
  `queryableSources`.
- The docs bullet said models outside the surface "stay queryable" for BOTH
  mechanisms. True of the convention, false of `explores`.
- The redundancy warning's escape clause was "keep it if you also rely on
  queryableSources". The boundary is on by default, so the author who never
  wrote that key is precisely the one who would delete `explores` and silently
  reopen every hidden source. Now keyed on the boundary itself, and it says
  what deleting costs. The disagreement and malformed warnings gained the same
  note, all three worded per `queryableSources` value so none of them claims a
  boundary cost where there is no boundary.
- The upgrade note recommended renaming the file as the only way back. There is
  a one-key way: `"explores": []` is read as a deliberate "do not curate" and
  suppresses the convention exactly. Verified live. Renaming also changes the
  model's identity, so `…/models/index.malloy` 404s and siblings importing it
  stop compiling, which the note now says.
- The note also missed the likeliest pre-existing shape. An `index.malloy` that
  is all imports and no `export { … }` exports nothing, so the package now
  serves one model with zero sources and looks empty. Verified live. Called out
  with both fixes.
- Three code comments asserted curation requires a manifest key, two of them in
  `model.ts`, which is not otherwise in this branch and so had never been
  re-read against the feature that invalidated them.

Also: the docs said the disagreement "logs a warning at load". It does not; the
warning rides the package's `warnings` on the REST response and nothing logs it,
so an operator sent to the logs would find nothing.

`docs/malloyyo-dashboards-design.md` still proposes defaulting
`queryableSources` too. It is a design note under Open questions, but it is the
only other in-repo description of the convention, so it now carries a short
note on what shipped and why it differs.

Gate: typecheck 0, unit 2329/0, integration 254/0, scaffolder 385/0,
openapi-spec-validator OK, prettier clean bar the six standing git-ignored
generated files.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…request

Pass 5, three findings, all real.

The security one is the pass-3 mitigation defeating itself. The "your
declaration was ignored, the boundary is NOT enforced" warning was stored in
`manifestWarnings`, which `reloadAllModels` replaces wholesale from a fresh
disk parse. Any PATCH carrying `manifestLocation` triggers that reload, and
every GET emits `manifestLocation: null`, so a client that round-trips the
whole object always sends it. The warning was therefore destroyed before the
response was built, for exactly the client the echo rule exists to serve: they
got a 200, a body showing `explores` with `queryableSources` of `declared`,
no warnings, and nothing gated. Reproduced live before the fix.

Warnings raised after load now live in their own array that a reload does not
touch, and both are reported. The same mechanism fixes the second half: a PATCH
carrying only `queryableSources` at a convention package used to return 200 with
no warning at all, because that warning is derived when the manifest is parsed
and the next parse had not happened yet. The PATCH response is when the operator
is looking, so it is said there too.

Third: `exploresPatchIgnoredUnderConvention` told an author who wants the
boundary to declare `explores` in publisher.json, which produces nothing if the
package also has `queryableSources: "all"`. It was the one warning of the five
not branching on that value, and its subject is an author actively trying to arm
the boundary, so a wrong remedy there fails in the direction this whole feature
guards. Now branched, like the others.

Also, a docs correction that understated a cost by a whole package: renaming
`index.malloy` was described as making a sibling that imports it "stop
compiling". A compile failure aborts the entire package load, so the package
disappears from `GET .../packages` and the only trace is `loadErrors` in
`/api/v0/status`. Verified live.

Gate: typecheck 0, unit 2330/0, integration 256/0, scaffolder 385/0. The
postLoadWarnings fix is mutation-tested: reverting it fails three integration
tests.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…face

The inverse of the defect the previous commit fixed, and just as misleading.
`reloadAllModels` sets the origin flag directly rather than through
`setPackageMetadata`, so it never cleared `postLoadWarnings`. An author who
reads "the query boundary is NOT enforced", does exactly what that warning
tells them to do (declare `explores` in publisher.json) and reloads in place
ended up with the boundary genuinely ON and a warning still insisting it was
off.

Found while mutation-testing the previous commit, and the mutation is the
reason this is worth recording. My first test for it used `?reload=true`,
which runs `Package.create` and returns a fresh object whose post-load
warnings are empty regardless, so it passed with the fix reverted: a test that
could not fail, pinning nothing. The reachable path is an IN-PLACE reload
(a manifest bind, or a PATCH carrying `manifestLocation`), which reloads the
same Package. Rewritten against that path, it now fails without the fix.

Gate: typecheck 0, unit 2330/0, integration 257/0, scaffolder 385/0.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…rsede them

Two findings from a confirmation sweep over the previous commit. Both are the
defect class that commit set out to fix, reached through paths it did not
cover: the only signal an operator gets about an unenforced boundary was
either deleted or contradicted.

A rejected PATCH deleted the warning permanently. `setPackageMetadata` runs
before validation and its filter is destructive, so a body that flips the
origin stripped `postLoadWarnings` on the way in; the rollback then restored
only `manifestWarnings`, because that is all the snapshot held. The package
was left serving a curated surface with nothing saying its boundary was off.
Both arrays are now snapshotted and restored together.

Successive PATCHes setting different `queryableSources` values stacked one
remedy per value, so a package could carry two contradictory instructions, one
of them wrong for the value now in force. That warning now supersedes its own
earlier instance rather than accumulating.

The test for the first of these is worth a note, because it passed with the
fix reverted at first. The rollback hands a previously-computed metadata object
back to `setPackageMetadata`, which fossilizes a copy of the old warnings
inside it, and `getPackageMetadata` only overwrites that copy when the live
list is non-empty. So the deletion is invisible until something else
repopulates the list. The test now forces that before asserting, and fails
without the fix.

Gate: typecheck 0, unit 2330/0, integration 259/0. The scaffolder suite is
383/0 with 2 blocked: its e2e refuses to run while anything else is listening
on port 4000, and another worktree's dev server holds it. Both are the
`documented URLs` tests, unrelated to this change, and the suite was 385/0
earlier today before that server started.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…Index case

Two findings from pass 8, and both are the same class: a fix that reached one
of two siblings.

The previous commit taught one post-load warning to supersede its own earlier
instance and left the other accumulating. `exploresPatchIgnoredUnderConvention`
returns two strings differing only in their remedy, so exact de-dupe kept both,
and an operator who PATCHed `queryableSources: "declared"` and then `"all"` was
left holding a stale instruction saying that declaring the surface "does
enforce it" when under "all" it does not. Reproduced live by the reviewer, end
to end, including the operator following the stale remedy and still getting an
unenforced boundary. Both warnings now supersede by prefix.

The scaffolder's index-name normalization was pinned by nothing. The existing
test scaffolds `index`, where `${sourceName}.malloy` is already "index.malloy",
so removing the normalization is a no-op and the test passes either way. Only a
capitalized name shows it: without the fix, `Index` puts the model in
Index.malloy and the surface template in index.malloy, which is one path on a
case-insensitive filesystem (one destroys the other) and on a case-sensitive
one leaves a package the server never curates, because its convention test is
an exact match. Now tested, and the test fails without the fix.

Both fixes mutation-tested.

Gate: typecheck 0, unit 2330/0, integration 259/0, scaffolder 386/0. The two
scaffolder e2e cases blocked in the previous commit pass again now that the
other worktree's dev server has released port 4000, confirming that failure was
environmental.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
The brief that specified this feature asked for a log at load naming the file
the convention picked, so a discovery surface that nothing in publisher.json
mentions is never invisible to an operator working out why a model stopped
being listed. Nothing asserted it existed: neutering the whole log call broke
no test, so it could have been deleted or silently stopped firing.

Found by mutation, using the spyOn(logger, ...) pattern package_worker_path
already establishes. It asserts the file it picked AND queryBoundaryEnforced:
false, which is the operator's real next question and the one thing a reader of
that line most needs.

Gate: typecheck 0, unit 2331/0, integration 259/0, scaffolder 386/0.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…ries

The completeness question nobody had asked: this changes served behaviour for
existing packages and had no RELEASE_NOTES entry. That file reserves
`## [Unreleased]` sections for exactly this ("breaking changes, migration
steps"), and the release process copies every one of them into the GitHub
release, so a change with a migration and no section ships silently.

The entry leads with the behaviour change rather than the feature, states the
deliberate departure (the convention curates discovery and never enables the
query boundary, because that denial is a 404 nobody could act on), warns that
`explores` may now be a value the server derived rather than one the author
wrote, and gives the migration: `"explores": []` is the one-key way to keep
the old behaviour, renaming is worse than it sounds, and an all-imports
`index.malloy` serves an empty-looking package.

Two one-line index fixes found in the same sweep: docs/README.md's row for
discovery-and-access.md did not mention the convention, so someone scanning the
index would not find it; and skills/README.md, which is the authority on what
the shared skills may discuss, listed `explores`/`queryableSources` as fair
game but not the convention that malloy-publish/SKILL.md now documents.

Also checked and clean, recorded so it is not redone: no package in examples/
or tests/fixtures has a root index.malloy except the one this branch added, so
the in-repo blast radius is still zero; nothing in the SDK, app or CLI reads
`explores`; and the remaining untouched mentions (api-overview, security-posture,
the lookml-review references, governed-analytics) are all still true as written.

Gate: typecheck 0, unit 2331/0, integration 259/0.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…listing consumers

Two nits from a post-rebase interaction review. Neither is caused by the
rebase; both are mine.

model.ts hasEmptyDiscoverySurface still said "no curation (no `explores`)".
That is the same false claim this branch already corrected twice in the same
file, sixty lines above one of them, and my sweep missed it. Curation now means
a surface from either source, so the warning DOES fire for a convention-derived
one, which is exactly the aggregator shape the release note warns about. A
reader reasoning from the old comment would conclude the opposite.

The migration text reassured that nothing becomes unreachable because the
convention does not gate queries. True, and scoped to query access only. A
catalog, a chat surface, MCP getContext or a search indexer reads the listing
rather than querying by name, so it does narrow, and a source outside the
surface drops out even carrying an #(index) tag. Worth saying plainly one day
after #990 shipped a header for the adjacent problem, because that header does
not help here: it lifts an identity gate and this is the discovery axis.

Declined, and it belongs to its author rather than to this PR: #990 added
docs/authorize-bypass-deployment.md without a row in the docs index, so an
operator page whose premise is that operators do not know the feature exists is
itself unlisted. Flagged for Nathan.

Also settled by that review, recorded because it was a real open question: this
change does NOT create a sibling of the bug #990 fixed. That one was cross-axis,
an identity control silently defeating a discovery opt-in, which is why an
identity-axis escape hatch was the right fix. This is same-axis: a per-field
tag against a per-package published surface, where the package-level statement
is the more general one, and inverting it would let a field annotation defeat
curation. Access is untouched either way, pinned by query_boundary.spec.ts:623.

Gate: typecheck 0, unit 2365/0, integration 259/0.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
An operator review found the change created a question it gave no way to
answer, and that the mitigation I documented does not work for the operator it
addresses.

Post-upgrade there was no way to inventory which packages are curated by
convention. On the wire a derived surface is indistinguishable from a declared
one, so the only signal was a load-time log line, and a replica up past its log
retention could only regenerate the answer by reloading every package and
reading stderr. `exploresFromConvention` is now on the package response,
read-only and written from the private field last so an inbound value cannot
survive. Before upgrading the inventory is still possible without it, and the
release note now carries that recipe, because the window closes on upgrade.

The mitigation was worse. "Add `"explores": []`" is true but has to land in
the package's own publisher.json: a PATCH writes into the server's
publisher_data/ copy, so --init, a fresh server root or a replica that
re-copies the package all revert it, and "frozenConfig": true refuses the call
with 403. For a fleet of packages you do not own there is no operator-side fix
at all, and the note said the opposite. It now says so plainly, and warns that
a PATCH omitting description deletes it, which matters once bulk-PATCHing is
the advertised route.

Three more from the same review: rolling back is clean and the note now says so
(the server never writes a derived explores and never persists the flag);
during a staged rollout a body read from a new replica and PATCHed to an old
one persists the surface as declared and arms the boundary fleet-wide, because
the old code has no echo detection; and the listing narrowing reaches anything
reading listings rather than querying by name, which the previous commit
covered.

Both new tests mutation-tested: removing the field fails both.

Gate: typecheck 0, unit 2365/0, integration 261/0, scaffolder 386/0,
openapi-spec-validator OK.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
@mlennie
mlennie force-pushed the monty/index-malloy-discovery branch from 43aef36 to 032d691 Compare August 13, 2026 19:29
@Sha-Bang

Copy link
Copy Markdown
Collaborator

Closing this as stale rather than rejected — the idea is sound and the design note behind it stands.

The blocker is drift. 23 of this PR's 30 files have been rewritten on main since it was opened on 2026-08-13, including model.ts, package.ts, package_manifest.ts, api-doc.yaml, and all three files of the package_load worker. Rebasing that isn't a rebase; it's a reimplementation against a codebase that moved underneath it, and no one has reviewed the original to inherit a verdict from.

The core of it is also a live design decision rather than a mechanical one. Splitting exploresDeclared so the convention curates discovery without switching on the query boundary is the right instinct — and the reasoning for it in the description, about packages in the wild and the 404 that's deliberately indistinguishable from "does not exist", is the most valuable thing here. exploresDeclared is still a single undivided predicate on main today, so that decision is genuinely open and should be made deliberately, not inherited through a conflict resolution.

The convention is still described under Open questions in docs/malloyyo-dashboards-design.md, so nothing is lost by closing this. Anyone picking it up should start from that note and current main.

@jswir

jswir commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Revived as #1206, reimplemented against current main rather than rebased: zero commits from this branch are in it, and the base is one day off main instead of 147 commits behind.

It also resolves the open decision this PR's closing note called out, in the opposite direction. The convention arms the query boundary rather than curating listings only, because the population that split was protecting turns out to be empty (checked across malloy-samples, the bundled examples, and the Malloyyo example repos). That reversal deletes most of the machinery here: no exploresFromConvention, no boundaryDeclared, no post-load warning channel, no PATCH origin rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants