diff --git a/schema/00_common.cue b/schema/00_common.cue index a2f51ff..8ada511 100644 --- a/schema/00_common.cue +++ b/schema/00_common.cue @@ -12,7 +12,7 @@ package schema // and therefore the generated Go — byte-identical to the pre-package one. // Execution context for a plan step. -#Context: ("build" | "deploy" | "runtime") @go(-) +#Context: or(_contextWord) @go(-) // #OpVerb — the verb DISCRIMINATOR vocabulary: the subset of #Op fields that are // VERBS (exactly-one-set discriminators), as opposed to the shared modifiers @@ -24,8 +24,7 @@ package schema // field (the schemagen gate "OpVerbs ⊆ AuthoringVerbs" proves it) and MUST have a // VerbCatalog entry (the registry bijection gate proves it). Keep in lockstep with // the `--- verb discriminators ---` group in #Op. -#OpVerb: ("mkdir" | "copy" | "write" | "link" | "download" | "setcap" | "build" | "config" | - "plugin") @go(-) +#OpVerb: or(_opVerbWord) @go(-) // --------------------------------------------------------------------------- // Plan steps: the unified run/check/agent-run/agent-check/include vocabulary. @@ -236,7 +235,7 @@ package schema // the BuildKit sharing mode; owned renders a uid/gid-owned cache. #CacheMount: { dst: string & =~"^/" - sharing?: *"locked" | "shared" | "private" + sharing?: *"locked" | or(_sharingWord) owned?: bool } @@ -401,3 +400,32 @@ package schema // time, not about being unstuck. force_stop?: bool @go(ForceStop) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Context +_context: {build: {}, deploy: {}, runtime: {}} @vocab(spec) +_contextWord: [for w, _ in _context {w}] +// #OpVerb +_opVerb: { + mkdir: {} + copy: {} + write: {} + link: {} + download: {} + setcap: {} + build: {} + config: {} + plugin: {} +} @vocab(spec) +_opVerbWord: [for w, _ in _opVerb {w}] +// #CacheMount.sharing +_sharing: {locked: {}, shared: {}, private: {}} @vocab(spec) +_sharingWord: [for w, _ in _sharing {w}] diff --git a/schema/README.md b/schema/README.md index bfde989..ebc7d8c 100644 --- a/schema/README.md +++ b/schema/README.md @@ -55,6 +55,48 @@ embedding the closed `#Op`) is legal through v0.17 and rejected by v0.18's seman with `field not allowed`. Raising the language version is the §10.1 pattern rewrite (TODO 2.16), not a bump. +## Vocabulary tables (S3) + +Every closed set of words in this schema is a **vocabulary table**, never a union +spelled out alternative by alternative (DESIGN.md §10.1 S3, D-PAT-3): + +```cue +_context: {build: {}, deploy: {}, runtime: {}} @vocab(spec) +_contextWord: [for w, _ in _context {w}] + +#Context: or(_contextWord) @go(-) +``` + +- **The table is the ONE home.** A new word is ONE row there; the derived + `_xWord` list and the enum an author meets are recomputations of it, so the + three cannot disagree. +- **`@vocab()` names the owner** (§3.1). Every set in this module is + compiled into the `spec` contract module, so the owner is `spec` + (`scripts/design-consistency.sh` in the umbrella accepts `spec` as a + vocabulary owner beside `core` and the §3.1 plugin words). +- **A field whose default is a word keeps that default inline**: + `sharing?: *"locked" | or(_sharingWord)`. The default is part of the surface + (omitting the key materializes it), and it is also a row of the table, so the + accepted set is unchanged. +- **A two-value literal default stays as written** (`*false | bool`, + `mode?: *"internal" | "external"`): S3's one permitted inline enum is a + default with a single alternative. +- **Naming.** A table is named after its word (`_sharing`) when that name is + unique in the schema, and `` (`_vmNetworkMode`, + `_cacheMountSharing`) when two defs have a field of the same name. Two defs + with the SAME words keep two tables: a field name is not a vocabulary, and + merging them would couple two shapes that only coincide today. +- **Tables live at the foot of the file that uses them**, one section per file, + each table preceded by a `// #Def.field` comment naming its site. The + concatenation is one compilation unit, so a table is visible to every file — + placing it beside its use keeps the pair findable. + +`schema/vocab_s3_test.go` is the gate: every table must carry its `@vocab`, its +derived list must BE its rows, every table must be cited by a field and every +citation must resolve — and the test **injects a row into every table** and +asserts the derived list gains it, which is the only way to see that the list +derives rather than restates. + ## Regenerating `spec` — `charly task cue-gen` In THIS repository: diff --git a/schema/agent.cue b/schema/agent.cue index 318aaf7..a5331d7 100644 --- a/schema/agent.cue +++ b/schema/agent.cue @@ -21,7 +21,7 @@ package schema #CredentialMount: { src: string & !="" dst: string & !="" - mode?: "copy" | "bind" + mode?: or(_credentialMountModeWord) optional?: bool } @@ -70,3 +70,16 @@ package schema progress_no_improvement_timeout?: #Duration @go(ProgressNoImprovementTimeout) output_format?: string @go(OutputFormat) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #CredentialMount.mode +_credentialMountMode: {copy: {}, bind: {}} @vocab(spec) +_credentialMountModeWord: [for w, _ in _credentialMountMode {w}] diff --git a/schema/agent_control.cue b/schema/agent_control.cue index 56cad43..a2fd926 100644 --- a/schema/agent_control.cue +++ b/schema/agent_control.cue @@ -7,7 +7,7 @@ package schema #UUIDv7: string & =~"^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" #TargetHop: { - transport: "inproc" | "exec" | "ssh" | "grpc" | "tmux" + transport: or(_targetHopTransportWord) address?: string user?: string port?: int & >0 & <=65535 @@ -41,8 +41,8 @@ package schema protocol_version: int & >=1 @go(ProtocolVersion,type=int) entrypoint?: [...string] profiles?: [...string] - interaction_modes?: [...("structured" | "terminal")] @go(InteractionModes) - persistence?: "none" | "session" | "required" + interaction_modes?: [...or(_interactionModesWord)] @go(InteractionModes) + persistence?: or(_agentRuntimeCapabilityPersistenceWord) semantic_completion?: bool @go(SemanticCompletion) } @@ -50,7 +50,7 @@ package schema id: #UUIDv7 @go(ID) runtime: string & !="" target: #TargetSpec - state: "new" | "active" | "detached" | "closed" | "failed" + state: or(_agentSessionStateWord) created_at: string @go(CreatedAt) updated_at: string @go(UpdatedAt) storage_ref?: string @go(StorageRef) @@ -84,7 +84,7 @@ package schema #AgentEvent: { run_id: #UUIDv7 @go(RunID) sequence: int & >=1 - type: "started" | "message" | "tool" | "delegation" | "settled" | "status" | "completed" | "aborted" | "failed" + type: or(_agentEventTypeWord) time: string payload?: {...} @go(Payload,type=map[string]any) } @@ -109,7 +109,7 @@ package schema edges?: [...#AgentDelegationEdge] coordinator?: string concurrency?: int & >=1 - evidence_policy?: "target" | "coordinator" | "both" @go(EvidencePolicy) + evidence_policy?: or(_evidencePolicyWord) @go(EvidencePolicy) } // Durable control-plane projection of a declarative team. The authored graph @@ -128,7 +128,7 @@ package schema owner: string & !="" session_id?: #UUIDv7 @go(SessionID) run_id?: #UUIDv7 @go(RunID) - state: "delegated" | "active" | "settled" | "failed" + state: or(_agentFederationRecordStateWord) updated_at: string @go(UpdatedAt) metadata?: #StrMap } @@ -136,7 +136,7 @@ package schema #Incident: { id: #UUIDv7 @go(ID) run_id?: #UUIDv7 @go(RunID) - state: "needs_rca" | "rca_active" | "awaiting_recovery" | "resolved" + state: or(_incidentStateWord) summary: string & !="" created_at: string @go(CreatedAt) evidence_refs?: [...string] @go(EvidenceRefs) @@ -145,7 +145,7 @@ package schema #RCARecord: { id: #UUIDv7 @go(ID) incident_id: #UUIDv7 @go(IncidentID) - state: "active" | "complete" + state: or(_rcaRecordStateWord) findings?: [...string] root_cause?: string @go(RootCause) completed_at?: string @go(CompletedAt) @@ -167,10 +167,10 @@ package schema id: #UUIDv7 @go(ID) incident_id: #UUIDv7 @go(IncidentID) rca_id?: #UUIDv7 @go(RCAID) - action: "reattach" | "resume" | "restart" | "rebuild-target" | "change-runtime" | "reassign" | "abort" | "operator" + action: or(_recoveryDecisionActionWord) authorized_emergency_abort?: bool @go(AuthorizedEmergencyAbort) params?: #RecoveryParams @go(Params,type=*RecoveryParams) - state: "planned" | "applied" | "failed" + state: or(_recoveryDecisionStateWord) decided_at: string @go(DecidedAt) applied_at?: string @go(AppliedAt) error?: string @@ -187,12 +187,12 @@ package schema semantic_adapter?: string @go(SemanticAdapter) keys?: [...string] signals?: [...string] - persistence?: "none" | "detach" | "required" - transcript?: "none" | "raw" | "screen" | "both" + persistence?: or(_terminalProfilePersistenceWord) + transcript?: or(_transcriptWord) } #TerminalInput: { - kind: "text" | "paste" | "command" | "key" | "resize" | "signal" | "close" + kind: or(_terminalInputKindWord) text?: string key?: string cols?: int & >0 & <=1000 @@ -212,8 +212,8 @@ package schema #TerminalFrame: { run_id: #UUIDv7 @go(RunID) sequence: int & >=1 - kind: "raw" | "screen" | "status" | "exit" | "error" | "resync" - stream?: "stdout" | "stderr" | "terminal" + kind: or(_terminalFrameKindWord) + stream?: or(_streamWord) data?: bytes snapshot?: string status?: string @@ -237,3 +237,100 @@ package schema reason?: string time: string } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #TargetHop.transport +_targetHopTransport: {inproc: {}, exec: {}, ssh: {}, grpc: {}, tmux: {}} @vocab(spec) +_targetHopTransportWord: [for w, _ in _targetHopTransport {w}] +// #AgentRuntimeCapability.interaction_modes +_interactionModes: {structured: {}, terminal: {}} @vocab(spec) +_interactionModesWord: [for w, _ in _interactionModes {w}] +// #AgentRuntimeCapability.persistence +_agentRuntimeCapabilityPersistence: {none: {}, session: {}, required: {}} @vocab(spec) +_agentRuntimeCapabilityPersistenceWord: [for w, _ in _agentRuntimeCapabilityPersistence {w}] +// #AgentSession.state +_agentSessionState: {new: {}, active: {}, detached: {}, closed: {}, failed: {}} @vocab(spec) +_agentSessionStateWord: [for w, _ in _agentSessionState {w}] +// #AgentEvent.type +_agentEventType: { + started: {} + message: {} + tool: {} + delegation: {} + settled: {} + status: {} + completed: {} + aborted: {} + failed: {} +} @vocab(spec) +_agentEventTypeWord: [for w, _ in _agentEventType {w}] +// #AgentTeam.evidence_policy +_evidencePolicy: {target: {}, coordinator: {}, both: {}} @vocab(spec) +_evidencePolicyWord: [for w, _ in _evidencePolicy {w}] +// #AgentFederationRecord.state +_agentFederationRecordState: {delegated: {}, active: {}, settled: {}, failed: {}} @vocab(spec) +_agentFederationRecordStateWord: [for w, _ in _agentFederationRecordState {w}] +// #Incident.state +_incidentState: { + needs_rca: {} + rca_active: {} + awaiting_recovery: {} + resolved: {} +} @vocab(spec) +_incidentStateWord: [for w, _ in _incidentState {w}] +// #RCARecord.state +_rcaRecordState: {active: {}, complete: {}} @vocab(spec) +_rcaRecordStateWord: [for w, _ in _rcaRecordState {w}] +// #RecoveryDecision.action +_recoveryDecisionAction: { + reattach: {} + resume: {} + restart: {} + "rebuild-target": {} + "change-runtime": {} + reassign: {} + abort: {} + operator: {} +} @vocab(spec) +_recoveryDecisionActionWord: [for w, _ in _recoveryDecisionAction {w}] +// #RecoveryDecision.state +_recoveryDecisionState: {planned: {}, applied: {}, failed: {}} @vocab(spec) +_recoveryDecisionStateWord: [for w, _ in _recoveryDecisionState {w}] +// #TerminalProfile.persistence +_terminalProfilePersistence: {none: {}, detach: {}, required: {}} @vocab(spec) +_terminalProfilePersistenceWord: [for w, _ in _terminalProfilePersistence {w}] +// #TerminalProfile.transcript +_transcript: {none: {}, raw: {}, screen: {}, both: {}} @vocab(spec) +_transcriptWord: [for w, _ in _transcript {w}] +// #TerminalInput.kind +_terminalInputKind: { + text: {} + paste: {} + command: {} + key: {} + resize: {} + signal: {} + "close": {} +} @vocab(spec) +_terminalInputKindWord: [for w, _ in _terminalInputKind {w}] +// #TerminalFrame.kind +_terminalFrameKind: { + raw: {} + screen: {} + status: {} + exit: {} + error: {} + resync: {} +} @vocab(spec) +_terminalFrameKindWord: [for w, _ in _terminalFrameKind {w}] +// #TerminalFrame.stream +_stream: {stdout: {}, stderr: {}, terminal: {}} @vocab(spec) +_streamWord: [for w, _ in _stream {w}] diff --git a/schema/box.cue b/schema/box.cue index 67179fc..1ed48e3 100644 --- a/schema/box.cue +++ b/schema/box.cue @@ -12,10 +12,10 @@ package schema // Package formats (BoxConfig.Build / BuildFormats). Named #BuildFormat to avoid // colliding with distro.cue's #Format (the package-format definition struct). -#BuildFormat: "rpm" | "deb" | "pac" | "aur" | "apk" @go(-) +#BuildFormat: or(_buildFormatWord) @go(-) // Builder build-type slots (BoxConfig.Produce + BoxConfig.Builder keys). -#BuildType: "pixi" | "npm" | "cargo" | "aur" @go(-) +#BuildType: or(_buildTypeWord) @go(-) // MergeConfig (config.go). CLOSED. #BoxMerge: { @@ -86,7 +86,7 @@ package schema user?: string & !="" uid?: int & >=0 @go(UID,type=*int) gid?: int & >=0 @go(GID,type=*int) - user_policy?: *"auto" | "adopt" | "create" @go(UserPolicy) + user_policy?: *"auto" | or(_userPolicyWord) @go(UserPolicy) builder?: {[string]: string & !=""} produce?: [...#BuildType] @@ -101,7 +101,7 @@ package schema // omits is unselectable, which is what kept `openrc` out of reach despite being // a first-class init with its own service_template, management commands and // unit_path_template. - init?: "supervisord" | "systemd" | "systemd-container" | "openrc" + init?: or(_boxInitWord) data_image?: bool @go(DataImage) bootc?: bool // image is bootc-bootable (for `charly vm build` → qcow2) readiness?: #Readiness @go(Readiness,optional=nillable) @@ -122,7 +122,7 @@ package schema stage_parallelism?: int & >=0 @go(StageParallelism,type=*int) // 0 = auto (min(NCPU, cap)) — stages of ONE build stage_parallelism_cap?: int & >=1 @go(StageParallelismCap,type=*int) // ceiling for the auto calc context_ignore?: [...(string & !="")] @go(ContextIgnore) - cache?: "image" | "registry" | "gha" | "none" + cache?: or(_cacheWord) keep_images?: int & >=0 @go(KeepImages,type=*int) keep_check_runs?: int & >=0 @go(KeepCheckRuns,type=*int) @@ -130,7 +130,35 @@ package schema alias?: [...#BoxAlias] plan?: [...#Step] - check_level?: *"noagent" | "none" | "build" | "agent" @go(CheckLevel) + check_level?: *"noagent" | or(_checkLevelWord) @go(CheckLevel) shell?: #Shell @go(Shell,optional=nillable) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #BuildFormat +_buildFormat: {rpm: {}, deb: {}, pac: {}, aur: {}, apk: {}} @vocab(spec) +_buildFormatWord: [for w, _ in _buildFormat {w}] +// #BuildType +_buildType: {pixi: {}, npm: {}, cargo: {}, aur: {}} @vocab(spec) +_buildTypeWord: [for w, _ in _buildType {w}] +// #Box.user_policy +_userPolicy: {auto: {}, adopt: {}, create: {}} @vocab(spec) +_userPolicyWord: [for w, _ in _userPolicy {w}] +// #Box.init +_boxInit: {supervisord: {}, systemd: {}, "systemd-container": {}, openrc: {}} @vocab(spec) +_boxInitWord: [for w, _ in _boxInit {w}] +// #Box.cache +_cache: {image: {}, registry: {}, gha: {}, none: {}} @vocab(spec) +_cacheWord: [for w, _ in _cache {w}] +// #Box.check_level +_checkLevel: {noagent: {}, none: {}, build: {}, agent: {}} @vocab(spec) +_checkLevelWord: [for w, _ in _checkLevel {w}] diff --git a/schema/candy.cue b/schema/candy.cue index a0bba6e..024e30f 100644 --- a/schema/candy.cue +++ b/schema/candy.cue @@ -18,7 +18,7 @@ package schema plan?: [...#Step] // --- maturity / engine --- - status?: *"testing" | "working" | "broken" + status?: *"testing" | or(_candyStatusWord) // engine — a candy's required run engine; per-image resolution walks the // candy chain (engine.go ResolveBoxEngine) and cross-candy conflicts are a // validate error. RETAINED by the schema-compaction consumer audit. @@ -222,10 +222,10 @@ package schema // with `systemctl start ` / `systemctl --user start `. #PackagingSystemdUnit: { name: string & !="" - scope: "system" | "user" + scope: or(_packagingSystemdUnitScopeWord) exec: string & !="" description?: string & !="" - restart?: "no" | "on-failure" | "always" | "unless-stopped" + restart?: or(_packagingSystemdUnitRestartWord) after?: [...(string & !="")] wants?: [...(string & !="")] environment?: {[string]: string} @go(Environment,type=map[string]string) @@ -250,7 +250,7 @@ package schema // against the target distro tag chain (compileServiceSteps + generate.go). distro?: [...(string & !="")] env?: #StrMap - restart?: "no" | "on-failure" | "always" | "unless-stopped" + restart?: or(_candyServiceRestartWord) working_directory?: string & !="" @go(WorkingDirectory) user?: string & !="" after?: [...(string & !="")] @@ -258,7 +258,7 @@ package schema wanted_by?: [...(string & !="")] @go(WantedBy) stdout?: string & !="" // "journal" | "none" | "file:" stop_timeout?: (string & !="") | int @go(StopTimeout,type=string) // "20s" or an unquoted 20 (Go-coerced; Go field is string) - scope?: "system" | "user" + scope?: or(_candyServiceScopeWord) enable?: bool // exec_start_pre / exec_start_post — ordered hook commands run around a CUSTOM // (exec:) entry's own start, rendered by the init system's service_template @@ -276,7 +276,7 @@ package schema exec_start_pre?: [...(string & !="")] @go(ExecStartPre) exec_start_post?: [...(string & !="")] @go(ExecStartPost) overrides?: #CandyServiceOverrides @go(Overrides,optional=nillable) - kind?: "program" | "eventlistener" + kind?: or(_candyServiceKindWord) event?: string & !="" @go(Events) auto_start?: bool @go(AutoStart,type=*bool) start_retry?: int & >=0 @go(StartRetries,type=int) @@ -292,7 +292,7 @@ package schema // // type — systemd Type=; supervisord approximates oneshot with startsecs=0; // OpenRC distinguishes forking via command_background. - type?: "simple" | "exec" | "forking" | "oneshot" | "notify" | "idle" @go(Type) + type?: or(_candyServiceTypeWord) @go(Type) // requires — a HARD dependency, distinct from after:'s ordering. systemd // Requires=; OpenRC depend(){ need … }, which its template already emits for // localmount. supervisord has no equivalent and ignores it. @@ -495,7 +495,7 @@ package schema #CandyApk: { package?: string & !="" apk?: string & !="" - source?: *"apk-pure" | "google-play" | "f-droid" | "huawei-app-gallery" + source?: *"apk-pure" | or(_sourceWord) arch?: string & !="" app_version?: string & !="" // exactly one of package/apk (disjunction keeps it CLOSED; matchN would open it). @@ -653,3 +653,54 @@ package schema // plugin-box's validPluginClasses), which is the side that NAMES an undeclared class; // word and parent are lowercase-hyphenated. #PluginCapability: string & =~#PluginCapabilityPattern + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Candy.status +_candyStatus: {testing: {}, working: {}, broken: {}} @vocab(spec) +_candyStatusWord: [for w, _ in _candyStatus {w}] +// #PackagingSystemdUnit.scope +_packagingSystemdUnitScope: {system: {}, user: {}} @vocab(spec) +_packagingSystemdUnitScopeWord: [for w, _ in _packagingSystemdUnitScope {w}] +// #PackagingSystemdUnit.restart +_packagingSystemdUnitRestart: { + no: {} + "on-failure": {} + always: {} + "unless-stopped": {} +} @vocab(spec) +_packagingSystemdUnitRestartWord: [for w, _ in _packagingSystemdUnitRestart {w}] +// #CandyService.restart +_candyServiceRestart: {no: {}, "on-failure": {}, always: {}, "unless-stopped": {}} @vocab(spec) +_candyServiceRestartWord: [for w, _ in _candyServiceRestart {w}] +// #CandyService.scope +_candyServiceScope: {system: {}, user: {}} @vocab(spec) +_candyServiceScopeWord: [for w, _ in _candyServiceScope {w}] +// #CandyService.kind +_candyServiceKind: {program: {}, eventlistener: {}} @vocab(spec) +_candyServiceKindWord: [for w, _ in _candyServiceKind {w}] +// #CandyService.type +_candyServiceType: { + simple: {} + exec: {} + forking: {} + oneshot: {} + notify: {} + idle: {} +} @vocab(spec) +_candyServiceTypeWord: [for w, _ in _candyServiceType {w}] +// #CandyApk.source +_source: { + "apk-pure": {} + "google-play": {} + "f-droid": {} + "huawei-app-gallery": {} +} @vocab(spec) +_sourceWord: [for w, _ in _source {w}] diff --git a/schema/cli_model.cue b/schema/cli_model.cue index 6d87262..4470f2f 100644 --- a/schema/cli_model.cue +++ b/schema/cli_model.cue @@ -5,7 +5,7 @@ package schema #CLIArg: { prop: string & !="" name: string - type: "string" | "boolean" | "integer" | "number" | "array" | "object" + type: or(_cliArgTypeWord) help?: string enum?: [...string] default?: string @@ -15,7 +15,7 @@ package schema is_slice?: bool @go(IsSlice) is_map?: bool @go(IsMap) negated?: bool - elem_type?: "string" | "boolean" | "integer" | "number" | "array" | "object" @go(ElemType) + elem_type?: or(_elemTypeWord) @go(ElemType) } #CLILeaf: { @@ -31,3 +31,33 @@ package schema version: string leaves: [...#CLILeaf] } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #CLIArg.type +_cliArgType: { + "string": {} + boolean: {} + integer: {} + "number": {} + array: {} + object: {} +} @vocab(spec) +_cliArgTypeWord: [for w, _ in _cliArgType {w}] +// #CLIArg.elem_type +_elemType: { + "string": {} + boolean: {} + integer: {} + "number": {} + array: {} + object: {} +} @vocab(spec) +_elemTypeWord: [for w, _ in _elemType {w}] diff --git a/schema/deploy.cue b/schema/deploy.cue index 0801725..a52ef0b 100644 --- a/schema/deploy.cue +++ b/schema/deploy.cue @@ -26,7 +26,7 @@ package schema // VirtualMachine), whose lifecycle an out-of-process plugin owns (NOT `charly vm`); // distinct from `ssh` so a venue test names the host-libvirt vm unambiguously. // none — external-in-place (zero value). - venue?: ("container" | "ssh" | "shell" | "parent" | "kubevirt" | "none") @go(Venue, type=string) + venue?: or(_venueWord) @go(Venue, type=string) // image_backed: the substrate runs a baked OCI image (pod). image_backed?: bool @go(ImageBacked) // image_context: the substrate composes over an image build context (pod overlay, kubernetes manifests). @@ -62,7 +62,7 @@ package schema // instead of switching on the substrate kind word: `engine:` on a substrate // without "engine" is an error, and likewise `backend:`/`rootful:`. This is // the DATA half of "engine: only on a pod deploy, backend: only on a vm deploy". - selectors?: [...("engine" | "backend" | "rootful")] @go(Selectors) + selectors?: [...or(_selectorsWord)] @go(Selectors) } // #DescentDescriptor is the loader-DERIVED venue-hop descriptor (Cutover H + P9). @@ -78,7 +78,7 @@ package schema // container-exec — enter the container by name (pod; podman/docker per engine). // ssh — an ssh hop into the guest (vm; kubevirt shares this transport). // reject — unreachable via the deploy chain (kubernetes → use kubectl). - transport: "none" | "container-exec" | "ssh" | "reject" + transport: or(_descentDescriptorTransportWord) // host_rooted: the substrate's own ROOT executor runs directly on the host // (local), so the check runner uses rootExecutorForDeployNode, not a container chain. host_rooted?: bool @go(HostRooted) @@ -96,7 +96,7 @@ package schema // authored `target:` outright. The former default `*"pod"` is dropped (Go's // classifyTarget supplies the empty→pod default). Generated as a plain Go // `string` (the loader stamps it; the CUE enum still validates a pinned value). - target?: ("pod" | "vm" | "kubernetes" | "local" | "android" | "kindcluster" | "kubevirt" | "firecracker") @go(Target,type=string) // loader-DERIVED (yaml:"-") + target?: or(_targetWord) @go(Target,type=string) // loader-DERIVED (yaml:"-") // member_of is a loader-DERIVED runtime field (never authored; rejected by // #DeployValue): it marks a folded deploy-level member entry registered as a @@ -132,9 +132,9 @@ package schema from?: string & =~"^[a-z0-9]+([.-][a-z0-9]+)*(:[a-zA-Z0-9._-]+)?$" // the same-kind template ref, or the unified NAME:TAG spelling (VM deploys: the loader splits the last colon into from + from_snapshot); the entity part allows the NAMESPACE-QUALIFIED form (ns.entity, a git-linked import ref) image?: string & !="" - kind?: "service" | "daemon" | "batch" | "scheduled" | "oneshot" + kind?: or(_deployKindWord) replica?: int & >=0 @go(,type=int) - restart?: "always" | "on-failure" | "never" + restart?: or(_deployRestartWord) schedule?: string & !="" tunnel?: #Tunnel @go(Tunnel,type=*TunnelYAML) @@ -280,7 +280,7 @@ package schema // / --anchor still force skip structurally. // In-place (local/external) beds have no restartable venue: restart-only // leaves the gate step unchanged there (the in-place re-apply), same as full. - update_gate?: *"full" | "restart-only" | "skip" @go(UpdateGate) + update_gate?: *"full" | or(_updateGateWord) @go(UpdateGate) disposable?: bool @go(,type=*bool) // keep_on_failure — whether a check bed that FAILS keeps its venue alive for @@ -308,7 +308,7 @@ package schema // through the verb's OWN served context rules (the runner never branches on // capture kind). instrument?: [...#Instrument] - lifecycle?: "scratch" | "dev" | "test" | "qa" | "staging" | "prod" | "custom" + lifecycle?: or(_lifecycleWord) // RecordWrap is the whole-run recording wrap (deploy record: field). #RecordWrap: { terminal?: bool @@ -353,7 +353,7 @@ package schema // up alongside its parent on the shared network. // in-substrate — an entity key inside the kind body: a NESTED member, // deployed into the parent's venue. - position: ("deploy-level" | "in-substrate") @go(Position,type=string) + position: or(_positionWord) @go(Position,type=string) // node is the member's own deploy node (the recursive tree edge). node: #Deploy } @@ -373,7 +373,7 @@ package schema #Check: #Deploy #Tunnel: (("tailscale" | "cloudflare") | { - provider: "tailscale" | "cloudflare" + provider: or(_providerWord) tunnel?: string & !="" public?: #PortScope private?: #PortScope @@ -406,7 +406,7 @@ package schema ssh_user?: string backend?: #VmBackend // "auto" persisted pre-resolution (the vm deploy lifecycle hook); krun is a resolved/selected backend cloud_init_rendered_digest?: string - charly_install_strategy?: "auto" | "scp" | "url" | "skip" + charly_install_strategy?: or(_charlyInstallStrategyWord) // port_forwards persists the auto-allocated host ports for `auto:` // network.port_forwards entries (guest-port → allocated host port), so the // allocation is reused across the vm-create → deploy-add sequence — the @@ -532,7 +532,7 @@ package schema #DeployVolume: { name: string & !="" - type?: "volume" | "bind" | "encrypted" + type?: or(_deployVolumeTypeWord) host?: string & !="" path?: string & =~"^/" data_seeded?: bool @go(DataSeeded) @@ -555,13 +555,13 @@ package schema #DeployStorage: { name: string & !="" size?: string & !="" - class_hint?: "fast" | "cheap" | "encrypted" | "default" @go(ClassHint) - access?: "single-writer" | "many-readers" | "many-writers" + class_hint?: or(_classHintWord) @go(ClassHint) + access?: or(_accessWord) path?: string & =~"^/" } #KubernetesDeploy: { namespace?: #EntityRef - workload?: "Deployment" | "StatefulSet" | "DaemonSet" | "Pod" | "Job" | "CronJob" + workload?: or(_workloadWord) patches?: [...#KubernetesPatch] raw?: [...string] // helm_charts — chart releases the kustomize/helm venues install alongside the @@ -597,7 +597,7 @@ package schema #Preemptible: ([...(string & !="")] | { holds?: [...(string & !="")] stop?: "shutdown" - restore?: "always" | "on-success" + restore?: or(_restoreWord) }) @go(-) // gengotypes: hand PreemptibleConfig // --------------------------------------------------------------------------- @@ -1033,3 +1033,85 @@ package schema requires_exclusive?: [...string] @go(RequiresExclusive) requires_shared?: [...string] @go(RequiresShared) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #DeployTraits.venue +_venue: {container: {}, ssh: {}, shell: {}, parent: {}, kubevirt: {}, none: {}} @vocab(spec) +_venueWord: [for w, _ in _venue {w}] +// #DeployTraits.selectors +_selectors: {engine: {}, backend: {}, rootful: {}} @vocab(spec) +_selectorsWord: [for w, _ in _selectors {w}] +// #DescentDescriptor.transport +_descentDescriptorTransport: {none: {}, "container-exec": {}, ssh: {}, reject: {}} @vocab(spec) +_descentDescriptorTransportWord: [for w, _ in _descentDescriptorTransport {w}] +// #Deploy.target +_target: { + pod: {} + vm: {} + kubernetes: {} + local: {} + android: {} + kindcluster: {} + kubevirt: {} + firecracker: {} +} @vocab(spec) +_targetWord: [for w, _ in _target {w}] +// #Deploy.kind +_deployKind: {service: {}, daemon: {}, batch: {}, scheduled: {}, oneshot: {}} @vocab(spec) +_deployKindWord: [for w, _ in _deployKind {w}] +// #Deploy.restart +_deployRestart: {always: {}, "on-failure": {}, never: {}} @vocab(spec) +_deployRestartWord: [for w, _ in _deployRestart {w}] +// #Deploy.update_gate +_updateGate: {full: {}, "restart-only": {}, skip: {}} @vocab(spec) +_updateGateWord: [for w, _ in _updateGate {w}] +// #Deploy.lifecycle +_lifecycle: { + scratch: {} + dev: {} + test: {} + qa: {} + staging: {} + prod: {} + custom: {} +} @vocab(spec) +_lifecycleWord: [for w, _ in _lifecycle {w}] +// #Member.position +_position: {"deploy-level": {}, "in-substrate": {}} @vocab(spec) +_positionWord: [for w, _ in _position {w}] +// #Tunnel.provider +_provider: {tailscale: {}, cloudflare: {}} @vocab(spec) +_providerWord: [for w, _ in _provider {w}] +// #VmDeployState.charly_install_strategy +_charlyInstallStrategy: {auto: {}, scp: {}, url: {}, skip: {}} @vocab(spec) +_charlyInstallStrategyWord: [for w, _ in _charlyInstallStrategy {w}] +// #DeployVolume.type +_deployVolumeType: {volume: {}, bind: {}, encrypted: {}} @vocab(spec) +_deployVolumeTypeWord: [for w, _ in _deployVolumeType {w}] +// #DeployStorage.class_hint +_classHint: {fast: {}, cheap: {}, encrypted: {}, default: {}} @vocab(spec) +_classHintWord: [for w, _ in _classHint {w}] +// #DeployStorage.access +_access: {"single-writer": {}, "many-readers": {}, "many-writers": {}} @vocab(spec) +_accessWord: [for w, _ in _access {w}] +// #KubernetesDeploy.workload +_workload: { + "Deployment": {} + "StatefulSet": {} + "DaemonSet": {} + "Pod": {} + "Job": {} + "CronJob": {} +} @vocab(spec) +_workloadWord: [for w, _ in _workload {w}] +// #Preemptible.restore +_restore: {always: {}, "on-success": {}} @vocab(spec) +_restoreWord: [for w, _ in _restore {w}] diff --git a/schema/desktop.cue b/schema/desktop.cue index e85b446..0b94969 100644 --- a/schema/desktop.cue +++ b/schema/desktop.cue @@ -133,7 +133,7 @@ package schema compositor!: string & !="" // syntax is a label for diagnostics and for choosing an escaping strategy. - syntax?: *"lua" | "ini" | "sway" | "xml" | "yaml" + syntax?: *"lua" | or(_syntaxWord) // config_path_template is where the rendered config lands, as a Go template so a // compositor that keys off its own name does not need a second field. @@ -186,13 +186,13 @@ package schema // nothing installed produces a black screen and an empty journal, which is the single worst // failure this surface has. #DisplayManager: { - manager: *"sddm" | "greetd" | "gdm" | "ly" + manager: *"sddm" | or(_managerWord) // session is the #SessionDesktop.id to start. Cross-checked at load. session!: string & =~"^[a-z0-9]+(-[a-z0-9]+)*$" autologin?: #AutoLogin - numlock?: *"none" | "on" | "off" + numlock?: *"none" | or(_numlockWord) // theme names a greeter theme package or directory. Free-form: every greeter spells // this differently and none of them validate it either. @@ -263,13 +263,50 @@ package schema // #DesktopWindow is the placement the session renderer turns into a window rule. #DesktopWindow: { - placement?: *"default" | "float" | "tile" | "fullscreen" | "maximize" + placement?: *"default" | or(_placementWord) workspace?: string & !="" size?: string & =~"^[0-9]+x[0-9]+$" } // #DesktopCategory is the freedesktop registered MAIN categories, verbatim. Closed on // purpose — see the note on `categories`. -#DesktopCategory: "AudioVideo" | "Audio" | "Video" | "Development" | "Education" | - "Game" | "Graphics" | "Network" | "Office" | "Science" | "Settings" | - "System" | "Utility" +#DesktopCategory: or(_desktopCategoryWord) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Session.syntax +_syntax: {lua: {}, ini: {}, sway: {}, xml: {}, yaml: {}} @vocab(spec) +_syntaxWord: [for w, _ in _syntax {w}] +// #DisplayManager.manager +_manager: {sddm: {}, greetd: {}, gdm: {}, ly: {}} @vocab(spec) +_managerWord: [for w, _ in _manager {w}] +// #DisplayManager.numlock +_numlock: {none: {}, on: {}, off: {}} @vocab(spec) +_numlockWord: [for w, _ in _numlock {w}] +// #DesktopWindow.placement +_placement: {default: {}, "float": {}, tile: {}, fullscreen: {}, maximize: {}} @vocab(spec) +_placementWord: [for w, _ in _placement {w}] +// #DesktopCategory +_desktopCategory: { + "AudioVideo": {} + "Audio": {} + "Video": {} + "Development": {} + "Education": {} + "Game": {} + "Graphics": {} + "Network": {} + "Office": {} + "Science": {} + "Settings": {} + "System": {} + "Utility": {} +} @vocab(spec) +_desktopCategoryWord: [for w, _ in _desktopCategory {w}] diff --git a/schema/distro.cue b/schema/distro.cue index 961e5df..3d3601b 100644 --- a/schema/distro.cue +++ b/schema/distro.cue @@ -33,7 +33,7 @@ package schema #Format: { cache_mount?: [...#CacheMount] @go(CacheMount) - section_field?: {[string]: "list" | "list_of_maps"} @go(SectionFields) + section_field?: {[string]: or(_sectionFieldWord)} @go(SectionFields) uninstall_template?: string @go(UninstallTemplate) // present_template is a command rendered for this format whose stdout lists, // one per line, the packages from the rendered set that the venue ALREADY has @@ -283,3 +283,16 @@ package schema // answer carries the vm entity's per-distro extras verbatim. answer?: {[string]: string} @go(Answers) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Format.section_field.[string] +_sectionField: {list: {}, list_of_maps: {}} @vocab(spec) +_sectionFieldWord: [for w, _ in _sectionField {w}] diff --git a/schema/distro_vocab.cue b/schema/distro_vocab.cue index c1d85d3..55854d8 100644 --- a/schema/distro_vocab.cue +++ b/schema/distro_vocab.cue @@ -26,13 +26,13 @@ package schema // would add exported contract-module API with zero callers. #DistroTrait: { // format is the native package format. The four charly implements. - format: "rpm" | "deb" | "pac" | "apk" + format: or(_distroTraitFormatWord) // ssh_unit is the service name OpenSSH installs. Debian-family ships `ssh`; // everyone else ships `sshd`. Getting this wrong leaves a guest unreachable. - ssh_unit: "ssh" | "sshd" + ssh_unit: or(_sshUnitWord) // init is the guest's service manager. It selects HOW a service is enabled and // started, never a distro-specific command inline in Go. - init: "systemd" | "openrc" + init: or(_distroTraitInitWord) // ovmf_family is which vendor's OVMF firmware layout a HOST of this distro ships, // selecting where the edk2 images live. It is a property of the distro like the // other three, and it lived in a hand-maintained `ovmf_distro_aliases:` map kept in @@ -43,7 +43,7 @@ package schema // exactly, where ovmfCandidatesForDistro tries the union of all families and the // not-found error emits the generic install hint. archarm (aarch64 firmware paths // differ) and alpine are unset for that reason, as they were before. - ovmf_family?: "fedora" | "arch" | "debian" + ovmf_family?: or(_ovmfFamilyWord) } @go(-) #Distros: [string]: #DistroTrait @@ -83,3 +83,25 @@ package schema // #DistroID is the closed id vocabulary, DERIVED from #Distros' own keys so the two can // never disagree. A VM source's `distro:` is validated against this. #DistroID: or([for id, _ in #Distros {id}]) @go(-) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #DistroTrait.format +_distroTraitFormat: {rpm: {}, deb: {}, pac: {}, apk: {}} @vocab(spec) +_distroTraitFormatWord: [for w, _ in _distroTraitFormat {w}] +// #DistroTrait.ssh_unit +_sshUnit: {ssh: {}, sshd: {}} @vocab(spec) +_sshUnitWord: [for w, _ in _sshUnit {w}] +// #DistroTrait.init +_distroTraitInit: {systemd: {}, openrc: {}} @vocab(spec) +_distroTraitInitWord: [for w, _ in _distroTraitInit {w}] +// #DistroTrait.ovmf_family +_ovmfFamily: {fedora: {}, arch: {}, debian: {}} @vocab(spec) +_ovmfFamilyWord: [for w, _ in _ovmfFamily {w}] diff --git a/schema/engine.cue b/schema/engine.cue index aec9b4d..062678b 100644 --- a/schema/engine.cue +++ b/schema/engine.cue @@ -54,7 +54,7 @@ package schema // The word `containerd` means "containerd, driven through the nerdctl CLI", // rootful OR rootless. `nerdctl` is the CLI binary, never an engine word: the // former `engine: nerdctl` spelling is deleted (R5, no alias, no dual path). -#EngineName: ("podman" | "docker" | "containerd") +#EngineName: or(_engineNameWord) // #EngineBinaries — the engine WORD → CLI BINARY mapping. The two are NOT the // same word: containerd is driven through nerdctl, while podman/docker use their @@ -76,7 +76,7 @@ package schema // direct — an ephemeral argv launch with no unit (docker today). // `charly task cue-gen` emits this as spec.EngineRunModes; ValidateRunMode derives // from it, and container.IsRunMode reads it. -#EngineRunMode: ("quadlet" | "systemd-unit" | "direct") +#EngineRunMode: or(_engineRunModeWord) // #EngineUnitRunModes — the SUBSET of #EngineRunMode that is supervised by a // generated unit file (as opposed to an ephemeral argv launch). This is the @@ -93,7 +93,7 @@ package schema // are explicit operator choices. `charly task cue-gen` emits it as // spec.RootfulModes, so a resolver validates a flag/env/settings value without a // hand-listed word set. -#RootfulMode: ("auto" | "machine" | "sudo" | "native") +#RootfulMode: or(_rootfulModeWord) // #EngineCapability — the static facts about an engine, answered by OpDescribe. // This is the PUBLISHED provider contract: the fields a provider fills and the @@ -155,7 +155,7 @@ package schema // gpu_arg_style — the vendor-GPU passthrough flag family. // cdi — podman's `--device nvidia.com/gpu=all` // gpus — docker/containerd's `--gpus all` - gpu_arg_style!: ("cdi" | "gpus") @go(GPUArgStyle) + gpu_arg_style!: or(_gpuArgStyleWord) @go(GPUArgStyle) // userns_keepid_arg — the per-container keep-id argv when supported (podman // `--userns=keep-id`); empty otherwise. userns_keepid_arg?: string @go(UsernsKeepIDArg) @@ -184,7 +184,7 @@ package schema // loader reads this instead of a word literal: containerd hosts ["pod"]; a // kind:vm through the krun backend is reached by the VM-backend class, not by // widening this list. An engine may host only these kinds and nothing else. - substrates!: [...("pod" | "vm")] @go(Substrates) + substrates!: [...or(_substratesWord)] @go(Substrates) // runtime_handlers — the OCI runtime handlers this engine knows (containerd: // crun/krun/runc/nvidia). A run plan selects `--runtime` from this set; the // handler word is DATA, never a literal in the plan builder. @@ -321,3 +321,28 @@ package schema capability?: #EngineCapability @go(Capability,optional=nillable) error?: string @go(Error) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #EngineName +_engineName: {podman: {}, docker: {}, containerd: {}} @vocab(spec) +_engineNameWord: [for w, _ in _engineName {w}] +// #EngineRunMode +_engineRunMode: {quadlet: {}, "systemd-unit": {}, direct: {}} @vocab(spec) +_engineRunModeWord: [for w, _ in _engineRunMode {w}] +// #RootfulMode +_rootfulMode: {auto: {}, machine: {}, sudo: {}, native: {}} @vocab(spec) +_rootfulModeWord: [for w, _ in _rootfulMode {w}] +// #EngineCapability.gpu_arg_style +_gpuArgStyle: {cdi: {}, gpus: {}} @vocab(spec) +_gpuArgStyleWord: [for w, _ in _gpuArgStyle {w}] +// #EngineCapability.substrates +_substrates: {pod: {}, vm: {}} @vocab(spec) +_substratesWord: [for w, _ in _substrates {w}] diff --git a/schema/firecracker.cue b/schema/firecracker.cue index 3e2c78b..1a910f4 100644 --- a/schema/firecracker.cue +++ b/schema/firecracker.cue @@ -143,7 +143,7 @@ package schema // forwarding the substrate controls. #FirecrackerForwarding: { ip_forward: *true | false @go(IPForward,type=bool) - policy?: *"append-accept" | "netns" | "deny" @go(Policy) + policy?: *"append-accept" | or(_firecrackerForwardingPolicyWord) @go(Policy) } // #FirecrackerMachine is the microVM shape. vcpu_count and mem_size_mib mirror @@ -153,10 +153,10 @@ package schema vcpu_count?: int & >=1 & <=32 @go(VCPUCount,type=int) mem_size_mib?: int & >=128 @go(MemSizeMiB,type=int) smt?: bool - huge_pages?: *"None" | "Transparent" | "2M" @go(HugePages) + huge_pages?: *"None" | or(_hugePagesWord) @go(HugePages) // cpu_template mirrors Firecracker's (deprecated) CpuTemplate enum; it exists // for snapshot/clone portability across hosts. - cpu_template?: *"None" | "C3" | "T2" | "T2S" | "T2CL" | "T2A" | "V1N1" @go(CPUTemplate) + cpu_template?: *"None" | or(_cpuTemplateWord) @go(CPUTemplate) drives?: [...#FirecrackerDrive] @go(Drives) // rate_limits bound the interfaces and the root drive; an omitted bucket is // unlimited (Firecracker's own semantics). @@ -235,7 +235,7 @@ package schema serial_out_path?: string @go(SerialOutPath) rate_limiter?: #FirecrackerRateLimiter @go(RateLimiter,optional=nillable) log_path?: string @go(LogPath) - log_level?: *"Info" | "Error" | "Warning" | "Debug" | "Trace" | "Off" @go(LogLevel) + log_level?: *"Info" | or(_firecrackerConsoleLogLevelWord) @go(LogLevel) metrics_path?: string @go(MetricsPath) } @@ -261,7 +261,7 @@ package schema // manage says how charly treats an existing /etc/containerd/config.toml: // `merge` (default) keeps every setting it does not model, `full` takes the // file over, `adopt` changes nothing and only reports. - manage?: *"merge" | "full" | "adopt" @go(Manage) + manage?: *"merge" | or(_manageWord) @go(Manage) version?: int & >=2 & <=3 @go(Version,type=int) root?: string @go(Root) state?: string @go(State) @@ -325,7 +325,7 @@ package schema // unauthenticated upstream, so naming a wider address is a deliberate act. bind_address?: *"127.0.0.1:8080" | string @go(BindAddress) metrics?: #FirecrackerFireactionsMetrics @go(Metrics,optional=nillable) - log_level?: *"info" | "debug" | "warn" | "error" @go(LogLevel) + log_level?: *"info" | or(_firecrackerFireactionsLogLevelWord) @go(LogLevel) github?: #FirecrackerFireactionsGitHub @go(GitHub,optional=nillable) pools: [...#FirecrackerPool] @go(Pools) } @@ -365,7 +365,7 @@ package schema // image is a runner guest image: charly's own (a box built into a rootfs // artefact) or an upstream-compatible one. image: string & !="" @go(Image) - image_pull_policy?: *"IfNotPresent" | "Always" | "Never" @go(ImagePullPolicy) + image_pull_policy?: *"IfNotPresent" | or(_imagePullPolicyWord) @go(ImagePullPolicy) // organization is ORGANISATION-scoped: the orchestrator's GitHub calls are // org-level only (a repository-scoped runner is not expressible). organization?: string @go(Organization) @@ -378,3 +378,49 @@ package schema metadata?: {[string]: _} @go(Metadata) rate_limits?: #FirecrackerRateLimits @go(RateLimits,optional=nillable) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #FirecrackerForwarding.policy +_firecrackerForwardingPolicy: {"append-accept": {}, netns: {}, deny: {}} @vocab(spec) +_firecrackerForwardingPolicyWord: [for w, _ in _firecrackerForwardingPolicy {w}] +// #FirecrackerMachine.huge_pages +_hugePages: {"None": {}, "Transparent": {}, "2M": {}} @vocab(spec) +_hugePagesWord: [for w, _ in _hugePages {w}] +// #FirecrackerMachine.cpu_template +_cpuTemplate: { + "None": {} + "C3": {} + "T2": {} + "T2S": {} + "T2CL": {} + "T2A": {} + "V1N1": {} +} @vocab(spec) +_cpuTemplateWord: [for w, _ in _cpuTemplate {w}] +// #FirecrackerConsole.log_level +_firecrackerConsoleLogLevel: { + "Info": {} + "Error": {} + "Warning": {} + "Debug": {} + "Trace": {} + "Off": {} +} @vocab(spec) +_firecrackerConsoleLogLevelWord: [for w, _ in _firecrackerConsoleLogLevel {w}] +// #FirecrackerContainerd.manage +_manage: {merge: {}, full: {}, adopt: {}} @vocab(spec) +_manageWord: [for w, _ in _manage {w}] +// #FirecrackerFireactions.log_level +_firecrackerFireactionsLogLevel: {info: {}, debug: {}, warn: {}, error: {}} @vocab(spec) +_firecrackerFireactionsLogLevelWord: [for w, _ in _firecrackerFireactionsLogLevel {w}] +// #FirecrackerRunner.image_pull_policy +_imagePullPolicy: {"IfNotPresent": {}, "Always": {}, "Never": {}} @vocab(spec) +_imagePullPolicyWord: [for w, _ in _imagePullPolicy {w}] diff --git a/schema/init.cue b/schema/init.cue index a48126b..e0518a1 100644 --- a/schema/init.cue +++ b/schema/init.cue @@ -24,7 +24,7 @@ package schema extends?: string & !="" @go(Extends) // The one mandatory field: the build model. - model: "fragment_assembly" | "file_copy" + model: or(_initModelWord) header_file?: string & !="" @go(HeaderFile) fragment_dir?: string & !="" @go(FragmentDir) @@ -269,3 +269,16 @@ package schema render?: #ServiceRenderInput @go(Render,optional=nillable) config?: #InitResolveInput @go(Config,optional=nillable) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Init.model +_initModel: {fragment_assembly: {}, file_copy: {}} @vocab(spec) +_initModelWord: [for w, _ in _initModel {w}] diff --git a/schema/instrument.cue b/schema/instrument.cue index 5dce861..d537cb2 100644 --- a/schema/instrument.cue +++ b/schema/instrument.cue @@ -22,7 +22,7 @@ package schema // PhasePrepare/PhaseInstall/PhaseCleanup), so the two referencing fields carry // explicit @go(type=) pins (`[]string` / `string`) instead — the scalar-collapse // contract of scalar_aliases.go (hand-written, never generated). -#Phase: ("build" | "live" | "update" | "teardown") @go(-) +#Phase: or(_phaseWord) @go(-) // #PipelineWord — ONE post-run pipeline stage: a single plugin-VERB dispatch, // authored in its sugar form `: ` (the plan's @@ -86,7 +86,7 @@ package schema // origin — where the capture came from: a background SESSION the runner // owned, a plan STEP that recorded inline, or a structured SUB-RUN // (aggregate include). - origin!: "session" | "step" | "sub-run" + origin!: or(_originWord) // verb — the capture verb word that produced the artifacts ("spice", // "record", "wl", "vnc", "transcode", ...). OPEN — it names a dispatched // word, never a closed enum. @@ -135,3 +135,19 @@ package schema kind!: string & !="" validators?: {...} } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Phase +_phase: {build: {}, live: {}, update: {}, teardown: {}} @vocab(spec) +_phaseWord: [for w, _ in _phase {w}] +// #EvidenceRow.origin +_origin: {session: {}, step: {}, "sub-run": {}} @vocab(spec) +_originWord: [for w, _ in _origin {w}] diff --git a/schema/kindcluster.cue b/schema/kindcluster.cue index 9e0f750..87c586f 100644 --- a/schema/kindcluster.cue +++ b/schema/kindcluster.cue @@ -47,7 +47,7 @@ package schema // admission_policy mirrors the kubernetes template's admission policy (read by // candy/plugin-k8sgen to set the pod security context). - admission_policy?: "restricted" | "baseline" | "privileged" @go(AdmissionPolicy) + admission_policy?: or(_kindclusterAdmissionPolicyWord) @go(AdmissionPolicy) // Cluster policy — the SAME defs the `kubernetes:` template uses (R3). storage?: #KubernetesStorage @go(Storage,optional=nillable) @@ -78,3 +78,16 @@ package schema listen_address: *"0.0.0.0" | string @go(ListenAddress) protocol: *"TCP" | "UDP" @go(Protocol) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Kindcluster.admission_policy +_kindclusterAdmissionPolicy: {restricted: {}, baseline: {}, privileged: {}} @vocab(spec) +_kindclusterAdmissionPolicyWord: [for w, _ in _kindclusterAdmissionPolicy {w}] diff --git a/schema/kubernetes.cue b/schema/kubernetes.cue index a4f3331..54895ff 100644 --- a/schema/kubernetes.cue +++ b/schema/kubernetes.cue @@ -22,7 +22,7 @@ package schema // told about. Empty keeps the ambient file that plain `kubectl` reads — what // a charly-created k3s/kind cluster relies on. kubeconfig?: string @go(Kubeconfig) - admission_policy?: "restricted" | "baseline" | "privileged" @go(AdmissionPolicy) + admission_policy?: or(_kubernetesAdmissionPolicyWord) @go(AdmissionPolicy) default_namespace?: *"default" | (string & =~"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") @go(DefaultNamespace) storage?: #KubernetesStorage @@ -65,16 +65,16 @@ package schema class_cheap?: string @go(ClassCheap) class_encrypted?: string @go(ClassEncrypted) class_fast?: string @go(ClassFast) - access_mode_default?: ("ReadWriteOnce" | "ReadWriteMany" | "ReadOnlyMany" | "ReadWriteOncePod") @go(AccessModeDefault) + access_mode_default?: or(_accessModeDefaultWord) @go(AccessModeDefault) } #KubernetesIngressDefaults: { enabled?: bool class?: string cert_issuer?: string @go(CertIssuer) - path_type_default?: ("Prefix" | "Exact" | "ImplementationSpecific") @go(PathTypeDefault) + path_type_default?: or(_pathTypeDefaultWord) @go(PathTypeDefault) } #KubernetesImagesDefaults: { - pull_policy?: ("IfNotPresent" | "Always" | "Never") @go(PullPolicy) + pull_policy?: or(_kubernetesImagesDefaultsPullPolicyWord) @go(PullPolicy) pull_secrets?: [...string] @go(PullSecrets) } #KubernetesPodDefaults: { @@ -88,3 +88,34 @@ package schema labels?: [string]: string annotations?: [string]: string } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Kubernetes.admission_policy +_kubernetesAdmissionPolicy: {restricted: {}, baseline: {}, privileged: {}} @vocab(spec) +_kubernetesAdmissionPolicyWord: [for w, _ in _kubernetesAdmissionPolicy {w}] +// #KubernetesStorage.access_mode_default +_accessModeDefault: { + "ReadWriteOnce": {} + "ReadWriteMany": {} + "ReadOnlyMany": {} + "ReadWriteOncePod": {} +} @vocab(spec) +_accessModeDefaultWord: [for w, _ in _accessModeDefault {w}] +// #KubernetesIngressDefaults.path_type_default +_pathTypeDefault: {"Prefix": {}, "Exact": {}, "ImplementationSpecific": {}} @vocab(spec) +_pathTypeDefaultWord: [for w, _ in _pathTypeDefault {w}] +// #KubernetesImagesDefaults.pull_policy +_kubernetesImagesDefaultsPullPolicy: { + "IfNotPresent": {} + "Always": {} + "Never": {} +} @vocab(spec) +_kubernetesImagesDefaultsPullPolicyWord: [for w, _ in _kubernetesImagesDefaultsPullPolicy {w}] diff --git a/schema/kubevirt.cue b/schema/kubevirt.cue index b43fb87..4fdafde 100644 --- a/schema/kubevirt.cue +++ b/schema/kubevirt.cue @@ -67,8 +67,8 @@ package schema instancetype?: string preference?: string - run_strategy?: *"Always" | "Halted" | "Manual" | "RerunOnFailure" | "Once" @go(RunStrategy) - eviction_strategy?: "None" | "LiveMigrate" | "LiveMigrateIfPossible" | "External" @go(EvictionStrategy) + run_strategy?: *"Always" | or(_runStrategyWord) @go(RunStrategy) + eviction_strategy?: or(_evictionStrategyWord) @go(EvictionStrategy) termination_grace_period_seconds?: int & >=0 @go(TerminationGracePeriodSeconds,type=int) node_selector?: {[string]: string} @go(NodeSelector) @@ -87,7 +87,7 @@ package schema // (its ai.opencharly.* labels carry the VM metadata contract) or any // KubeVirt containerDisk image. image: string & !="" - pull_policy?: "Always" | "IfNotPresent" | "Never" @go(PullPolicy) + pull_policy?: or(_kubevirtSourcePullPolicyWord) @go(PullPolicy) pull_secret?: string @go(PullSecret) // disk_path_in_image points KubeVirt at the disk's location INSIDE the image // when it is NOT the scanned default (/disk/disk.img) — e.g. a charly VM box @@ -106,7 +106,7 @@ package schema data_volume: {[string]: _} size: string & !="" storage_class?: string @go(StorageClass) - content_type?: "kubevirt" | "archive" @go(ContentType) + content_type?: or(_contentTypeWord) @go(ContentType) image?: _|_ pull_policy?: _|_ pull_secret?: _|_ @@ -173,8 +173,8 @@ package schema } #KubevirtInput: { - type: "tablet" | "mouse" | "keyboard" - bus?: "virtio" | "usb" | "ps2" + type: or(_kubevirtInputTypeWord) + bus?: or(_busWord) } #KubevirtHostDevice: { @@ -186,8 +186,8 @@ package schema } #KubevirtWatchdog: { - model: "i6300esb" | "diag288" - action?: "poweroff" | "reset" | "shutdown" + model: or(_kubevirtWatchdogModelWord) + action?: or(_kubevirtWatchdogActionWord) } #KubevirtGPU: { @@ -197,8 +197,8 @@ package schema #KubevirtNetwork: { // pod is the default KubeVirt network (a masquerade interface over the pod net). - interface?: *"masquerade" | "bridge" | "pod" @go(Interface) - model?: *"virtio" | "e1000" | "rtl8139" @go(Model) + interface?: *"masquerade" | or(_interfaceWord) @go(Interface) + model?: *"virtio" | or(_kubevirtNetworkModelWord) @go(Model) // ports declare named ports on the interface (informational for a Service). ports?: [...#KubevirtPort] } @@ -222,3 +222,54 @@ package schema // snapshot_class names the VolumeSnapshotClass; empty → the cluster default. snapshot_class?: string @go(SnapshotClass) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #KubeVirt.run_strategy +_runStrategy: { + "Always": {} + "Halted": {} + "Manual": {} + "RerunOnFailure": {} + "Once": {} +} @vocab(spec) +_runStrategyWord: [for w, _ in _runStrategy {w}] +// #KubeVirt.eviction_strategy +_evictionStrategy: { + "None": {} + "LiveMigrate": {} + "LiveMigrateIfPossible": {} + "External": {} +} @vocab(spec) +_evictionStrategyWord: [for w, _ in _evictionStrategy {w}] +// #KubevirtSource.pull_policy +_kubevirtSourcePullPolicy: {"Always": {}, "IfNotPresent": {}, "Never": {}} @vocab(spec) +_kubevirtSourcePullPolicyWord: [for w, _ in _kubevirtSourcePullPolicy {w}] +// #KubevirtSource.content_type +_contentType: {kubevirt: {}, archive: {}} @vocab(spec) +_contentTypeWord: [for w, _ in _contentType {w}] +// #KubevirtInput.type +_kubevirtInputType: {tablet: {}, mouse: {}, keyboard: {}} @vocab(spec) +_kubevirtInputTypeWord: [for w, _ in _kubevirtInputType {w}] +// #KubevirtInput.bus +_bus: {virtio: {}, usb: {}, ps2: {}} @vocab(spec) +_busWord: [for w, _ in _bus {w}] +// #KubevirtWatchdog.model +_kubevirtWatchdogModel: {i6300esb: {}, diag288: {}} @vocab(spec) +_kubevirtWatchdogModelWord: [for w, _ in _kubevirtWatchdogModel {w}] +// #KubevirtWatchdog.action +_kubevirtWatchdogAction: {poweroff: {}, reset: {}, shutdown: {}} @vocab(spec) +_kubevirtWatchdogActionWord: [for w, _ in _kubevirtWatchdogAction {w}] +// #KubevirtNetwork.interface +_interface: {masquerade: {}, bridge: {}, pod: {}} @vocab(spec) +_interfaceWord: [for w, _ in _interface {w}] +// #KubevirtNetwork.model +_kubevirtNetworkModel: {virtio: {}, e1000: {}, rtl8139: {}} @vocab(spec) +_kubevirtNetworkModelWord: [for w, _ in _kubevirtNetworkModel {w}] diff --git a/schema/llm.cue b/schema/llm.cue index fed26c5..499f41e 100644 --- a/schema/llm.cue +++ b/schema/llm.cue @@ -92,7 +92,7 @@ package schema response_format?: #LLMResponseFormat // reasoning_effort: thinking control for reasoning models ("none" disables // thinking where the server honours it). - reasoning_effort?: "high" | "medium" | "low" | "none" @go(Reasoning_effort,type=string) + reasoning_effort?: or(_reasoningEffortWord) @go(Reasoning_effort,type=string) // reasoning: the object form of the same control (ollama accepts either). reasoning?: #LLMReasoning // stream_options: streaming response options. @@ -119,7 +119,7 @@ package schema // #LLMResponseFormat — the structured-output contract. type "json_schema" requires // the json_schema block; the schema field is the JSON Schema itself. #LLMResponseFormat: close({ - type: "text" | "json_object" | "json_schema" @go(Type,type=string) + type: or(_llmResponseFormatTypeWord) @go(Type,type=string) json_schema?: close({ name: string description?: string @@ -130,7 +130,7 @@ package schema // #LLMReasoning — the object form of the reasoning/thinking control. #LLMReasoning: close({ - effort?: "high" | "medium" | "low" | "none" @go(Effort,type=string) + effort?: or(_effortWord) @go(Effort,type=string) }) // #LLMStreamOptions — streaming response options. @@ -142,3 +142,22 @@ package schema #LLMNamedToolChoice: close({ function: close({name: string}) }) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #LLMParams.reasoning_effort +_reasoningEffort: {high: {}, medium: {}, low: {}, none: {}} @vocab(spec) +_reasoningEffortWord: [for w, _ in _reasoningEffort {w}] +// #LLMResponseFormat.type +_llmResponseFormatType: {text: {}, json_object: {}, json_schema: {}} @vocab(spec) +_llmResponseFormatTypeWord: [for w, _ in _llmResponseFormatType {w}] +// #LLMReasoning.effort +_effort: {high: {}, medium: {}, low: {}, none: {}} @vocab(spec) +_effortWord: [for w, _ in _effort {w}] diff --git a/schema/marketplace.cue b/schema/marketplace.cue index 06ae692..4c4cb0f 100644 --- a/schema/marketplace.cue +++ b/schema/marketplace.cue @@ -22,11 +22,11 @@ package schema settings?: #MarketplaceSettings // the harness wiring data (settings.json plugin-owned keys) }) #MarketplaceFamily: close({ - category?: *"images" | "commands" | "kind" | "development" // the README four-bucket classification + category?: *"images" | or(_marketplaceFamilyCategoryWord) // the README four-bucket classification description?: string & !="" // plugin.json + marketplace.json description keywords?: [...(string & !="")] // marketplace.json keywords version?: string & =~"^[0-9]+[.][0-9]+[.][0-9]+$" // plugin.json version (default: family candy CalVer) - profiles?: [...("developer" | "user" | "container")] // profiles.json membership + profiles?: [...or(_profilesWord)] // profiles.json membership mcp_servers?: [...#MarketplaceMCPServer] @go(McpServers,type=[]MarketplaceMCPServer) // plugins//.mcp.json entries }) #MarketplaceMCPServer: close({ @@ -45,3 +45,19 @@ package schema // hooks — the hook entity names to wire into settings.json hooks.. hooks?: [...string] @go(Hooks,type=[]string) }) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #MarketplaceFamily.category +_marketplaceFamilyCategory: {images: {}, commands: {}, kind: {}, development: {}} @vocab(spec) +_marketplaceFamilyCategoryWord: [for w, _ in _marketplaceFamilyCategory {w}] +// #MarketplaceFamily.profiles +_profiles: {developer: {}, user: {}, container: {}} @vocab(spec) +_profilesWord: [for w, _ in _profiles {w}] diff --git a/schema/node.cue b/schema/node.cue index 7e66bf6..6aef16e 100644 --- a/schema/node.cue +++ b/schema/node.cue @@ -51,7 +51,7 @@ package schema // the ProjectLoader seam) and threaded to the plugin via // op.Env (F5); the parser gate admits them because resourceKindSet has them. candy is NOT a // resource kind (it nests no deploy members — it is the box⊻layer factory). -#ResourceKind: ("pod" | "vm" | "kubernetes" | "local" | "android" | "kindcluster" | "kubevirt" | "firecracker") @go(-) +#ResourceKind: or(_resourceKindWord) @go(-) // --------------------------------------------------------------------------- // Per-kind node VALUES — the COMPLETE per-kind def, authored INLINE: the kind @@ -221,3 +221,25 @@ package schema // candy/plugin-gpu/data.yml and candy/plugin-doctor/data.yml. {[!~"^(repo|import|discover|defaults|provides|cache|ledger|system|providers|compiled_plugins|context_ignore_baseline|ovmf_paths|ovmf_distro_aliases)$"]: #Node} }) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #ResourceKind +_resourceKind: { + pod: {} + vm: {} + kubernetes: {} + local: {} + android: {} + kindcluster: {} + kubevirt: {} + firecracker: {} +} @vocab(spec) +_resourceKindWord: [for w, _ in _resourceKind {w}] diff --git a/schema/oci.cue b/schema/oci.cue index bec5352..9998472 100644 --- a/schema/oci.cue +++ b/schema/oci.cue @@ -150,7 +150,7 @@ package schema variant?: string // libc: the C library the binary links against. See the header: image-spec // cannot express this, and a musl host must refuse a glibc binary by name. - libc!: "glibc" | "musl" + libc!: or(_libcWord) } // protocol: the transport family the binary was built against. OPTIONAL DIAGNOSTIC, not // a gate: DESIGN §8 says "There is no version negotiation. Compatibility is the @@ -207,3 +207,16 @@ package schema // names" without re-deriving which layer is which). Recomputed from the manifest. layers!: {[string]: string} }) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #PluginArtifactMetadata.platform.libc +_libc: {glibc: {}, musl: {}} @vocab(spec) +_libcWord: [for w, _ in _libc {w}] diff --git a/schema/pipeline.cue b/schema/pipeline.cue index 696efb3..3136010 100644 --- a/schema/pipeline.cue +++ b/schema/pipeline.cue @@ -61,7 +61,7 @@ package schema cwd?: string & !="" stdin?: string & !="" timeout_ms?: int & >0 @go(TimeoutMs) - on_error?: "fail" | "continue" | "skip_rest" @go(OnError) + on_error?: or(_onErrorWord) @go(OnError) retry?: #PipelineRetry } @@ -233,3 +233,16 @@ package schema "agent-runtime": "plan: — the agent-run:/agent-check: intents" "terminal": "charly: [shell|tui, …] reaches the terminal providers" } @go(-) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #PipelineFlow.on_error +_onError: {fail: {}, continue: {}, skip_rest: {}} @vocab(spec) +_onErrorWord: [for w, _ in _onError {w}] diff --git a/schema/seam.cue b/schema/seam.cue index 11cd378..74f0242 100644 --- a/schema/seam.cue +++ b/schema/seam.cue @@ -418,7 +418,7 @@ package schema } #PodLifecyclePlan: { - mode!: "quadlet" | "direct" @go(Mode) // runQuadlet (systemctl) vs runDirect (podman run) + mode!: or(_podLifecyclePlanModeWord) @go(Mode) // runQuadlet (systemctl) vs runDirect (podman run) svc_name?: string @go(SvcName) // serviceNameInstance — quadlet unit container_name!: string @go(ContainerName) // containerNameInstance — engine target run_argv?: [...string] @go(RunArgv) // buildStartArgs output — direct mode `podman run -d` @@ -1384,7 +1384,7 @@ package schema // field per opts type so a NEW deploy verb never needs a NEW CUE field, only a new decode // branch plugin-side. #DeployTargetDispatchRequest: { - op!: "add" | "update" | "del" | "start" | "stop" | "status" | "logs" | "shell" | "attach" | "rebuild" + op!: or(_deployTargetDispatchRequestOpWord) name!: string @go(Name) word!: string @go(Word) has_lifecycle?: bool @go(HasLifecycle) @@ -1595,3 +1595,30 @@ package schema #BoxFetchResolveReply: { path?: string @go(Path) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #PodLifecyclePlan.mode +_podLifecyclePlanMode: {quadlet: {}, direct: {}} @vocab(spec) +_podLifecyclePlanModeWord: [for w, _ in _podLifecyclePlanMode {w}] +// #DeployTargetDispatchRequest.op +_deployTargetDispatchRequestOp: { + add: {} + update: {} + del: {} + start: {} + stop: {} + status: {} + logs: {} + shell: {} + attach: {} + rebuild: {} +} @vocab(spec) +_deployTargetDispatchRequestOpWord: [for w, _ in _deployTargetDispatchRequestOp {w}] diff --git a/schema/skill.cue b/schema/skill.cue index 880c4bb..f8931ee 100644 --- a/schema/skill.cue +++ b/schema/skill.cue @@ -47,9 +47,22 @@ package schema tools?: [...(string & !="")] // agent type: allowed tool set references?: [...#SkillReference] // SKILL.md references/.md split files — authored in the FILE once the body moved triggers?: [...(string & !="")] // R0-dispatcher trigger phrases (generated dispatcher table rows) - category?: *"development" | "commands" | "kind" | "images" + category?: *"development" | or(_skillCategoryWord) }) #SkillReference: close({ name: string & =~"^[a-z0-9][a-z0-9-]*$" // file stem (references/.md) content: string & !="" }) + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #Skill.category +_skillCategory: {development: {}, commands: {}, kind: {}, images: {}} @vocab(spec) +_skillCategoryWord: [for w, _ in _skillCategory {w}] diff --git a/schema/status.cue b/schema/status.cue index 9448beb..0c8430e 100644 --- a/schema/status.cue +++ b/schema/status.cue @@ -36,7 +36,7 @@ package schema // kubevirt was missing here while spec/status_types.go already carried // SubstrateKubevirt; firecracker joins both in the same change, so the CUE enum and // the hand-written Go consts name the same substrates again. -#SubstrateKind: "pod" | "vm" | "kubernetes" | "local" | "android" | "kindcluster" | "kubevirt" | "firecracker" @go(-) +#SubstrateKind: or(_substrateKindWord) @go(-) // #DeploymentStatus — the rendered shape for the table + JSON outputs across every // deployment substrate (pod / vm / kubernetes / local / android / kindcluster / @@ -130,3 +130,25 @@ package schema rows?: [...#DeploymentStatus] @go(Rows) single?: #DeploymentStatus @go(Single) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #SubstrateKind +_substrateKind: { + pod: {} + vm: {} + kubernetes: {} + local: {} + android: {} + kindcluster: {} + kubevirt: {} + firecracker: {} +} @vocab(spec) +_substrateKindWord: [for w, _ in _substrateKind {w}] diff --git a/schema/vm.cue b/schema/vm.cue index 6d5e2ba..fc09eca 100644 --- a/schema/vm.cue +++ b/schema/vm.cue @@ -18,7 +18,7 @@ package schema // resolver validates `--backend` / `CHARLY_VM_BACKEND` / `vm.backend` against the // schema's own word list instead of carrying literals. `auto` is NOT a member: // it is a resolution selector, never a provider word. -#VmBackendName: ("libvirt" | "qemu" | "krun") +#VmBackendName: or(_vmBackendNameWord) // #VmBackend — the authored/persisted VM-backend selector: the provider words // plus "auto", which resolves by detection (today: libvirt, then qemu) and is @@ -60,20 +60,20 @@ package schema disk_size?: string @go(DiskSize) ram?: string cpu?: int & >=1 @go(Cpus,type=int) // yaml key is singular `cpu` (VmSpec.Cpus yaml:"cpu") - machine?: "q35" | "virt" | "i440fx" | "pc" + machine?: or(_machineWord) // firmware is REQUIRED-with-default (not optional): an if-guard can only // reference a field that always resolves to a concrete value, and an // OPTIONAL field — even one carrying a default — errors with "cannot // reference optional field" when absent. Required-with-default materializes // "bios" on omission (matching the Go empty→bios behavior) AND stays // referenceable by the uefi-secure cross-rule below. - firmware: *"bios" | "uefi-insecure" | "uefi-secure" + firmware: *"bios" | or(_vmFirmwareWord) backend: #VmBackend autostart: *false | true // autostart:true requires the libvirt backend (qemu has no persistent daemon). if autostart { - backend: "auto" | "libvirt" + backend: or(_backendWord) } // krun carries the krun backend's knobs. Optional and ignored by the other @@ -90,7 +90,7 @@ package schema // FIELD-named conflict (null for the struct-valued fields, a negated enum for // machine) rather than a bare `_|_`, whose error names only #Vm. if backend == "krun" { - source: #VmSource & {kind: "container_disk" | "bootc"} + source: #VmSource & {kind: or(_vmKindWord)} libvirt?: null autostart: false firmware: "bios" @@ -165,8 +165,8 @@ package schema } | { kind: "bootc" box: string & !="" - transport?: "registry" | "containers-storage" | "oci" | "oci-archive" - rootfs?: "ext4" | "xfs" | "btrfs" + transport?: or(_vmSourceTransportWord) + rootfs?: or(_rootfsWord) root_size?: string kernel_args?: string url?: _|_ @@ -186,7 +186,7 @@ package schema kind: "imported" libvirt_name: string & !="" disk_path: string & !="" - disk_format: "qcow2" | "raw" + disk_format: or(_diskFormatWord) adopted_at?: string last_synced_at?: string url?: _|_ @@ -201,7 +201,7 @@ package schema // (candy_select.go), so an unrecognized id is a typo, not an extension point. distro: #DistroID builder_image?: string - rootfs?: "ext4" | "xfs" | "btrfs" + rootfs?: or(_rootfsWord) root_size?: string kernel_args?: string package?: [...string] @@ -352,7 +352,7 @@ package schema #VmNetwork: { model?: string - mode: *"user" | "bridge" | "nat" | "network" + mode: *"user" | or(_vmNetworkModeWord) bridge?: string mac?: string @go(MAC) // Each entry is ":". The host side may be a fixed port OR the @@ -377,8 +377,8 @@ package schema } & ({port_auto?: false} | {port?: int & <=0}) @go(-) // gengotypes: hand VmSsh (spec/union_types.go) #VmKeyInjection: { - smbios?: "auto" | "enabled" | "disabled" @go(SMBIOS) - cloud_init?: "auto" | "enabled" | "disabled" @go(CloudInit) + smbios?: or(_smbiosWord) @go(SMBIOS) + cloud_init?: or(_cloudInitWord) @go(CloudInit) } #VmSnapshot: { @@ -441,7 +441,7 @@ package schema // VmCharlyInstall has ONLY `strategy` (the vm-spec skill's url/checksum are // STALE — the Go struct dropped them). auto: scp host binary post-boot; // scp: explicit form; skip: user-managed. - strategy?: *"auto" | "scp" | "skip" + strategy?: *"auto" | or(_strategyWord) } // --------------------------------------------------------------------------- @@ -545,7 +545,7 @@ package schema // mode is REQUIRED-with-default (renderer default host-passthrough) so the // custom⇒model if-guard below can reference it (optional fields error when // absent). #LibvirtCPU only instantiates when `cpu:` is present. - mode: *"host-passthrough" | "host-model" | "custom" + mode: *"host-passthrough" | or(_libvirtCPUModeWord) model?: string check?: string // none|partial|full — no Go validator, kept plain migratable?: string // on|off — no Go validator, kept plain @@ -567,7 +567,7 @@ package schema } #LibvirtCPUFeature: { - policy?: "force" | "require" | "optional" | "disable" | "forbid" + policy?: or(_libvirtCPUFeaturePolicyWord) name: string & !="" // LibvirtCPUFeature.Name yaml:"name" — required } @@ -585,7 +585,7 @@ package schema } #LibvirtClock: { - offset?: "utc" | "localtime" | "timezone" | "variable" | "absolute" + offset?: or(_offsetWord) timezone?: string adjustment?: string basis?: string @@ -752,7 +752,7 @@ package schema } #LibvirtGraphics: { - type: "vnc" | "spice" | "rdp" | "sdl" | "egl-headless" | "dbus" // required + type: or(_libvirtGraphicsTypeWord) // required port?: int @go(,type=int) autoport?: string @go(AutoPort) listen?: #LibvirtListen @go(Listen,type=LibvirtGraphicsListeners,optional=nillable) @@ -813,8 +813,7 @@ package schema // The vocabulary is closed because libvirt's own RNG closes it (domaincommon.rng, // the type='virtio' group): any other value is rejected at DEFINE time with an // error that blames , not the attribute. Rejecting it here names the field. - device?: "virtio-vga" | "virtio-vga-gl" | "virtio-gpu" | "virtio-gpu-gl" | - "vhost-user-vga" | "vhost-user-gpu" + device?: or(_deviceWord) ram?: int @go(,type=int) vram?: int @go(VRAM,type=int) @@ -861,8 +860,8 @@ package schema // Both enums are closed by libvirt's RNG (domaincommon.rng, the video // element). A name like "qxl" — the plausible guess, since it is a valid video // MODEL — is not a valid driver name and fails at define time. - name?: "qemu" | "vhostuser" - vgaconf?: "io" | "on" | "off" @go(VGAConf) + name?: or(_nameWord) + vgaconf?: or(_vgaconfWord) @go(VGAConf) // The virtioOptions toggles. libvirt spells these on/off, where the video model's // own attributes beside them are yes/no — the renderer keeps the two apart. @@ -900,9 +899,9 @@ package schema #LibvirtPCIHex: string & =~"^(0[xX][0-9a-fA-F]+|[0-9]+)$" #LibvirtHostdev: { - type: "pci" | "usb" | "scsi" | "mdev" // required + type: or(_libvirtHostdevTypeWord) // required mode?: string - managed?: "yes" | "no" + managed?: or(_managedWord) source: {[string]: string} // LibvirtHostdev.Source yaml:"source" — required typed map rom?: {[string]: string} driver?: {[string]: string} @@ -921,8 +920,8 @@ package schema #LibvirtFilesystem: { type?: string - driver?: "virtiofs" | "9p" | "path" - accessmode?: "passthrough" | "mapped" | "squash" @go(AccessMode) + driver?: or(_driverWord) + accessmode?: or(_accessmodeWord) @go(AccessMode) source: string & !="" // required (host path) target: string & !="" // required (guest mount tag) readonly?: bool @go(,type=*bool) @@ -993,7 +992,7 @@ package schema } #LibvirtLaunchSecurity: { - type?: "sev" | "sev-es" | "sev-snp" | "tdx" + type?: or(_libvirtLaunchSecurityTypeWord) cbitpos?: int @go(CBitPos,type=int) reduced_phys_bits?: int @go(ReducedPhysBits,type=int) policy?: string @@ -1044,3 +1043,110 @@ package schema snapshot?: [...#VmSnapshot] @go(Snapshots) raw?: bytes @go(Raw,type=RawBody) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #VmBackendName +_vmBackendName: {libvirt: {}, qemu: {}, krun: {}} @vocab(spec) +_vmBackendNameWord: [for w, _ in _vmBackendName {w}] +// #Vm.machine +_machine: {q35: {}, virt: {}, i440fx: {}, pc: {}} @vocab(spec) +_machineWord: [for w, _ in _machine {w}] +// #Vm.firmware +_vmFirmware: {bios: {}, "uefi-insecure": {}, "uefi-secure": {}} @vocab(spec) +_vmFirmwareWord: [for w, _ in _vmFirmware {w}] +// #Vm.backend +_backend: {auto: {}, libvirt: {}} @vocab(spec) +_backendWord: [for w, _ in _backend {w}] +// #Vm.source.kind +_vmKind: {container_disk: {}, bootc: {}} @vocab(spec) +_vmKindWord: [for w, _ in _vmKind {w}] +// #VmSource.transport +_vmSourceTransport: { + registry: {} + "containers-storage": {} + oci: {} + "oci-archive": {} +} @vocab(spec) +_vmSourceTransportWord: [for w, _ in _vmSourceTransport {w}] +// #VmSource.rootfs +_rootfs: {ext4: {}, xfs: {}, btrfs: {}} @vocab(spec) +_rootfsWord: [for w, _ in _rootfs {w}] +// #VmSource.disk_format +_diskFormat: {qcow2: {}, raw: {}} @vocab(spec) +_diskFormatWord: [for w, _ in _diskFormat {w}] +// #VmNetwork.mode +_vmNetworkMode: {user: {}, bridge: {}, nat: {}, network: {}} @vocab(spec) +_vmNetworkModeWord: [for w, _ in _vmNetworkMode {w}] +// #VmKeyInjection.smbios +_smbios: {auto: {}, enabled: {}, disabled: {}} @vocab(spec) +_smbiosWord: [for w, _ in _smbios {w}] +// #VmKeyInjection.cloud_init +_cloudInit: {auto: {}, enabled: {}, disabled: {}} @vocab(spec) +_cloudInitWord: [for w, _ in _cloudInit {w}] +// #VmCharlyInstall.strategy +_strategy: {auto: {}, scp: {}, skip: {}} @vocab(spec) +_strategyWord: [for w, _ in _strategy {w}] +// #LibvirtCPU.mode +_libvirtCPUMode: {"host-passthrough": {}, "host-model": {}, custom: {}} @vocab(spec) +_libvirtCPUModeWord: [for w, _ in _libvirtCPUMode {w}] +// #LibvirtCPUFeature.policy +_libvirtCPUFeaturePolicy: { + force: {} + require: {} + optional: {} + disable: {} + forbid: {} +} @vocab(spec) +_libvirtCPUFeaturePolicyWord: [for w, _ in _libvirtCPUFeaturePolicy {w}] +// #LibvirtClock.offset +_offset: {utc: {}, localtime: {}, timezone: {}, variable: {}, absolute: {}} @vocab(spec) +_offsetWord: [for w, _ in _offset {w}] +// #LibvirtGraphics.type +_libvirtGraphicsType: { + vnc: {} + spice: {} + rdp: {} + sdl: {} + "egl-headless": {} + dbus: {} +} @vocab(spec) +_libvirtGraphicsTypeWord: [for w, _ in _libvirtGraphicsType {w}] +// #LibvirtVideo.device +_device: { + "virtio-vga": {} + "virtio-vga-gl": {} + "virtio-gpu": {} + "virtio-gpu-gl": {} + "vhost-user-vga": {} + "vhost-user-gpu": {} +} @vocab(spec) +_deviceWord: [for w, _ in _device {w}] +// #LibvirtVideoDriver.name +_name: {qemu: {}, vhostuser: {}} @vocab(spec) +_nameWord: [for w, _ in _name {w}] +// #LibvirtVideoDriver.vgaconf +_vgaconf: {io: {}, on: {}, off: {}} @vocab(spec) +_vgaconfWord: [for w, _ in _vgaconf {w}] +// #LibvirtHostdev.type +_libvirtHostdevType: {pci: {}, usb: {}, scsi: {}, mdev: {}} @vocab(spec) +_libvirtHostdevTypeWord: [for w, _ in _libvirtHostdevType {w}] +// #LibvirtHostdev.managed +_managed: {yes: {}, no: {}} @vocab(spec) +_managedWord: [for w, _ in _managed {w}] +// #LibvirtFilesystem.driver +_driver: {virtiofs: {}, "9p": {}, path: {}} @vocab(spec) +_driverWord: [for w, _ in _driver {w}] +// #LibvirtFilesystem.accessmode +_accessmode: {passthrough: {}, mapped: {}, squash: {}} @vocab(spec) +_accessmodeWord: [for w, _ in _accessmode {w}] +// #LibvirtLaunchSecurity.type +_libvirtLaunchSecurityType: {sev: {}, "sev-es": {}, "sev-snp": {}, tdx: {}} @vocab(spec) +_libvirtLaunchSecurityTypeWord: [for w, _ in _libvirtLaunchSecurityType {w}] diff --git a/schema/vm_box_metadata.cue b/schema/vm_box_metadata.cue index 8ac2b45..24dfeca 100644 --- a/schema/vm_box_metadata.cue +++ b/schema/vm_box_metadata.cue @@ -31,7 +31,7 @@ package schema // ssh_user — the ssh login user the deploy host connects as. ssh_user?: string @go(SSHUser) // firmware — the guest firmware mode the box boots under. - firmware?: "bios" | "uefi-insecure" | "uefi-secure" @go(Firmware) + firmware?: or(_vmBoxMetadataFirmwareWord) @go(Firmware) // init — the guest init system (systemd | openrc). init?: string @go(Init) // charly_install — how the charly guest agent gets in: auto | scp | skip. @@ -70,7 +70,7 @@ package schema // with it — which the VMM attaches as two separate files (the substrate contract is // `kind: firecracker`, spec#227). #VmBoxSource: { - kind!: "cloud_image" | "bootc" | "clone" | "bootstrap" | "iso" | "container_disk" | "rootfs" | "kernel" @go(Kind) + kind!: or(_vmBoxSourceKindWord) @go(Kind) // clone: the source entity the box's disk was cloned from. from_vm?: string @go(FromVm) // clone: the base snapshot id the clone overlay was created from. @@ -90,7 +90,7 @@ package schema // that works on a host with no loop device, no device-mapper and no privileged // container (umbrella ledger probe P3). // kernel is the uncompressed vmlinux the VMM loads alongside it. - format?: "ext4" | "raw" | "vmlinux" @go(Format) + format?: or(_vmBoxSourceFormatWord) @go(Format) // rootfs: the size the rootfs was built at, in MiB. The deploy needs it to decide // whether the delivered filesystem has to GROW before the guest boots — a truncated // file alone does not grow the filesystem inside it. @@ -122,3 +122,31 @@ package schema // kernel_path — where the kernel file lives in the image. kernel_path?: string @go(KernelPath) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #VmBoxMetadata.firmware +_vmBoxMetadataFirmware: {bios: {}, "uefi-insecure": {}, "uefi-secure": {}} @vocab(spec) +_vmBoxMetadataFirmwareWord: [for w, _ in _vmBoxMetadataFirmware {w}] +// #VmBoxSource.kind +_vmBoxSourceKind: { + cloud_image: {} + bootc: {} + clone: {} + bootstrap: {} + iso: {} + container_disk: {} + rootfs: {} + kernel: {} +} @vocab(spec) +_vmBoxSourceKindWord: [for w, _ in _vmBoxSourceKind {w}] +// #VmBoxSource.format +_vmBoxSourceFormat: {ext4: {}, raw: {}, vmlinux: {}} @vocab(spec) +_vmBoxSourceFormatWord: [for w, _ in _vmBoxSourceFormat {w}] diff --git a/schema/vocab_s3_test.go b/schema/vocab_s3_test.go new file mode 100644 index 0000000..7be7a3a --- /dev/null +++ b/schema/vocab_s3_test.go @@ -0,0 +1,314 @@ +package schema_test + +// vocab_s3_test.go — the §10.1 S3 gate over the shipped schema. +// +// Every closed set of words in this schema is a vocabulary table (`_x: {word: {}, +// ...} @vocab(owner)`), its word list is DERIVED from that table +// (`_xWord: [for w, _ in _x {w}]`), and the enum an author meets is `or(_xWord)` +// (D-PAT-3's form half; DESIGN.md §10.1 S3). That shape is what makes "a new word is +// ONE row" true: the table, the derived list and the field's accepted set cannot +// disagree, and no file restates another's word list. +// +// The gate reads the SOURCE through the CUE parser (so it can see the `or(...)` a +// field cites and the `@vocab` attribute a table carries — neither survives in the +// compiled value) and the VALUE through the runtime's own concatenation +// (schemaconcat.ConcatSchema, the same body charly core's sharedCueSchema compiles), +// so it judges the schema that ships rather than a fixture. +// +// A table whose derived list is checked against itself proves nothing, so the +// mutation half INJECTS a row into every table and asserts the derived list — and +// therefore the enum — gains it. Each mutation first proves it APPLIED: the injected +// word must be absent from the unmutated list and present after. + +import ( + "fmt" + "sort" + "strings" + "testing" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/parser" + + "github.com/opencharly/spec/schema" + "github.com/opencharly/spec/schemaconcat" +) + +// vocabTable is ONE table found in the source: its label, its rows (in declaration +// order), and the attribute the declaration carries. +type vocabTable struct { + name string // the label without the leading "_", e.g. "context" + rows []string + owner string + file string + line int +} + +// vocabSite is ONE `or(_xWord)` citation: which file/line cites it and which table +// name it derives from. +type vocabSite struct { + table string + file string + line int +} + +// concatSchema returns the package-less body the runtime and the generator share. +func concatSchema(t *testing.T) string { + t.Helper() + src, files, err := schemaconcat.ConcatSchema(schema.FS, ".", nil) + if err != nil { + t.Fatalf("concatenating the shipped schema: %v", err) + } + if len(files) == 0 { + t.Fatal("the shipped schema concatenated to zero files") + } + return src +} + +// scanVocabulary walks every shipped schema file and returns the tables it declares +// and the `or(_xWord)` citations it carries. +func scanVocabulary(t *testing.T) (map[string]vocabTable, []vocabSite) { + t.Helper() + entries, err := schema.FS.ReadDir(".") + if err != nil { + t.Fatalf("read schema dir: %v", err) + } + tables := map[string]vocabTable{} + var sites []vocabSite + names := make([]string, 0, len(entries)) + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".cue") { + names = append(names, e.Name()) + } + } + sort.Strings(names) + for _, name := range names { + raw, err := schema.FS.ReadFile(name) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + f, err := parser.ParseFile(name, raw, parser.ParseComments) + if err != nil { + t.Fatalf("parse %s: %v", name, err) + } + ast.Walk(f, func(n ast.Node) bool { + fld, ok := n.(*ast.Field) + if !ok { + return true + } + label := strings.Trim(labelText(t, fld.Label), `"`) + if strings.HasPrefix(label, "_") && !strings.HasSuffix(label, "Word") { + if rows, ok := rowLabels(t, fld.Value); ok { + key := strings.TrimPrefix(label, "_") + if prev, dup := tables[key]; dup { + t.Errorf("%s:%d and %s:%d both declare _%s — a table is one home (D-PAT-2)", + prev.file, prev.line, name, fld.Pos().Line(), key) + } + tables[key] = vocabTable{ + name: key, rows: rows, owner: vocabOwner(fld), file: name, line: fld.Pos().Line(), + } + } + } + // every `or(_xWord)` citation, wherever it appears + ast.Walk(fld.Value, func(c ast.Node) bool { + call, ok := c.(*ast.CallExpr) + if !ok { + return true + } + if fn := labelText(t, call.Fun); fn != "or" || len(call.Args) != 1 { + return true + } + ident := labelText(t, call.Args[0]) + if !strings.HasPrefix(ident, "_") || !strings.HasSuffix(ident, "Word") { + return true + } + sites = append(sites, vocabSite{ + table: strings.TrimSuffix(strings.TrimPrefix(ident, "_"), "Word"), + file: name, line: call.Pos().Line(), + }) + return true + }, nil) + return true + }, nil) + } + return tables, sites +} + +// labelText renders a label/expression's identifier text. +func labelText(t *testing.T, n ast.Node) string { + t.Helper() + switch x := n.(type) { + case *ast.Ident: + return x.Name + case *ast.BasicLit: + return x.Value + case *ast.SelectorExpr: + return labelText(t, x.X) + "." + labelText(t, x.Sel) + } + return "" +} + +// rowLabels returns the regular field labels of a table literal, or ok=false when +// the value is not a struct literal of rows. +func rowLabels(t *testing.T, e ast.Expr) ([]string, bool) { + t.Helper() + lit, ok := e.(*ast.StructLit) + if !ok { + return nil, false + } + var out []string + for _, el := range lit.Elts { + fld, ok := el.(*ast.Field) + if !ok { + return nil, false + } + out = append(out, strings.Trim(labelText(t, fld.Label), `"`)) + } + if len(out) == 0 { + return nil, false + } + return out, true +} + +// vocabOwner returns the value of the declaration's `@vocab(...)` attribute. The +// attribute is carried as TEXT (`@vocab(spec)`), so it is read by splitting it rather +// than by walking a body the AST does not expose. +func vocabOwner(fld *ast.Field) string { + for _, a := range fld.Attrs { + key, body := a.Split() + if key != "vocab" { + continue + } + return strings.TrimSpace(body) + } + return "" +} + +// compiledSchema compiles the concatenation — the runtime's own composition. +func compiledSchema(t *testing.T, src string) cue.Value { + t.Helper() + v := cuecontext.New().CompileString(src) + if err := v.Err(); err != nil { + t.Fatalf("the shipped schema does not compile: %v", err) + } + return v +} + +func decodeLists(t *testing.T, tables map[string]vocabTable, body string) map[string][]string { + t.Helper() + v := compiledSchema(t, body+harnessFor(tables)) + var out struct { + Words map[string][]string `json:"words"` + } + if err := v.LookupPath(cue.ParsePath("#TestVocabulary")).Decode(&out); err != nil { + t.Fatalf("lift the derived word lists: %v", err) + } + return out.Words +} + +// TestEveryTableDerivesItsWordList asserts the S3 shape holds everywhere: every +// table carries an @vocab owner, every table's derived list IS its row set in +// declaration order, and every table has more than one row. +func TestEveryTableDerivesItsWordList(t *testing.T) { + tables, sites := scanVocabulary(t) + if len(tables) == 0 { + t.Fatal("no vocabulary tables found — the scan would pass over nothing") + } + if len(sites) == 0 { + t.Fatal("no `or(_xWord)` citation found — the scan would pass over nothing") + } + lists := decodeLists(t, tables, concatSchema(t)) + for name, tbl := range tables { + if tbl.owner != "spec" { + t.Errorf("%s:%d: table _%s carries @vocab(%q); every set in this module is compiled into the `spec` contract module, so the owner is `spec`", + tbl.file, tbl.line, name, tbl.owner) + } + if len(tbl.rows) < 2 { + t.Errorf("%s:%d: table _%s has %d row(s) — a one-word table is not a closed set of words, it is a constant", + tbl.file, tbl.line, name, len(tbl.rows)) + } + got, ok := lists[name] + if !ok { + t.Errorf("table _%s has no derived _%sWord list in the compiled schema", name, name) + continue + } + if strings.Join(got, "\x00") != strings.Join(tbl.rows, "\x00") { + t.Errorf("_%sWord is %v but the _%s table's rows are %v — the list is restated, not derived", + name, got, name, tbl.rows) + } + } +} + +// TestEveryTableIsCitedAndEveryCitationResolves asserts the wiring is one-to-one: a +// table nothing cites is dead (its rows constrain nothing), and a citation of a table +// that does not exist is a compile error the next reader would have to find. +func TestEveryTableIsCitedAndEveryCitationResolves(t *testing.T) { + tables, sites := scanVocabulary(t) + cited := map[string]bool{} + for _, s := range sites { + cited[s.table] = true + if _, ok := tables[s.table]; !ok { + t.Errorf("%s:%d cites _%sWord, which no file declares", s.file, s.line, s.table) + } + } + for name, tbl := range tables { + if !cited[name] { + t.Errorf("%s:%d: table _%s is cited by no field or definition — its rows constrain nothing", + tbl.file, tbl.line, name) + } + } +} + +// TestInjectedRowReachesTheDerivedList mutates EVERY table: a row appended to the +// table must appear in the derived list, which is the only way a runtime test can see +// that the list DERIVES rather than restates. Equality cannot see it — a hand-written +// list has exactly the table's words — so each mutation also proves it APPLIED (the +// injected word is absent before and present after). +func TestInjectedRowReachesTheDerivedList(t *testing.T) { + const injected = "probe-injected-word" + tables, _ := scanVocabulary(t) + base := concatSchema(t) + before := decodeLists(t, tables, base) + keys := make([]string, 0, len(tables)) + for k := range tables { + keys = append(keys, k) + } + sort.Strings(keys) + for _, name := range keys { + if contains(before[name], injected) { + t.Fatalf("the unmutated _%sWord already carries %q — the mutation would be unprovable here", name, injected) + } + src := base + fmt.Sprintf("\n_%s: {%q: {}}\n", name, injected) + after := decodeLists(t, tables, src) + if !contains(after[name], injected) { + t.Errorf("a row was injected into _%s and its derived list did not gain %q — _%sWord does not derive from the table", + name, injected, name) + } + } +} + +func contains(hay []string, needle string) bool { + for _, h := range hay { + if h == needle { + return true + } + } + return false +} + +// harnessFor renders the lifting definition for the tables the scan found. +func harnessFor(tables map[string]vocabTable) string { + keys := make([]string, 0, len(tables)) + for k := range tables { + keys = append(keys, k) + } + sort.Strings(keys) + var b strings.Builder + b.WriteString("\n#TestVocabulary: {words: {\n") + for _, k := range keys { + fmt.Fprintf(&b, "\t%s: _%sWord\n", k, k) + } + b.WriteString("}}\n") + return b.String() +} diff --git a/schema/workflow.cue b/schema/workflow.cue index 2ad5918..eff52fd 100644 --- a/schema/workflow.cue +++ b/schema/workflow.cue @@ -108,7 +108,7 @@ package schema // live wire an engine's reply is built from. #WorkflowStepResult: { id!: string & !="" - status!: "ok" | "skipped" | "failed" | "cancelled" | "needs_approval" | "needs_input" + status!: or(_workflowStepResultStatusWord) exit_code?: int @go(ExitCode,type=*int) stdout?: string stderr?: string @@ -161,7 +161,7 @@ package schema // #WorkflowRunReply — lobster envelope v1. `resume_token` is present exactly when // status is needs_approval/needs_input and is wire-compatible with upstream lobster. #WorkflowRunReply: { - status!: "ok" | "needs_approval" | "needs_input" | "cancelled" | "error" + status!: or(_workflowRunReplyStatusWord) output?: string requires_approval?: #WorkflowApproval @go(RequiresApproval) requires_input?: #WorkflowInputRequest @go(RequiresInput) @@ -210,7 +210,7 @@ package schema // #WorkflowScheduleRequest — the systemd-user-timer scheduler surface. #WorkflowScheduleRequest: { - op!: "apply" | "list" | "remove" | "run-now" + op!: or(_workflowScheduleRequestOpWord) pipeline?: string & !="" } @@ -235,7 +235,7 @@ package schema // `github-actions` is the designed-but-unbuilt consumer. #WorkflowEmitRequest: { pipeline!: string & !="" - format!: [...("lobster" | "charly-yml" | "github-actions")] @go(Format) + format!: [...or(_workflowEmitRequestFormatWord)] @go(Format) out_dir?: string & !="" @go(OutDir) } @@ -255,3 +255,38 @@ package schema schedule!: bool emit_formats!: [...string] @go(EmitFormats) } + +// --- vocabulary tables (S3) -------------------------------------------------- +// Every closed set of words is a table keyed by word, `Word` is derived from it, +// and the enum an author meets is `or(Word)` — so the table and the enum cannot +// disagree and a new word is ONE row here. `@vocab` names the owner (§3.1): these +// sets are compiled into THIS contract module, so the owner is `spec`. +// +// Each table is named after its word when that name is unique in the schema, and +// `` when it is not; the comment above it names its site. + +// #WorkflowStepResult.status +_workflowStepResultStatus: { + ok: {} + skipped: {} + failed: {} + cancelled: {} + needs_approval: {} + needs_input: {} +} @vocab(spec) +_workflowStepResultStatusWord: [for w, _ in _workflowStepResultStatus {w}] +// #WorkflowRunReply.status +_workflowRunReplyStatus: { + ok: {} + needs_approval: {} + needs_input: {} + cancelled: {} + error: {} +} @vocab(spec) +_workflowRunReplyStatusWord: [for w, _ in _workflowRunReplyStatus {w}] +// #WorkflowScheduleRequest.op +_workflowScheduleRequestOp: {apply: {}, list: {}, remove: {}, "run-now": {}} @vocab(spec) +_workflowScheduleRequestOpWord: [for w, _ in _workflowScheduleRequestOp {w}] +// #WorkflowEmitRequest.format +_workflowEmitRequestFormat: {lobster: {}, "charly-yml": {}, "github-actions": {}} @vocab(spec) +_workflowEmitRequestFormatWord: [for w, _ in _workflowEmitRequestFormat {w}]