feat(workspace): allow creation without an initial repository

Keep bootstrap, signing identity, config and resource-key allocation atomic with optional Repository intent. Preserve repository-present fingerprints for saved receipts; replay reads current authority at the initial key and never recreates missing assets.

Provide explicit repository-free CLI creation, optional TUI/Web creation inputs, nullable generated contracts, and valid empty catalog/Worker paths. Retain existing authenticated non-owner viewing policy pending the T-717 authorization decision.

Validation: root cargo check; server tests 639 lib + 11 bin; server-api 70 default / 77 typescript; client 52 lib + 2 integration; tui 337; yoi 11 lib + 120 bin. OpenAPI/TS freshness, cargo fmt and diff checks pass. Web check/build, 18 model tests, 268 component tests and Chromium 8 width/theme combinations pass; before/after visual evidence is documented in web/workspace/test/visual-review. Full Web suite has one independently reproduced baseline Ticket source-policy failure.
This commit is contained in:
2026-10-08 18:34:44 +09:00
parent e939cd4e4b
commit 7919616db5
37 changed files with 2335 additions and 354 deletions
+26 -6
View File
@@ -4239,22 +4239,27 @@ pub struct InitialRepositoryIntent {
pub default_ref: Option<String>,
}
/// Request for atomically creating a Workspace and its initial Repository.
/// Request for atomically creating a Workspace with an optional initial Repository.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceCreateRequest {
pub operation_key: String,
pub display_name: String,
pub repository: InitialRepositoryIntent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<InitialRepositoryIntent>,
}
/// Response returned after atomically creating a Workspace and its first Repository.
/// Response returned after atomically creating a Workspace.
///
/// On replay, Workspace/config and the Repository at the originally requested key are
/// current authority, not historical snapshots. Repository is null when creation omitted
/// it or that key has since been removed/renamed; replay never recreates an asset.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceCreateResponse {
pub workspace: WorkspaceSummary,
pub repository: WorkspaceRepositoryRecord,
pub repository: Option<WorkspaceRepositoryRecord>,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub config_revision: u64,
@@ -12204,12 +12209,27 @@ mod tests {
let request = WorkspaceCreateRequest {
operation_key: "workspace-create-1".to_string(),
display_name: "Workspace".to_string(),
repository: InitialRepositoryIntent {
repository: Some(InitialRepositoryIntent {
repository_key: "main".to_string(),
uri: "/srv/repositories/main".to_string(),
default_ref: Some("develop".to_string()),
},
}),
};
for repository in [None, Some(serde_json::Value::Null)] {
let mut wire =
serde_json::json!({"operation_key": "empty", "display_name": "Workspace"});
if let Some(repository) = repository {
wire["repository"] = repository;
}
let decoded: WorkspaceCreateRequest = serde_json::from_value(wire).unwrap();
assert!(decoded.repository.is_none());
assert!(
serde_json::to_value(decoded)
.unwrap()
.get("repository")
.is_none()
);
}
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["operation_key"], "workspace-create-1");
assert_eq!(json["repository"]["uri"], "/srv/repositories/main");