Skip to content

feat(swiss): catalog input keys as kyaml paths, answers recorded on the release - #139

Open
huskcass wants to merge 3 commits into
modelsphere:mainfrom
huskcass:catalog-input-kyaml-keys
Open

huskcass wants to merge 3 commits into
modelsphere:mainfrom
huskcass:catalog-input-kyaml-keys

Conversation

@huskcass

@huskcass huskcass commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

What type of PR is this?

  • feature

What this PR does and why

Console side of modelsphere/modelsphere#75: catalog inputs[].key is a kyaml-style path, so an input can select a value inside a list (e.g. volumes.[name=draft-model].hostPath.path, extraArgs.[^=--speculative-draft-model-path=] with a patcher).

Area Change
Resolver lib/inputKey.ts: field, [field=value], [=value], [^=prefix], bare number. Exactly one primitive or no answer; field-only keys may be absent. patcher fills {value} on write, is stripped on read. Same rules as model-catalog hack/lib/inputs.js.
Deploy write A list key copies the variant's list into the form layer and patches the matched item. The web writes its effective answer (typed, release, site, default), since swissd only writes dotted keys. Field-only keys behave as before.
Annotation The written answers go out as additionalAnnotations["swiss.modelsphere.dev/inputs"] = [{key, value}] (modelsphere/swiss#62). Omitted if over 16384 bytes.
Upgrade Answers come from the release plan's annotation. A key the new variant no longer lists is deprecated; a listed key that does not resolve to one primitive is broken. Both are dropped and shown above the inputs. What the old answers wrote (leaf, or the copied list) is removed from the form layer and written again against the new variant. Other form fields and the edit layer are untouched. Releases without the annotation read field-only answers from the form layer as before. Rollback is unchanged.

Both deploy UIs (swiss pages and the inferences deploy sheet) are wired.

Which issue(s) this PR fixes

Refs modelsphere/modelsphere#75

How it was tested

Unit tests: resolver (one/zero/several matches per segment kind, patcher read/write, list copy without mutating the variant), annotation build/parse, upgrade recheck (deprecated/broken) and form-layer rebuild, the dropped-inputs notice.

Checks

  • npm run typecheck && npm test && npm run build in web/
  • No internal hostnames, IPs, registries or credentials

Notes for reviewers

  • swissd still loads, writes and checks inputs with dotted keys only, so a variant using list keys needs a swiss follow-up (catalog validation, and applyInputs/checkInputs skipping keys it cannot evaluate).
  • Answers from the site or default for list keys are recorded too, since the annotation is what the next upgrade removes and rewrites.

Release note

Catalog inputs can select a value inside a list (kyaml-style keys with an optional patcher); answers are recorded on the release and rechecked on upgrade.

…he release

Catalog inputs[].key can now select a value inside a list
(modelsphere/modelsphere#75):

- One resolver (lib/inputKey.ts) for field, [field=value], [=value],
  [^=prefix] and bare-number segments. A key answers only when it reaches
  exactly one string, number or boolean; field-only keys may be absent.
- An optional patcher fills {value} on write and is stripped on read.
- A list key copies the variant's list into the form layer and patches
  only the matched item. The web writes its effective answer (typed,
  release, site, default), since swissd only writes dotted keys.
  Field-only keys keep today's behaviour.
- The written answers go to swissd as additionalAnnotations
  swiss.modelsphere.dev/inputs ([{key, keyType, value}], modelsphere/swiss#62).
- An upgrade reads the answers from that annotation, drops keys the new
  variant no longer lists (deprecated) or cannot resolve (broken) and shows
  them, removes what the old answers wrote from the form layer and writes
  them again against the new variant. Releases without the annotation
  read field-only answers from the form layer as before.

Refs modelsphere/modelsphere#75

Co-authored-by: aceforeverd <11715200+aceforeverd@users.noreply.github.com>

@huskcass huskcass left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers.

The key rules match model-catalog#108 (findAt is the same walk), list keys copy the variant's list and patch one item, and upgrade reads the annotation, drops deprecated/broken answers and rewrites. Rollback and releases without the annotation behave as before. CI green.

  • Medium: list-key answers that came from the site or the input default are recorded, and on the next upgrade they come back as "release" answers that win over the site. A later site-profile change then never reaches a list key, while field keys keep following the profile. Record only typed answers, and on upgrade drop the list copies for every list key the old variant had.
  • Medium: inputAnnotations silently returns nothing when the JSON is over 16384 bytes. On an upgrade swiss then keeps the old record, which no longer matches the rewritten form layer. A few inputs never get near 16 KiB. Drop the check and let swissd's 400 surface.
  • Low: the annotation carries keyType: "kyaml" and parseInputRecords skips any other keyType. The swiss API convention is {key, value}; drop keyType, INPUT_KEY_TYPE and that filter.
  • Low: guards for cases the catalog CI already rules out: the patcher parts.length !== 2 branches, the parent == null || at == null check in writeAt, and most of the per-item shape checks in parseInputRecords.
  • Low: releaseInputState runs three times per render (in inputRows, for droppedInputs, in planRequest), and the dropped-inputs notice is written twice. Compute it once per page and pass it down.

Verdict: good to merge after the first Medium; the rest are optional cuts.

— Review Golden

swiss treats swiss.modelsphere.dev/inputs as opaque, so drop keyType from
written records and stop filtering on it when reading.

Co-authored-by: aceforeverd <11715200+aceforeverd@users.noreply.github.com>

@huskcass huskcass left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers.

The annotation is now {key, value} only: INPUT_KEY_TYPE and every keyType reference are gone, reads no longer filter on it (a record that still has keyType parses fine), and the tests were updated to match. Nothing else changed. CI green (publish pending).

  • Low: records.map(({ key, value }) => ({ key, value })) in inputAnnotations is now a no-op copy; JSON.stringify(records) is enough.
  • The other points from the previous review remain open.

Verdict: good to merge once the open points are settled.

— Review Golden

swissd no longer fills site or default input answers, so the web writes
the effective answer (typed, release, site, default) for every key,
dotted field keys included. The inputs annotation records only typed
answers and answers carried from the release, so later site changes
still apply on upgrade. An oversized annotation is sent as is and
swissd rejects it.

Co-authored-by: aceforeverd <11715200+aceforeverd@users.noreply.github.com>

@huskcass huskcass left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blockers:

  • The commit adds 55 files under the repo-root node_modules/ (.vite/vitest and .vitest-cache, about 14.8k lines). .gitignore only covers /web/node_modules/. Remove them, and ignore /node_modules/ too.

Every key now gets typed, then release, then site, then default, written into the form layer, which matches #75 §3. Only typed and release answers are recorded, inputAnnotations is just JSON.stringify(records), and an oversized value goes to swissd for its 400. That settles both earlier Mediums and the no-op copy.

  • Medium: a deploy with no typed answers sends no annotation (inputAnnotations([], { replace: undefined }) is undefined), so the release has none. On the next upgrade releaseInputState takes the no-annotation branch and reads storageKVOffload.mooncake.master back from the form layer as a release answer. The old site value then wins over the current site and gets recorded. #75 §4 gives [] for "no user answers" and keeps the form-layer fallback for older releases only. Send the annotation ([] when empty) on every deploy and upgrade of a variant with inputs.
  • Low: a site or default answer is not recorded, so dropInputWrites never removes its write. If the new variant drops the input, or the site answer goes away with no default, the old leaf or list copy stays in the form layer and shadows the variant's list. Also drop writes for the new variant's own input keys before rebuilding.
  • Low: comments still describe swissd answering and checking inputs (inputs.ts:20, :34, :68, :81, :270). After swiss#63 the web is the only check, so the catch that leaves a bad pattern "to swissd" now lets anything through. The PR description is also stale: "Omitted if over 16384 bytes", "swissd only writes dotted keys", and site/default answers being recorded.
  • Earlier Lows still open: the guards catalog CI already covers (parts.length !== 2, parent == null || at == null in writeAt, per-item checks in parseInputRecords), and releaseInputState running three times per render.

Verdict: good to merge with swiss#63 once the cache files are gone and the empty annotation is sent.

— Review Golden

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.

1 participant