fix(items): adopt spec #79 — tag the platform defaults, and give the install form a vocabulary - #22
Merged
Merged
Conversation
…mezone
spec#79 (ADR 0018) made `platformDefault.type` REQUIRED with one member,
SELF_ADDRESS. n8n's three address inputs -- n8nHost, webhookUrl and
editorBaseUrl -- carried only a `source`, so the component stopped validating
against spec@main the moment that merged. This repository fetches the bundles
unpinned on every run, so the failure is real today, not on the next release.
The same change opened a presentation vocabulary the item could not use before,
and the fields it applies to are exactly the ones being touched:
`format` names what each value is. n8nHost binds PUBLIC_HOSTNAME, a host with no
port, which is now sayable as HOSTNAME; webhookUrl and editorBaseUrl are URLs
addressing a network service, which is ENDPOINT_URL rather than the residual
URI. timezone becomes TIMEZONE -- the field that motivated the proposal, and the
one entry here where the derivation does real work: component 6.4 obliges a
client to offer the IANA identifiers rather than a text box.
`ui.order` and `ui.prominence` fix an install form nobody chose. This item's
blueprint declares `parameters: {}`, so the form is derived, and 6.4's tiebreak
sorts a document declaring no order by input name as UTF-8 bytes -- which today
means editorBaseUrl, n8nHost, timezone, webhookUrl. That leads with the most
advanced field and splits the two URLs. Timezone is the only field a deploying
user has any reason to set, so it goes first, and the three custom-domain
overrides go behind a disclosure. SECONDARY is not isRequired restated: all four
inputs are optional, and three of them are also advanced.
`ui.examples` is never submitted -- 6.4 separates it from `default` on exactly
that point -- so n8nHost carries `n8n.example.com` to show the host-alone form
its label only implies.
The component moves to version 2. Per 7613630 a content change that reuses its
version resolves against the already-published record and is silently ignored at
sync. The listing and blueprint do not move: nothing here changes what the item
does, only how its form asks. Note the listing's "Webhook, editor, and host URLs
default to this deployment's own public https URL" is loosely worded now that
HOSTNAME makes the host/URL distinction explicit, but the sentence's meaning is
unchanged and correcting it would drag both documents to version 2 for prose.
Verified: `items/n8n` 25/25 against the live spec@main bundles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
…th the email spec#79 (ADR 0018) made `platformDefault.type` REQUIRED with one member, SELF_ADDRESS. publicUrl and csrfTrustedOrigins carried only a `source`, so this component stopped validating against spec@main when that merged. The bundles are fetched unpinned on every run, so the break is live rather than pending. Same change, same file, the presentation vocabulary it opened: publicUrl gets `format: ENDPOINT_URL`. It is a URL addressing a network service, which is the specific member; URI is the residual one and would have been the least informative true thing to say. csrfTrustedOrigins gets no format at all, and a comment saying why. The value is a comma-separated list, and every format member names a convention the whole scalar follows -- "https://a,https://b" is not a URL. Component 6.4 binds a client's rendering, so one told this was a single URL could normalise or trim it and quietly corrupt a two-origin list. The constraint that actually matters here -- exact origin, scheme included, no trailing slash -- has lived in a YAML comment and a description since a2bf352; ui.examples is the first place it can be stated in a form a client can use, so it carries both a single origin and a two-origin value. `ui.order` fixes an install form nobody chose. The blueprint declares `parameters: {}`, so the form is derived, and 6.4 sorts a document declaring no order by input name as UTF-8 bytes: adminEmail, adminPassword, csrfTrustedOrigins, publicUrl, secretKey. That interleaves the two custom-domain fields with the secrets and splits the pair the comment above them describes as one decision. Ordered now as the item actually reads: adminEmail first -- it is the only input in this component a person must type, with no generator, no platformDefault and no default -- then the two generated credentials, then the two custom-domain overrides behind a disclosure. SECONDARY on those two and not on the secrets. The distinction 6.4 draws is not isRequired restated: the platform has already computed the right value for publicUrl and csrfTrustedOrigins and a deploying user should leave both blank, whereas an auto-generated admin password is a value they will want to see and copy. The component moves to version 3. Per 7613630 a content change reusing its version resolves against the already-published record and is silently ignored at sync. Listing and blueprint stay at 2: the listing's claim that "the public URL, and the origins the sign-in form is accepted from, both default to this deployment's own https URL" is exactly as true as it was. Verified: `items/label-studio` 25/25 against the live spec@main bundles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
spec#79 (ADR 0018) made `platformDefault.type` REQUIRED with one member,
SELF_ADDRESS. publicOrigin carried only a `source`, so this component stopped
validating against spec@main when that merged; the bundles are fetched unpinned
on every run, so the failure is live.
publicOrigin also gains `format: ENDPOINT_URL`. It is the https origin of this
deployment's own Control UI, written into gateway.controlUi.allowedOrigins at
first boot -- a URL addressing a network service, which is the specific member.
URI is the residual one and says less.
timezone gains `format: TIMEZONE`. This is the field the whole proposal was
argued from: openclaw and n8n hand-rolled the same IANA timezone twice, both
`type: STRING` with `default: UTC` and the intent stated only in prose. Component
6.4 now obliges a client to offer the identifiers 6.3 names, so the same
declaration that was documentation becomes a picker. The vocabulary is IANA's and
revised several times a year, so nothing is restated here -- 6.3 names where it
is published, and every mainstream runtime already carries the database.
`ui.order` fixes a form nobody chose. The blueprint declares `parameters: {}`, so
the form is derived, and 6.4 sorts a document declaring no order by input name as
UTF-8 bytes: gatewayToken, publicOrigin, timezone. That puts the custom-domain
override -- the one field here that should normally be left alone -- ahead of the
only field a deploying user has a reason to set. Ordered now as gateway token,
timezone, then publicOrigin behind a disclosure.
SECONDARY on publicOrigin alone. 6.4 draws it as distinct from isRequired: all
three inputs are optional, but only publicOrigin is one the platform has already
answered correctly and the user should leave blank.
The component moves to version 2. Per 7613630 a content change reusing its
version resolves against the already-published record and is silently ignored at
sync. Listing and blueprint stay at 1 -- the listing's claim that the public
origin is written into the allowlist automatically and overridden only for a
custom domain is unchanged.
Verified: `items/openclaw` 25/25 against the live spec@main bundles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
…es beside them spec#79 (ADR 0018) added `HOSTNAME` and `TIMEZONE` to the format vocabulary and gave `ui` an order, a prominence, examples and enum labels. Nothing here was broken by it -- mlflow's two inputs validate unchanged -- but they are the two plainest cases in the corpus of a value whose shape was stated only in prose. backendStoreUri gets `format: CONNECTION_STRING`, not URI. The shipped default, `sqlite:////mlflow/mlflow.db`, addresses no network service at all -- it is a file -- so ENDPOINT_URL would be false for the value the item actually deploys. SQLAlchemy's URI grammar is the backing store's own connection form, which is that member's definition; URI is the residual member and would have been true and useless. artifactsDestination gets no format, deliberately. Its default `/mlflow/mlruns` is a bare POSIX path with no scheme, so it is not a URI; the production values are (s3://, gs://) but are not connection forms either. No member covers "path or storage URI", and 6.4 binds a client's rendering, so the honest declaration is none. The shape goes in examples instead. ui.examples rather than an enum, which is the thing worth recording. The issue this closes floats `enum` for backendStoreUri on the grounds that sqlite versus Postgres is a choice rather than free text. It is the wrong tool three times over. 6.4's derivation says a control for a non-empty enum MUST NOT offer a value outside its members, and a SQLAlchemy URI's value space is unbounded -- host, port, database, credentials, driver. It would falsify this item's own listing, which advertises upgrading the two store URIs to Postgres and S3 as deploy parameters. And the candidate members are templates, not values: a chooser offering postgresql://user:pass@host:5432/mlflow hands the user something that cannot be submitted as offered, which is the exact confusion `examples` was added to remove -- 6.4 is explicit that an example is never submitted and a default is. So the corpus still holds zero enums, and ui.enumLabels stays unused. Both stay PRIMARY. They are required, they carry working defaults, and together they are this item's entire configuration surface; hiding either behind a disclosure would contradict the listing. `ui.order` only corrects the derived form's byte order, which put the artifact store ahead of the metadata store it depends on. The component moves to version 2; per 7613630 a content change reusing its version is silently ignored at sync. Listing and blueprint stay at 2 -- naming a value's convention does not change what the item does, and taking the enum would have been the one change here that made the listing stale. Verified: `items/mlflow` 25/25, full corpus 377/377 against the live spec@main bundles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
spec#79 (ADR 0018) opened a presentation vocabulary this item could not use. Nothing here was broken by it; ollamaBaseUrl was simply one of the two plain misses the corpus review turned up -- ENDPOINT_URL has existed since v1 and http://ollama:11434 is the textbook case for it. The judgement worth recording is the prominence one, because the obvious reading is wrong. ollamaBaseUrl is optional and looks advanced, which is the shape of the six platform-default inputs that do become SECONDARY in this change. It is their opposite. Those six are advanced because the platform has already computed the right value and a deploying user should leave them blank; this one's default names a sibling `ollama` service that is NOT part of this deployment, so the shipped value cannot work. A field whose default is knowingly wrong must not sit behind a disclosure. It also has to stay directly under the switch that governs it: both the input's own description and the listing tell the user to turn the integration on and set the base URL, and a disclosure boundary between the two breaks that sentence. A comment in the file says so, so the next author does not quietly "fix" it. `ui.examples` carries the public form the default does not. The issue this closes reads the default itself as an example wearing a default's clothes, and it has a point -- 6.4 separates the two on exactly this, an example is never submitted and a default is. Removing it is a behaviour change though, not a presentation one: the env var would go unset. Left alone deliberately, and worth its own issue. `ui.order` is already what byte order gives, since the input names happen to sort that way. Declared anyway, so the ordering survives a fourth input rather than depending on a spelling accident. The component moves to version 3; per 7613630 a content change reusing its version is silently ignored at sync. Listing and blueprint stay at 1 -- and stay true precisely because ollamaBaseUrl stayed PRIMARY. Precedent for a component moving alone is acc69a8, which took this item from 1 to 2. Verified: `items/open-webui` 25/25 against the live spec@main bundles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
postgres is the only item in this catalog that cannot be deployed without typing something, and the reason is one input. postgresPassword is isRequired, is isSensitive, and carries no generator -- while label-studio mints both adminPassword and secretKey exactly the same way, and eleven of the thirteen items already require nothing typed at all. It is the single place in the corpus where a secret the platform could mint is handed to a person instead, and the asymmetry is invisible until the whole corpus is read end to end. Give it a generator. The shape is the house one, seen eleven times already: byteLength and encoding. ALPHANUMERIC rather than HEX so the value survives being pasted into a postgresql:// URL without percent-encoding, which matters more here than anywhere else in the corpus because this item's whole purpose is to be wired into something else. 32 bytes matches the other machine credentials -- litellm's master key, meilisearch's master key, qdrant's API key -- rather than the 24-byte tier the two human-facing admin passwords use. The component schema already obliged the rest: the generator branch requires isSensitive true and suppliedBy USER, both of which were set, and since spec#79 forbids a sibling platformDefault, which there is not. It also gains the description it never had -- postgres was the only component in the corpus whose inputs carried a label and nothing else -- and the three inputs gain a ui.order, since the file is open and the derived form otherwise sorts them postgresDb, postgresPassword, postgresUser by UTF-8 bytes. All three stay PRIMARY: postgresDb is optional and ordinary, which is the distinction 6.4 draws against the six advanced fields elsewhere in this branch. This drags the listing, which is why all three documents move. "You supply the database password at deploy" stops being true, so it is reworded to the phrasing the other ten listings already use, and ERR_VERSION_MISMATCH requires the listing and blueprint to agree, so the blueprint moves with it. The component moves for its own reason: per 7613630 a content change reusing its version resolves against the already-published record and is silently ignored at sync. Not verified: that the platform mints and injects this at deploy the way the other eleven generators are minted. The local suite has no capability phase, so what is checked here is that the document is valid and internally consistent, not that a live deployment comes up with a working superuser password. Verified: `items/postgres` 25/25 against the live spec@main bundles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
… requires Three synthetic components in the semantic-rule suite build a platformDefault carrying only a `source`. Since spec#79 that is not a document any conforming implementation would accept. They do not fail, and would not have. assertReports runs `diagnose`, the semantic checker alone; only the well-formed baseline goes through the fetched validator, and that baseline has no platformDefault. So this is hygiene rather than a fix -- a fixture exercising a semantic rule should not, in passing, model a shape the structural phase now rejects, or the next author reads it as the current form. Verified: 377/377, typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #21.
musher-dev/specmerged #79 (ADR 0018) at7b79284. This repository fetches the schema bundles fromspec@mainunpinned on every run, so the break is live now — the next nightly cron would have been the first red run.What was actually broken
Diffing the component bundle at
7b79284against its parent8ff7c2a, the only breaking change is thatComponentPlatformDefaultgained a REQUIREDtypewith one member,SELF_ADDRESS. Six inputs across three items carried only asource. Everything else in #79 is additive, and the two narrowings it landed are inert here — the corpus has zeroCONNECTIONinputs and nogeneratorbeside aplatformDefault.npm testonmainfails three suites with six errors, allmust have required property 'type'. On this branch: 377/377, typecheck clean.What else is in here
The same PR gives the fields it is already touching the vocabulary #79 opened, because the numbers that carried the proposal came from this corpus: of 28
USERinputs, 11 are typed by a person, and ten of those eleven reached a deploying user as an undifferentiated text box.format—TIMEZONEon n8n's and openclaw's timezones (the field that motivated the proposal, hand-rolled twice, and the one entry where §6.4's derivation does real work: a picker, not a text box).HOSTNAMEonn8nHost, matching itsPUBLIC_HOSTNAMEsource.ENDPOINT_URLon the four public URLs and onopen-webui.ollamaBaseUrl.CONNECTION_STRINGonmlflow.backendStoreUri.ui.order/ui.prominence— every blueprint here declaresparameters: {}, so every form is derived and today sorts by input name as UTF-8 bytes. n8n's form currently leads witheditorBaseUrland splits the URL pair; label-studio's interleaves the custom-domain fields with the secrets.SECONDARYgoes on the sixplatformDefaultinputs and nothing else.ui.examples— the shapes that were stated only in prose. The highest-value one islabel-studio.csrfTrustedOrigins, whose real constraint (exact origin, scheme included, no trailing slash) has lived in a YAML comment sincea2bf352and is now machine-readable.postgres.postgresPasswordgets a generator — issue §3. It was the only required, sensitive input in the corpus without one, and the sole reason postgres was not a zero-input deploy while eleven of thirteen items already are.Three judgement calls that go against the issue's own suggestions
URIanywhere. §6.3 makesURIthe residual member; every candidate here isENDPOINT_URLorCONNECTION_STRING, soURIwould have been the least informative true statement available.mlflow.backendStoreUri's shipped defaultsqlite:////mlflow/mlflow.dbaddresses no network service at all — it is a file — soENDPOINT_URLwould be false andCONNECTION_STRINGis the member's exact definition.enumonmlflow.backendStoreUri. §6.4 says a control for a non-emptyenumMUST NOT offer a value outside its members. A SQLAlchemy URI's value space is unbounded, and closing it would falsify that item's own listing, which advertises the Postgres + S3 upgrade as a deploy parameter.ui.examplesgives the discoverability without the closure — so the corpus still holds zero enums, andui.enumLabelsstays unused.open-webui.ollamaBaseUrlstays PRIMARY. It looks like the six advanced fields but is their opposite: their shipped value is already right, whereas its default names a sibling service that is not part of this deployment. A field whose default cannot work must not sit behind a disclosure, and it has to stay under the switch that governs it. A comment in the file records this so it is not "fixed" later.Two things the issue does not cover
metadata.version. Per7613630, a component content change that reuses its version resolves against the already-published record and is silently ignored at sync. Every touched component moves;ERR_VERSION_MISMATCHcompares listing↔blueprint only, so a blueprint moves only when its listing does. Only postgres's listing changes, because only §3 changes what the storefront claims.Why one PR and not three. The issue's §4 proposes one PR per item for the fix. That does not work here:
validate.ymlrunsnpm testover the whole corpus on everypull_request, and lefthook'spre-pushdoes the same. Until all six blocks carrytype, the first two of three PRs are red and cannot be pushed without--no-verify; stacking does not help, only the last is green. The README's rule has a stated reason — "so a rejection that only the platform can raise is easy to attribute" — which is about the capability phase; a spec-mandated required field carries no per-item platform risk, and one commit per item preserves the attribution anyway. The threefix(items)commits land first, so the platform'sapps/api/config/catalog.refpin can move to a tip where the corpus and the spec agree even if review on the rest runs long.Flagged, deliberately not done
open-webui.ollamaBaseUrl's defaulthttp://ollama:11434reads as an example wearing a default's clothes, as the issue says. Removing it is a behaviour change (the env var would go unset), not a presentation one. Worth its own issue.items/n8n/listing.yamlsays "Webhook, editor, and host URLs default to this deployment's own public https URL", butn8nHostbinds a bare host — whichformat: HOSTNAMEnow makes visible. Correcting the prose would drag n8n's listing and blueprint to version 2 for a sentence whose meaning did not change.PRIMARY. There is a real case forSECONDARY— a minted value is nothing the user decides — but that is a product judgement about the install form rather than a reading of the spec, and the issue does not propose it. One line per secret if wanted.Verified / not verified
Verified: full corpus 377/377 and
npm run typecheckclean, against the livespec@mainbundles the suite fetches on every run. Each item also validated individually.git diff --stat main..HEADtouches nine files and nothing else.Not verified: anything in the capability phase, which this repository has no way to run — that the platform mints and injects the new postgres password at deploy, and that the six
SELF_ADDRESSdefaults still resolve the same endpoints platform-side. Also unverified, and unchanged by this PR: thePUBLIC_URLtrailing-slash issue tracked inmusher-dev/platform#2468, which still leaves label-studio's 403 in place.🤖 Generated with Claude Code
https://claude.ai/code/session_019pvcJHCqQNnH1jRgFMYUWZ