diff --git a/.gitattributes b/.gitattributes index 1e093412c2..a1a2c45814 100644 --- a/.gitattributes +++ b/.gitattributes @@ -46,6 +46,7 @@ packages/spec/spec-changes.json merge=os-regen packages/spec/authorable-surface/** merge=os-regen packages/spec/authorable-surface.base.json merge=os-regen +packages/spec/authorable-defaults/** merge=os-regen packages/spec/json-schema.manifest/** merge=os-regen packages/spec/api-surface/** merge=os-regen packages/spec/api-surface-signatures.json merge=os-regen diff --git a/packages/spec/authorable-defaults/ai.json b/packages/spec/authorable-defaults/ai.json new file mode 100644 index 0000000000..6e00f55851 --- /dev/null +++ b/packages/spec/authorable-defaults/ai.json @@ -0,0 +1,66 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "ai", + "defaults": [ + "ai/AIModelConfig:provider = \"openai\"", + "ai/AIModelConfig:temperature = 0.7", + "ai/Agent:active = true", + "ai/Agent:surface = \"ask\"", + "ai/BlueprintNavItem:type = \"object\"", + "ai/BlueprintView:type = \"list\"", + "ai/CodeContent:language = \"text\"", + "ai/ConversationAnalytics:pruningEvents = 0", + "ai/ConversationAnalytics:summarizationEvents = 0", + "ai/ConversationAnalytics:tokensSavedByPruning = 0", + "ai/ConversationAnalytics:tokensSavedBySummarization = 0", + "ai/ConversationMessage:pinned = false", + "ai/ConversationSession:messages = []", + "ai/ConversationSession:status = \"active\"", + "ai/FileKnowledgeSource:mimeTypes = []", + "ai/ImageContent:detail = \"auto\"", + "ai/KnowledgeDocument:metadata = {}", + "ai/KnowledgeHit:metadata = {}", + "ai/KnowledgeRefreshPolicy:onRecordChange = true", + "ai/KnowledgeSource:adapterConfig = {}", + "ai/KnowledgeSource:aiExposed = true", + "ai/KnowledgeSource:refresh = {}", + "ai/MCPServerRef:active = true", + "ai/MCPToolBinding:approval = \"never\"", + "ai/ModelCapability:codeGeneration = false", + "ai/ModelCapability:functionCalling = false", + "ai/ModelCapability:imageGeneration = false", + "ai/ModelCapability:imageUnderstanding = false", + "ai/ModelCapability:reasoning = false", + "ai/ModelCapability:textEmbedding = false", + "ai/ModelCapability:textGeneration = true", + "ai/ModelConfig:deprecated = false", + "ai/ModelPricing:currency = \"USD\"", + "ai/ModelRegistry:enableAutoFallback = true", + "ai/ModelRegistryEntry:priority = 0", + "ai/ModelRegistryEntry:status = \"active\"", + "ai/ModelSelectionCriteria:excludeDeprecated = true", + "ai/ObjectKnowledgeSource:metadataFields = []", + "ai/PromptTemplate:version = \"1.0.0\"", + "ai/PromptVariable:required = false", + "ai/PromptVariable:type = \"string\"", + "ai/Skill:active = true", + "ai/Skill:surface = \"ask\"", + "ai/SolutionBlueprint:assumptions = []", + "ai/StructuredOutputConfig:maxRetries = 3", + "ai/StructuredOutputConfig:retryOnValidationFailure = true", + "ai/StructuredOutputConfig:strict = false", + "ai/TokenBudgetConfig:bufferPercentage = 0.1", + "ai/TokenBudgetConfig:enableSummarization = false", + "ai/TokenBudgetConfig:reserveTokens = 500", + "ai/TokenBudgetConfig:strategy = \"sliding_window\"", + "ai/TokenBudgetConfig:warnThreshold = 0.8", + "ai/TokenUsageStats:budgetUsed = 0", + "ai/TokenUsageStats:completionTokens = 0", + "ai/TokenUsageStats:messageCount = 0", + "ai/TokenUsageStats:promptTokens = 0", + "ai/TokenUsageStats:prunedMessageCount = 0", + "ai/TokenUsageStats:summarizedMessageCount = 0", + "ai/TokenUsageStats:totalTokens = 0", + "ai/ToolCall:type = \"function\"" + ] +} diff --git a/packages/spec/authorable-defaults/api.json b/packages/spec/authorable-defaults/api.json new file mode 100644 index 0000000000..025d84ad71 --- /dev/null +++ b/packages/spec/authorable-defaults/api.json @@ -0,0 +1,207 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "api", + "defaults": [ + "api/ApiDocumentationConfig:changelog = []", + "api/ApiDocumentationConfig:codeTemplates = []", + "api/ApiDocumentationConfig:enabled = true", + "api/ApiDocumentationConfig:generateOpenApi = true", + "api/ApiDocumentationConfig:generateTestCollections = true", + "api/ApiDocumentationConfig:servers = []", + "api/ApiDocumentationConfig:testCollections = []", + "api/ApiDocumentationConfig:title = \"API Documentation\"", + "api/ApiEndpoint:authRequired = true", + "api/ApiTestCollection:variables = {}", + "api/ApiTestRequest:headers = {}", + "api/ApiTestRequest:queryParams = {}", + "api/ApiTestRequest:variables = {}", + "api/ApiTestingUiConfig:defaultModelsExpandDepth = 1", + "api/ApiTestingUiConfig:displayRequestDuration = true", + "api/ApiTestingUiConfig:enableCors = true", + "api/ApiTestingUiConfig:enableFilter = true", + "api/ApiTestingUiConfig:enableTryItOut = true", + "api/ApiTestingUiConfig:path = \"/api-docs\"", + "api/ApiTestingUiConfig:syntaxHighlighting = true", + "api/ApiTestingUiConfig:theme = \"light\"", + "api/AuthFeaturesConfig:magicLink = false", + "api/AuthFeaturesConfig:organization = false", + "api/AuthFeaturesConfig:passkeys = false", + "api/AuthFeaturesConfig:twoFactor = false", + "api/AuthProviderInfo:type = \"social\"", + "api/BatchConfig:enabled = true", + "api/BatchConfig:maxRecordsPerBatch = 200", + "api/BatchEndpointsConfig:defaultAtomic = true", + "api/BatchEndpointsConfig:enableBatchEndpoint = true", + "api/BatchEndpointsConfig:maxBatchSize = 200", + "api/BatchLoadingStrategy:associationLoading = \"batch\"", + "api/BatchOptions:atomic = false", + "api/BatchOptions:continueOnError = false", + "api/BatchOptions:returnRecords = false", + "api/BulkRequest:allOrNone = true", + "api/CacheInvalidationRequest:cascade = false", + "api/CreateExportJobRequest:encoding = \"utf-8\"", + "api/CreateExportJobRequest:format = \"csv\"", + "api/CreateExportJobRequest:includeHeaders = true", + "api/CreateFlowRequest:runAs = \"user\"", + "api/CreateFlowRequest:status = \"draft\"", + "api/CreateFlowRequest:version = 1", + "api/CreateImportJobRequest:createMissingOptions = false", + "api/CreateImportJobRequest:dryRun = false", + "api/CreateImportJobRequest:runAutomations = false", + "api/CreateImportJobRequest:skipBlankMatchKey = false", + "api/CreateImportJobRequest:treatAsHistorical = false", + "api/CreateImportJobRequest:trimWhitespace = true", + "api/CreateImportJobRequest:writeMode = \"insert\"", + "api/CrossObjectBatchOperation:action = \"create\"", + "api/CrossObjectBatchRequest:atomic = true", + "api/CrudEndpointsConfig:dataPrefix = \"/data\"", + "api/CrudEndpointsConfig:objectParamStyle = \"path\"", + "api/DataLoaderConfig:batchScheduleFn = \"microtask\"", + "api/DataLoaderConfig:cacheEnabled = true", + "api/DataLoaderConfig:coalesceRequests = true", + "api/DataLoaderConfig:maxBatchSize = 100", + "api/DeviceRequestResponse:interval = 2", + "api/DispatcherConfig:fallback = \"404\"", + "api/DispatcherRoute:authRequired = true", + "api/DispatcherRoute:criticality = \"optional\"", + "api/ETag:weak = false", + "api/EnhancedApiError:retryable = false", + "api/ErrorHandlingConfig:enabled = true", + "api/ErrorHandlingConfig:exposeInternalErrors = false", + "api/ErrorHandlingConfig:includeDocumentation = true", + "api/ErrorHandlingConfig:includeRequestId = true", + "api/ErrorHandlingConfig:includeStackTrace = false", + "api/ErrorHandlingConfig:includeTimestamp = true", + "api/ErrorHandlingConfig:logErrors = true", + "api/FieldMappingEntry:required = false", + "api/FieldMappingEntry:transform = \"none\"", + "api/GetMetaItemCachedResponse:notModified = false", + "api/GetPresignedUrlRequest:scope = \"user\"", + "api/ImportRequest:createMissingOptions = false", + "api/ImportRequest:dryRun = false", + "api/ImportRequest:runAutomations = false", + "api/ImportRequest:skipBlankMatchKey = false", + "api/ImportRequest:treatAsHistorical = false", + "api/ImportRequest:trimWhitespace = true", + "api/ImportRequest:writeMode = \"insert\"", + "api/ImportValidationConfig:maxErrors = 100", + "api/ImportValidationConfig:mode = \"strict\"", + "api/ImportValidationConfig:trimWhitespace = true", + "api/InitiateChunkedUploadRequest:chunkSize = 5242880", + "api/InitiateChunkedUploadRequest:scope = \"user\"", + "api/InstallPackageRequest:enableOnInstall = true", + "api/ListExportJobsRequest:limit = 20", + "api/ListFlowsRequest:limit = 50", + "api/ListImportJobsRequest:limit = 50", + "api/ListImportJobsRequest:offset = 0", + "api/ListInstalledPackagesRequest:limit = 50", + "api/ListNotificationsRequest:limit = 20", + "api/ListRunsRequest:limit = 20", + "api/LoginRequest:type = \"email\"", + "api/MetadataBulkRegisterRequest:continueOnError = false", + "api/MetadataBulkRegisterRequest:validate = true", + "api/MetadataCacheResponse:notModified = false", + "api/MetadataEndpointsConfig:cacheTtl = 3600", + "api/MetadataEndpointsConfig:enableCache = true", + "api/MetadataEndpointsConfig:prefix = \"/meta\"", + "api/MetadataExportRequest:format = \"json\"", + "api/MetadataImportRequest:conflictResolution = \"skip\"", + "api/MetadataImportRequest:dryRun = false", + "api/MetadataImportRequest:validate = true", + "api/MetadataOverlaySaveRequest:active = true", + "api/MetadataOverlaySaveRequest:scope = \"platform\"", + "api/MetadataQueryRequest:page = 1", + "api/MetadataQueryRequest:pageSize = 50", + "api/MetadataQueryRequest:sortBy = \"name\"", + "api/MetadataQueryRequest:sortOrder = \"asc\"", + "api/Notification:read = false", + "api/NotificationPreferences:digest = \"none\"", + "api/NotificationPreferences:email = true", + "api/NotificationPreferences:inApp = true", + "api/NotificationPreferences:push = true", + "api/ODataConfig:enabled = true", + "api/ODataConfig:path = \"/odata\"", + "api/ODataQueryAdapter:usePrefix = true", + "api/ODataQueryAdapter:version = \"v4\"", + "api/OpenApiGenerationConfig:apiVersion = \"1.0.0\"", + "api/OpenApiGenerationConfig:enabled = true", + "api/OpenApiGenerationConfig:generateSchemas = true", + "api/OpenApiGenerationConfig:includeExamples = true", + "api/OpenApiGenerationConfig:includeInternal = false", + "api/OpenApiGenerationConfig:outputPath = \"/api/docs/openapi.json\"", + "api/OpenApiGenerationConfig:title = \"ObjectStack API\"", + "api/OpenApiGenerationConfig:uiFramework = \"swagger-ui\"", + "api/OpenApiGenerationConfig:uiPath = \"/api/docs\"", + "api/OpenApiGenerationConfig:version = \"3.0.3\"", + "api/OpenApiSpec:openapi = \"3.0.0\"", + "api/OpenApiSpec:servers = []", + "api/PackageInstallRequest:enableOnInstall = true", + "api/PackageRollbackRequest:rollbackCustomizations = true", + "api/PackageUpgradeRequest:createSnapshot = true", + "api/PackageUpgradeRequest:dryRun = false", + "api/PackageUpgradeRequest:mergeStrategy = \"three-way-merge\"", + "api/PackageUpgradeRequest:skipValidation = false", + "api/QueryOptimizationConfig:enableQueryPlan = false", + "api/RealtimeConfig:enabled = true", + "api/RealtimeConfig:transport = \"websocket\"", + "api/RequestValidationConfig:enabled = true", + "api/RequestValidationConfig:includeFieldErrors = true", + "api/RequestValidationConfig:mode = \"strict\"", + "api/RequestValidationConfig:validateBody = true", + "api/RequestValidationConfig:validateHeaders = false", + "api/RequestValidationConfig:validateParams = true", + "api/RequestValidationConfig:validateQuery = true", + "api/ResponseEnvelopeConfig:enabled = true", + "api/ResponseEnvelopeConfig:includeDuration = false", + "api/ResponseEnvelopeConfig:includeMetadata = true", + "api/ResponseEnvelopeConfig:includeRequestId = true", + "api/ResponseEnvelopeConfig:includeTimestamp = true", + "api/ResponseEnvelopeConfig:includeTraceId = false", + "api/ResponseEnvelopeConfig:skipIfWrapped = true", + "api/RestApiConfig:basePath = \"/api\"", + "api/RestApiConfig:enableBatch = true", + "api/RestApiConfig:enableCrud = true", + "api/RestApiConfig:enableDiscovery = true", + "api/RestApiConfig:enableMetadata = true", + "api/RestApiConfig:enableOpenApi = true", + "api/RestApiConfig:enableProjectScoping = false", + "api/RestApiConfig:enableUi = true", + "api/RestApiConfig:projectResolution = \"auto\"", + "api/RestApiConfig:version = \"v1\"", + "api/RestApiEndpoint:cacheable = false", + "api/RestApiEndpoint:public = false", + "api/RestApiPluginConfig:basePath = \"/api\"", + "api/RestApiPluginConfig:enabled = true", + "api/RestApiPluginConfig:version = \"v1\"", + "api/RestApiRouteRegistration:authRequired = true", + "api/RestQueryAdapter:fieldsParam = \"fields\"", + "api/RestQueryAdapter:filterStyle = \"bracket\"", + "api/RouteDefinition:category = \"api\"", + "api/RouteDefinition:public = false", + "api/RouteGenerationConfig:nameTransform = \"none\"", + "api/RouterConfig:basePath = \"/api\"", + "api/RouterConfig:mounts = {\"ai\":\"/ai\",\"analytics\":\"/analytics\",\"auth\":\"/auth\",\"automation\":\"/automation\",\"data\":\"/data\",\"i18n\":\"/i18n\",\"metadata\":\"/meta\",\"notifications\":\"/notifications\",\"packages\":\"/packages\",\"realtime\":\"/realtime\",\"storage\":\"/storage\",\"ui\":\"/ui\"}", + "api/ScheduleExportRequest:format = \"csv\"", + "api/ScheduledExport:enabled = true", + "api/ScheduledExport:format = \"csv\"", + "api/SessionUser:emailVerified = false", + "api/SessionUser:language = \"en\"", + "api/SessionUser:roles = []", + "api/VersioningConfig:headerName = \"ObjectStack-Version\"", + "api/VersioningConfig:includeInDiscovery = true", + "api/VersioningConfig:queryParamName = \"version\"", + "api/VersioningConfig:strategy = \"urlPath\"", + "api/VersioningConfig:urlPrefix = \"/api\"", + "api/WebSocketConfig:maxReconnectAttempts = 5", + "api/WebSocketConfig:pingInterval = 30000", + "api/WebSocketConfig:reconnect = true", + "api/WebSocketConfig:reconnectInterval = 1000", + "api/WebSocketConfig:timeout = 5000", + "api/WebSocketServerConfig:cursorSharing = false", + "api/WebSocketServerConfig:enabled = false", + "api/WebSocketServerConfig:heartbeatInterval = 30000", + "api/WebSocketServerConfig:path = \"/ws\"", + "api/WebSocketServerConfig:presence = false", + "api/WebSocketServerConfig:reconnectAttempts = 5" + ] +} diff --git a/packages/spec/authorable-defaults/automation.json b/packages/spec/authorable-defaults/automation.json new file mode 100644 index 0000000000..24cb8103a1 --- /dev/null +++ b/packages/spec/authorable-defaults/automation.json @@ -0,0 +1,78 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "automation", + "defaults": [ + "automation/ActionDescriptor:category = \"custom\"", + "automation/ActionDescriptor:deprecated = false", + "automation/ActionDescriptor:handlerContract = \"none\"", + "automation/ActionDescriptor:isAsync = false", + "automation/ActionDescriptor:maturity = \"ga\"", + "automation/ActionDescriptor:needsOutbox = false", + "automation/ActionDescriptor:paradigms = [\"flow\"]", + "automation/ActionDescriptor:source = \"plugin\"", + "automation/ActionDescriptor:supportsCancellation = false", + "automation/ActionDescriptor:supportsPause = false", + "automation/ActionDescriptor:supportsRetry = true", + "automation/ApprovalEscalation:action = \"notify\"", + "automation/ApprovalEscalation:enabled = false", + "automation/ApprovalEscalation:notifySubmitter = true", + "automation/ApprovalNodeConfig:behavior = \"first_response\"", + "automation/ApprovalNodeConfig:lockRecord = true", + "automation/ApprovalNodeConfig:maxRevisions = 3", + "automation/ApprovalNodeConfig:onEmptyApprovers = \"admin_rescue\"", + "automation/BpmnElementMapping:bidirectional = true", + "automation/BpmnExportOptions:includeExtensions = false", + "automation/BpmnExportOptions:includeLayout = true", + "automation/BpmnExportOptions:namespacePrefix = \"bpmn\"", + "automation/BpmnExportOptions:prettyPrint = true", + "automation/BpmnExportOptions:version = \"2.0\"", + "automation/BpmnImportOptions:importDocumentation = true", + "automation/BpmnImportOptions:importLayout = true", + "automation/BpmnImportOptions:unmappedStrategy = \"warn\"", + "automation/BpmnImportOptions:validateAfterImport = true", + "automation/BpmnInteropResult:diagnostics = []", + "automation/BpmnInteropResult:mappedCount = 0", + "automation/BpmnInteropResult:unmappedCount = 0", + "automation/ConcurrencyPolicy:lockScope = \"global\"", + "automation/ConcurrencyPolicy:maxConcurrent = 1", + "automation/ConcurrencyPolicy:onConflict = \"queue\"", + "automation/ETLDestination:writeMode = \"append\"", + "automation/ETLPipeline:enabled = true", + "automation/ETLPipeline:syncMode = \"full\"", + "automation/ETLTransformation:continueOnError = false", + "automation/ExecutionError:retryable = false", + "automation/Flow:runAs = \"user\"", + "automation/Flow:status = \"draft\"", + "automation/Flow:version = 1", + "automation/FlowEdge:isDefault = false", + "automation/FlowEdge:type = \"default\"", + "automation/FlowVariable:isInput = false", + "automation/FlowVariable:isOutput = false", + "automation/LoopConfig:iteratorVariable = \"item\"", + "automation/MapConfig:iteratorVariable = \"item\"", + "automation/NodeExecutorDescriptor:supportsCancellation = false", + "automation/NodeExecutorDescriptor:supportsPause = false", + "automation/NodeExecutorDescriptor:supportsRetry = true", + "automation/RetryPolicy:backoffMs = 1000", + "automation/RetryPolicy:backoffMultiplier = 1", + "automation/RetryPolicy:jitter = false", + "automation/RetryPolicy:maxRetries = 0", + "automation/RetryPolicy:maxRetryDelayMs = 30000", + "automation/ScheduleState:consecutiveFailures = 0", + "automation/ScheduleState:status = \"active\"", + "automation/ScheduleState:timezone = \"UTC\"", + "automation/ScheduleState:totalRuns = 0", + "automation/StateNode:type = \"atomic\"", + "automation/TryCatchConfig:errorVariable = \"$error\"", + "automation/WaitExecutorConfig:conditionMaxPolls = 0", + "automation/WaitExecutorConfig:conditionPollIntervalMs = 30000", + "automation/WaitExecutorConfig:defaultTimeoutBehavior = \"fail\"", + "automation/WaitExecutorConfig:defaultTimeoutMs = 86400000", + "automation/WaitExecutorConfig:maxPausedExecutions = 0", + "automation/WaitExecutorConfig:persistCheckpoints = true", + "automation/WaitExecutorConfig:webhookUrlPattern = \"/api/v1/automation/resume/{executionId}/{nodeId}\"", + "automation/Webhook:isActive = true", + "automation/Webhook:method = \"POST\"", + "automation/Webhook:timeoutMs = 30000" + ] +} diff --git a/packages/spec/authorable-defaults/cloud.json b/packages/spec/authorable-defaults/cloud.json new file mode 100644 index 0000000000..5af6ee244c --- /dev/null +++ b/packages/spec/authorable-defaults/cloud.json @@ -0,0 +1,75 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "cloud", + "defaults": [ + "cloud/AppDiscoveryRequest:limit = 10", + "cloud/AppSubscription:autoRenew = true", + "cloud/ArtifactReference:format = \"tgz\"", + "cloud/CreateListingRequest:pricing = \"free\"", + "cloud/CuratedCollection:published = false", + "cloud/CuratedCollection:sortOrder = 0", + "cloud/Environment:isDefault = false", + "cloud/Environment:isSystem = false", + "cloud/Environment:plan = \"free\"", + "cloud/Environment:status = \"provisioning\"", + "cloud/Environment:visibility = \"private\"", + "cloud/EnvironmentCredential:authorization = \"full_access\"", + "cloud/EnvironmentCredential:status = \"active\"", + "cloud/EnvironmentPackageInstallation:enabled = true", + "cloud/EnvironmentPackageInstallation:status = \"installed\"", + "cloud/EnvironmentPackageInstallation:withSampleData = false", + "cloud/FeaturedListing:active = true", + "cloud/FeaturedListing:priority = 0", + "cloud/InstallPackageToEnvironmentRequest:allowDraft = false", + "cloud/InstallPackageToEnvironmentRequest:enableOnInstall = true", + "cloud/InstallPackageToEnvironmentRequest:withSampleData = false", + "cloud/InstalledAppSummary:enabled = true", + "cloud/InstalledAppSummary:updateAvailable = false", + "cloud/ListInstalledAppsRequest:page = 1", + "cloud/ListInstalledAppsRequest:pageSize = 20", + "cloud/ListInstalledAppsRequest:sortBy = \"name\"", + "cloud/ListReviewsRequest:page = 1", + "cloud/ListReviewsRequest:pageSize = 10", + "cloud/ListReviewsRequest:sortBy = \"newest\"", + "cloud/MarketplaceInstallRequest:enableOnInstall = true", + "cloud/MarketplaceListing:packageType = \"app\"", + "cloud/MarketplaceListing:pricing = \"free\"", + "cloud/MarketplaceListing:status = \"draft\"", + "cloud/MarketplaceSearchRequest:page = 1", + "cloud/MarketplaceSearchRequest:pageSize = 20", + "cloud/MarketplaceSearchRequest:sortBy = \"relevance\"", + "cloud/MarketplaceSearchRequest:sortDirection = \"desc\"", + "cloud/Package:isStarter = false", + "cloud/Package:publisher = \"private\"", + "cloud/Package:visibility = \"private\"", + "cloud/PackageDependency:optional = false", + "cloud/PackageInstallation:status = \"installing\"", + "cloud/PackageManifest:dependencies = []", + "cloud/PackageManifest:metadataTypes = []", + "cloud/PackageManifest:migrations = []", + "cloud/PackageManifest:scope = \"environment\"", + "cloud/PackageSubmission:isNewListing = false", + "cloud/PackageSubmission:status = \"pending\"", + "cloud/PackageVersion:isPreRelease = false", + "cloud/PackageVersion:status = \"draft\"", + "cloud/PolicyAction:resolved = false", + "cloud/ProvisionEnvironmentRequest:visibility = \"private\"", + "cloud/ProvisionOrganizationRequest:defaultEnvironmentDisplayName = \"Production\"", + "cloud/ProvisionTenantRequest:plan = \"free\"", + "cloud/Publisher:verification = \"unverified\"", + "cloud/PublisherProfile:verification = \"unverified\"", + "cloud/PublishingAnalyticsRequest:timeRange = \"last_30d\"", + "cloud/ReviewCriterion:required = true", + "cloud/TenantDatabase:plan = \"free\"", + "cloud/TenantDatabase:status = \"provisioning\"", + "cloud/TenantRoutingConfig:enabled = false", + "cloud/TenantRoutingConfig:identificationSources = [\"subdomain\",\"header\",\"jwt_claim\"]", + "cloud/TenantRoutingConfig:jwtOrganizationClaim = \"organizationId\"", + "cloud/TenantRoutingConfig:tenantHeaderName = \"X-Tenant-ID\"", + "cloud/UpgradeEnvironmentPackageRequest:allowDraft = false", + "cloud/UserReview:helpfulCount = 0", + "cloud/UserReview:moderationStatus = \"pending\"", + "cloud/VersionRelease:channel = \"stable\"", + "cloud/VersionRelease:deprecated = false" + ] +} diff --git a/packages/spec/authorable-defaults/data.json b/packages/spec/authorable-defaults/data.json new file mode 100644 index 0000000000..fc0fdf263c --- /dev/null +++ b/packages/spec/authorable-defaults/data.json @@ -0,0 +1,126 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "data", + "defaults": [ + "data/ConditionalValidation:active = true", + "data/ConditionalValidation:events = [\"insert\",\"update\"]", + "data/ConditionalValidation:priority = 100", + "data/ConditionalValidation:severity = \"error\"", + "data/CrossFieldValidation:active = true", + "data/CrossFieldValidation:events = [\"insert\",\"update\"]", + "data/CrossFieldValidation:priority = 100", + "data/CrossFieldValidation:severity = \"error\"", + "data/Cube:public = false", + "data/CubeJoin:relationship = \"many_to_one\"", + "data/CurrencyConfig:currencyMode = \"dynamic\"", + "data/CurrencyConfig:defaultCurrency = \"CNY\"", + "data/CurrencyConfig:precision = 2", + "data/DataEngineDeleteOptions:multi = false", + "data/DataEngineInsertOptions:returning = true", + "data/DataEngineUpdateOptions:multi = false", + "data/DataEngineUpdateOptions:returning = false", + "data/DataEngineUpdateOptions:upsert = false", + "data/DataEngineVectorFindRequest:limit = 5", + "data/Datasource:active = true", + "data/Datasource:autoConnect = false", + "data/Datasource:origin = \"code\"", + "data/Datasource:schemaMode = \"managed\"", + "data/DocumentSchemaValidation:enabled = false", + "data/DocumentVersion:isLatest = false", + "data/ESignatureConfig:enabled = false", + "data/ESignatureConfig:expirationDays = 30", + "data/ESignatureConfig:reminderDays = 7", + "data/EngineDeleteOptions:multi = false", + "data/EngineUpdateOptions:multi = false", + "data/EngineUpdateOptions:returning = false", + "data/EngineUpdateOptions:upsert = false", + "data/ExternalColumn:primaryKey = false", + "data/ExternalDatasourceSettings:allowWrites = false", + "data/ExternalDatasourceSettings:queryTimeoutMs = 30000", + "data/ExternalDatasourceSettings:validation = {\"checkOnBoot\":true,\"onMismatch\":\"fail\"}", + "data/ExternalFieldMapping:readonly = true", + "data/Field:deleteBehavior = \"set_null\"", + "data/Field:externalId = false", + "data/Field:hidden = false", + "data/Field:multiple = false", + "data/Field:readonly = false", + "data/Field:required = false", + "data/Field:searchable = false", + "data/Field:sortable = true", + "data/Field:unique = false", + "data/FilePersistenceConfig:autoSaveInterval = 2000", + "data/FormatValidation:active = true", + "data/FormatValidation:events = [\"insert\",\"update\"]", + "data/FormatValidation:priority = 100", + "data/FormatValidation:severity = \"error\"", + "data/FullTextSearch:fuzzy = false", + "data/FullTextSearch:highlight = false", + "data/FullTextSearch:operator = \"or\"", + "data/ImportFieldMapping:transform = \"none\"", + "data/Index:unique = false", + "data/JSONValidation:active = true", + "data/JSONValidation:events = [\"insert\",\"update\"]", + "data/JSONValidation:priority = 100", + "data/JSONValidation:severity = \"error\"", + "data/Mapping:mode = \"insert\"", + "data/Mapping:sourceFormat = \"csv\"", + "data/MongoConfig:host = \"localhost\"", + "data/MongoConfig:port = 27017", + "data/MysqlConfig:host = \"localhost\"", + "data/MysqlConfig:port = 3306", + "data/NoSQLIndex:background = false", + "data/NoSQLIndex:sparse = false", + "data/NoSQLIndex:unique = false", + "data/Object:datasource = \"default\"", + "data/Object:isSystem = false", + "data/ObjectAccessConfig:default = \"public\"", + "data/ObjectCapabilities:activities = true", + "data/ObjectCapabilities:apiEnabled = true", + "data/ObjectCapabilities:clone = true", + "data/ObjectCapabilities:feeds = true", + "data/ObjectCapabilities:files = false", + "data/ObjectCapabilities:searchable = true", + "data/ObjectCapabilities:trackHistory = false", + "data/ObjectDependencyGraph:circularDependencies = []", + "data/ObjectExtension:priority = 200", + "data/ObjectExternalBinding:writable = false", + "data/ObjectFieldGroup:collapse = \"none\"", + "data/PoolConfig:connectionTimeoutMillis = 5000", + "data/PoolConfig:idleTimeoutMillis = 30000", + "data/PoolConfig:max = 10", + "data/PoolConfig:min = 2", + "data/PostgresConfig:host = \"localhost\"", + "data/PostgresConfig:port = 5432", + "data/PostgresConfig:schema = \"public\"", + "data/ReferenceResolution:targetField = \"name\"", + "data/ReplicationConfig:enabled = false", + "data/SQLDriverConfig:ssl = false", + "data/SSLConfig:rejectUnauthorized = true", + "data/ScriptBody:capabilities = []", + "data/ScriptValidation:active = true", + "data/ScriptValidation:events = [\"insert\",\"update\"]", + "data/ScriptValidation:priority = 100", + "data/ScriptValidation:severity = \"error\"", + "data/Seed:env = [\"prod\",\"dev\",\"test\"]", + "data/Seed:externalId = \"name\"", + "data/Seed:mode = \"upsert\"", + "data/SeedLoadResult:errors = []", + "data/SeedLoadResult:referencesDropped = 0", + "data/SeedLoadResult:summariesStale = 0", + "data/SeedLoaderConfig:batchSize = 1000", + "data/SeedLoaderConfig:defaultMode = \"upsert\"", + "data/SeedLoaderConfig:dryRun = false", + "data/SeedLoaderConfig:haltOnError = false", + "data/SeedLoaderConfig:multiPass = true", + "data/SeedLoaderConfig:transaction = false", + "data/SeedLoaderRequest:config = {\"batchSize\":1000,\"defaultMode\":\"upsert\",\"dryRun\":false,\"haltOnError\":false,\"multiPass\":true,\"transaction\":false}", + "data/ShardingConfig:enabled = false", + "data/SortNode:order = \"asc\"", + "data/SqliteConfig:filename = \":memory:\"", + "data/SqliteWasmConfig:filename = \":memory:\"", + "data/StateMachineValidation:active = true", + "data/StateMachineValidation:events = [\"insert\",\"update\"]", + "data/StateMachineValidation:priority = 100", + "data/StateMachineValidation:severity = \"error\"" + ] +} diff --git a/packages/spec/authorable-defaults/identity.json b/packages/spec/authorable-defaults/identity.json new file mode 100644 index 0000000000..6d1ec8045a --- /dev/null +++ b/packages/spec/authorable-defaults/identity.json @@ -0,0 +1,22 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "identity", + "defaults": [ + "identity/ApiKey:enabled = true", + "identity/EvalUser:positions = []", + "identity/Invitation:status = \"pending\"", + "identity/Position:delegatable = false", + "identity/SCIMAddress:primary = false", + "identity/SCIMBulkRequest:schemas = [\"urn:ietf:params:scim:api:messages:2.0:BulkRequest\"]", + "identity/SCIMBulkResponse:schemas = [\"urn:ietf:params:scim:api:messages:2.0:BulkResponse\"]", + "identity/SCIMEmail:primary = false", + "identity/SCIMError:schemas = [\"urn:ietf:params:scim:api:messages:2.0:Error\"]", + "identity/SCIMGroup:schemas = [\"urn:ietf:params:scim:schemas:core:2.0:Group\"]", + "identity/SCIMListResponse:schemas = [\"urn:ietf:params:scim:api:messages:2.0:ListResponse\"]", + "identity/SCIMPatchRequest:schemas = [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"]", + "identity/SCIMPhoneNumber:primary = false", + "identity/SCIMUser:active = true", + "identity/SCIMUser:schemas = [\"urn:ietf:params:scim:schemas:core:2.0:User\"]", + "identity/User:emailVerified = false" + ] +} diff --git a/packages/spec/authorable-defaults/integration.json b/packages/spec/authorable-defaults/integration.json new file mode 100644 index 0000000000..51ac6504cc --- /dev/null +++ b/packages/spec/authorable-defaults/integration.json @@ -0,0 +1,47 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "integration", + "defaults": [ + "integration/CircuitBreakerConfig:failureThreshold = 5", + "integration/CircuitBreakerConfig:halfOpenMaxRequests = 1", + "integration/CircuitBreakerConfig:monitoringWindow = 60000", + "integration/CircuitBreakerConfig:resetTimeoutMs = 30000", + "integration/Connector:authentication = {\"type\":\"none\"}", + "integration/Connector:connectionTimeoutMs = 30000", + "integration/Connector:enabled = true", + "integration/Connector:requestTimeoutMs = 30000", + "integration/Connector:status = \"inactive\"", + "integration/ConnectorFieldMapping:required = false", + "integration/ConnectorFieldMapping:syncMode = \"bidirectional\"", + "integration/DataSyncConfig:batchSize = 1000", + "integration/DataSyncConfig:conflictResolution = \"latest_wins\"", + "integration/DataSyncConfig:deleteMode = \"soft_delete\"", + "integration/DataSyncConfig:direction = \"import\"", + "integration/DataSyncConfig:realtimeSync = false", + "integration/DataSyncConfig:strategy = \"incremental\"", + "integration/DeclarativeConnectorEntry:authentication = {\"type\":\"none\"}", + "integration/DeclarativeConnectorEntry:connectionTimeoutMs = 30000", + "integration/DeclarativeConnectorEntry:enabled = true", + "integration/DeclarativeConnectorEntry:requestTimeoutMs = 30000", + "integration/DeclarativeConnectorEntry:status = \"inactive\"", + "integration/ErrorMappingConfig:defaultCategory = \"integration_error\"", + "integration/ErrorMappingConfig:logUnmapped = true", + "integration/HealthCheckConfig:expectedStatus = 200", + "integration/HealthCheckConfig:healthyThreshold = 1", + "integration/HealthCheckConfig:intervalMs = 60000", + "integration/HealthCheckConfig:timeoutMs = 5000", + "integration/HealthCheckConfig:unhealthyThreshold = 3", + "integration/RetryConfig:backoffMultiplier = 2", + "integration/RetryConfig:initialDelayMs = 1000", + "integration/RetryConfig:jitter = true", + "integration/RetryConfig:maxAttempts = 3", + "integration/RetryConfig:maxDelayMs = 60000", + "integration/RetryConfig:retryOnNetworkError = true", + "integration/RetryConfig:retryableStatusCodes = [408,429,500,502,503,504]", + "integration/RetryConfig:strategy = \"exponential_backoff\"", + "integration/WebhookConfig:isActive = true", + "integration/WebhookConfig:method = \"POST\"", + "integration/WebhookConfig:signatureAlgorithm = \"hmac_sha256\"", + "integration/WebhookConfig:timeoutMs = 30000" + ] +} diff --git a/packages/spec/authorable-defaults/kernel.json b/packages/spec/authorable-defaults/kernel.json new file mode 100644 index 0000000000..59c214cdeb --- /dev/null +++ b/packages/spec/authorable-defaults/kernel.json @@ -0,0 +1,205 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "kernel", + "defaults": [ + "kernel/ArtifactChecksum:algorithm = \"sha256\"", + "kernel/ArtifactSignature:algorithm = \"RSA-SHA256\"", + "kernel/BreakingChange:automatedMigration = false", + "kernel/ClusterCapabilityConfig:driver = \"memory\"", + "kernel/ClusterCapabilityConfig:heartbeatMs = 5000", + "kernel/ClusterCapabilityConfig:lockTtlMs = 15000", + "kernel/ClusterCapabilityConfig:tenantIsolation = \"channel-prefix\"", + "kernel/ClusterCapabilityConfig:useExistingPool = true", + "kernel/CompatibilityMatrixEntry:migrationRequired = false", + "kernel/CustomizationPolicy:allowAddFields = true", + "kernel/CustomizationPolicy:allowCustomization = true", + "kernel/CustomizationPolicy:allowDeleteFields = false", + "kernel/DependencyGraphNode:dependencies = []", + "kernel/EventClusterOptions:scope = \"local\"", + "kernel/EventHandler:async = true", + "kernel/EventHandler:priority = 0", + "kernel/EventMessageQueueConfig:batchSize = 1", + "kernel/EventMessageQueueConfig:compression = \"none\"", + "kernel/EventMessageQueueConfig:eventPattern = \"*\"", + "kernel/EventMessageQueueConfig:flushIntervalMs = 1000", + "kernel/EventMessageQueueConfig:format = \"json\"", + "kernel/EventMessageQueueConfig:includeMetadata = true", + "kernel/EventMetadata:priority = \"normal\"", + "kernel/EventPersistence:enabled = false", + "kernel/EventPersistence:storage = \"database\"", + "kernel/EventQueueConfig:concurrency = 10", + "kernel/EventQueueConfig:name = \"events\"", + "kernel/EventQueueConfig:priorityEnabled = true", + "kernel/EventReplayConfig:speed = 1", + "kernel/EventSourcingConfig:enabled = false", + "kernel/EventSourcingConfig:retention = 365", + "kernel/EventSourcingConfig:snapshotInterval = 100", + "kernel/EventSourcingConfig:snapshotRetention = 10", + "kernel/EventTypeDefinition:deprecated = false", + "kernel/EventTypeDefinition:version = \"1.0.0\"", + "kernel/EventWebhookConfig:enabled = true", + "kernel/EventWebhookConfig:method = \"POST\"", + "kernel/EventWebhookConfig:timeoutMs = 30000", + "kernel/ExecutionContext:isSystem = false", + "kernel/ExecutionContext:permissions = []", + "kernel/ExecutionContext:positions = []", + "kernel/ExtensionPoint:cardinality = \"multiple\"", + "kernel/GracefulDegradation:enabled = true", + "kernel/GracefulDegradation:fallbackMode = \"minimal\"", + "kernel/HotReloadConfig:debounceDelay = 1000", + "kernel/HotReloadConfig:enabled = false", + "kernel/HotReloadConfig:preserveState = true", + "kernel/HotReloadConfig:shutdownTimeout = 30000", + "kernel/HotReloadConfig:stateStrategy = \"memory\"", + "kernel/InstallPackageRequest:enableOnInstall = true", + "kernel/InstalledPackage:enabled = true", + "kernel/InstalledPackage:status = \"installed\"", + "kernel/KernelContext:features = {}", + "kernel/KernelContext:mode = \"production\"", + "kernel/KernelSecurityVulnerability:exploitAvailable = false", + "kernel/KernelSecurityVulnerability:patchAvailable = false", + "kernel/Manifest:defaultDatasource = \"default\"", + "kernel/Manifest:scope = \"project\"", + "kernel/MergeStrategyConfig:autoResolveNonConflicting = true", + "kernel/MergeStrategyConfig:defaultStrategy = \"three-way-merge\"", + "kernel/MetadataDiffItem:hasConflict = false", + "kernel/MetadataManagerConfig:fallback = \"none\"", + "kernel/MetadataManagerConfig:formats = [\"typescript\",\"json\",\"yaml\"]", + "kernel/MetadataManagerConfig:tableName = \"sys_metadata\"", + "kernel/MetadataManagerConfig:watch = false", + "kernel/MetadataOverlay:active = true", + "kernel/MetadataOverlay:scope = \"platform\"", + "kernel/MetadataPluginConfig:bootstrap = \"eager\"", + "kernel/MetadataPluginConfig:cacheMaxItems = 10000", + "kernel/MetadataPluginConfig:enableEvents = true", + "kernel/MetadataPluginConfig:enableVersioning = false", + "kernel/MetadataPluginConfig:validateOnWrite = true", + "kernel/MetadataPluginManifest:description = \"Core metadata management service for ObjectStack platform\"", + "kernel/MetadataQuery:page = 1", + "kernel/MetadataQuery:pageSize = 50", + "kernel/MetadataQuery:sortBy = \"name\"", + "kernel/MetadataQuery:sortOrder = \"asc\"", + "kernel/MetadataTypeRegistryEntry:allowOrgOverride = false", + "kernel/MetadataTypeRegistryEntry:allowRuntimeCreate = true", + "kernel/MetadataTypeRegistryEntry:executionPinned = false", + "kernel/MetadataTypeRegistryEntry:loadOrder = 100", + "kernel/MetadataTypeRegistryEntry:supportsOverlay = true", + "kernel/MetadataTypeRegistryEntry:supportsVersioning = false", + "kernel/MultiVersionSupport:enabled = false", + "kernel/MultiVersionSupport:maxConcurrentVersions = 2", + "kernel/MultiVersionSupport:selectionStrategy = \"latest\"", + "kernel/PackageArtifact:format = \"tgz\"", + "kernel/PackageArtifact:formatVersion = \"1.0\"", + "kernel/PackageDependencyResolutionResult:conflicts = []", + "kernel/PackageDependencyResolutionResult:errors = []", + "kernel/PackageDependencyResolutionResult:installOrder = []", + "kernel/Plugin:type = \"standard\"", + "kernel/PluginCaching:enabled = true", + "kernel/PluginCaching:keyStrategy = \"version\"", + "kernel/PluginCaching:storage = \"memory\"", + "kernel/PluginCapability:certified = false", + "kernel/PluginCapability:conformance = \"full\"", + "kernel/PluginCodeSplitting:chunkNaming = \"hashed\"", + "kernel/PluginCodeSplitting:enabled = true", + "kernel/PluginCodeSplitting:strategy = \"feature\"", + "kernel/PluginDependency:optional = false", + "kernel/PluginDependencyResolution:circularDependencies = \"warn\"", + "kernel/PluginDependencyResolution:conflictResolution = \"latest\"", + "kernel/PluginDependencyResolution:strategy = \"compatible\"", + "kernel/PluginDynamicImport:enabled = true", + "kernel/PluginDynamicImport:mode = \"async\"", + "kernel/PluginDynamicImport:prefetch = false", + "kernel/PluginDynamicImport:preload = false", + "kernel/PluginDynamicImport:timeout = 30000", + "kernel/PluginHealthCheck:autoRestart = false", + "kernel/PluginHealthCheck:failureThreshold = 3", + "kernel/PluginHealthCheck:interval = 30000", + "kernel/PluginHealthCheck:maxRestartAttempts = 3", + "kernel/PluginHealthCheck:restartBackoff = \"exponential\"", + "kernel/PluginHealthCheck:successThreshold = 1", + "kernel/PluginHealthCheck:timeout = 5000", + "kernel/PluginHotReload:debounceMs = 300", + "kernel/PluginHotReload:enabled = false", + "kernel/PluginHotReload:environment = \"development\"", + "kernel/PluginHotReload:preserveState = false", + "kernel/PluginHotReload:strategy = \"full\"", + "kernel/PluginInitialization:critical = false", + "kernel/PluginInitialization:mode = \"async\"", + "kernel/PluginInitialization:priority = 100", + "kernel/PluginInitialization:timeout = 30000", + "kernel/PluginInstallConfig:autoUpdate = false", + "kernel/PluginInterface:stability = \"stable\"", + "kernel/PluginLoadingConfig:strategy = \"lazy\"", + "kernel/PluginLoadingState:progress = 0", + "kernel/PluginLoadingState:retryCount = 0", + "kernel/PluginPerformanceMonitoring:enabled = false", + "kernel/PluginPerformanceMonitoring:onBudgetViolation = \"warn\"", + "kernel/PluginPerformanceMonitoring:reportingInterval = 60", + "kernel/PluginPerformanceMonitoring:samplingRate = 1", + "kernel/PluginPermission:required = true", + "kernel/PluginPermission:scope = \"plugin\"", + "kernel/PluginPermissionSet:defaultGrant = \"prompt\"", + "kernel/PluginPreloadConfig:enabled = false", + "kernel/PluginPreloadConfig:priority = 100", + "kernel/PluginProvenance:attestations = []", + "kernel/PluginProvenance:signatures = []", + "kernel/PluginRegistryEntry:deprecated = false", + "kernel/PluginSandboxing:enabled = false", + "kernel/PluginSandboxing:isolationLevel = \"none\"", + "kernel/PluginSandboxing:scope = \"automation-only\"", + "kernel/PluginSearchFilters:limit = 20", + "kernel/PluginSearchFilters:page = 1", + "kernel/PluginSearchFilters:sortOrder = \"desc\"", + "kernel/PluginStatistics:activeInstallations = 0", + "kernel/PluginStatistics:dependents = 0", + "kernel/PluginStatistics:downloads = 0", + "kernel/PluginStatistics:downloadsLastMonth = 0", + "kernel/PluginTrustScore:badges = []", + "kernel/PluginUpdateStrategy:mode = \"manual\"", + "kernel/PluginVendor:trustLevel = \"unverified\"", + "kernel/PluginVendor:verified = false", + "kernel/PreviewModeConfig:autoLogin = true", + "kernel/PreviewModeConfig:expiresInSeconds = 0", + "kernel/PreviewModeConfig:readOnly = false", + "kernel/PreviewModeConfig:simulatedRole = \"admin\"", + "kernel/PreviewModeConfig:simulatedUserName = \"Preview User\"", + "kernel/ProtocolFeature:enabled = true", + "kernel/RealTimeNotificationConfig:enabled = true", + "kernel/RealTimeNotificationConfig:eventPattern = \"*\"", + "kernel/RealTimeNotificationConfig:protocol = \"websocket\"", + "kernel/RealTimeNotificationConfig:tenantFilter = true", + "kernel/RealTimeNotificationConfig:userFilter = true", + "kernel/ResolvedPackageDependency:type = \"required\"", + "kernel/RollbackPackageRequest:rollbackCustomizations = true", + "kernel/RuntimeConfig:engine = \"v8-isolate\"", + "kernel/SBOM:format = \"cyclonedx\"", + "kernel/SBOMEntry:externalRefs = []", + "kernel/SandboxConfig:enabled = true", + "kernel/SandboxConfig:level = \"standard\"", + "kernel/SecurityPolicy:allowedLicenses = [\"MIT\",\"Apache-2.0\",\"BSD-3-Clause\",\"BSD-2-Clause\",\"ISC\"]", + "kernel/SecurityPolicy:prohibitedLicenses = [\"GPL-3.0\",\"AGPL-3.0\"]", + "kernel/SecurityScanResult:licenseIssues = []", + "kernel/SecurityVulnerability:cwe = []", + "kernel/SecurityVulnerability:references = []", + "kernel/ServiceClusterAnnotations:clusterScope = \"node\"", + "kernel/ServiceFactoryRegistration:factoryType = \"sync\"", + "kernel/ServiceFactoryRegistration:scope = \"singleton\"", + "kernel/ServiceFactoryRegistration:singleton = true", + "kernel/ServiceMetadata:scope = \"singleton\"", + "kernel/ServiceRegistryConfig:allowOverwrite = false", + "kernel/ServiceRegistryConfig:enableLogging = false", + "kernel/ServiceRegistryConfig:strictMode = true", + "kernel/StartupOptions:healthCheck = false", + "kernel/StartupOptions:parallel = false", + "kernel/StartupOptions:rollbackOnFailure = true", + "kernel/StartupOptions:timeout = 30000", + "kernel/TenantRuntimeContext:features = {}", + "kernel/TenantRuntimeContext:mode = \"production\"", + "kernel/UpgradePackageRequest:createSnapshot = true", + "kernel/UpgradePackageRequest:dryRun = false", + "kernel/UpgradePackageRequest:mergeStrategy = \"three-way-merge\"", + "kernel/UpgradePackageRequest:skipValidation = false", + "kernel/UpgradePlan:affectedCustomizations = 0", + "kernel/UpgradePlan:requiresMigration = false" + ] +} diff --git a/packages/spec/authorable-defaults/security.json b/packages/spec/authorable-defaults/security.json new file mode 100644 index 0000000000..c21bf08692 --- /dev/null +++ b/packages/spec/authorable-defaults/security.json @@ -0,0 +1,42 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "security", + "defaults": [ + "security/AccessMatrix:entries = []", + "security/AccessMatrix:version = 1", + "security/AdminScope:assignablePermissionSets = []", + "security/AdminScope:authorEnvironmentSets = false", + "security/AdminScope:includeSubtree = true", + "security/AdminScope:manageAssignments = false", + "security/AdminScope:manageBindings = false", + "security/CapabilityDeclaration:scope = \"platform\"", + "security/CriteriaSharingRule:accessLevel = \"read\"", + "security/CriteriaSharingRule:active = true", + "security/EffectiveObjectPermission:allowCreate = false", + "security/EffectiveObjectPermission:allowDelete = false", + "security/EffectiveObjectPermission:allowEdit = false", + "security/EffectiveObjectPermission:allowPurge = false", + "security/EffectiveObjectPermission:allowRead = false", + "security/EffectiveObjectPermission:allowRestore = false", + "security/EffectiveObjectPermission:allowTransfer = false", + "security/EffectiveObjectPermission:modifyAllRecords = false", + "security/EffectiveObjectPermission:viewAllRecords = false", + "security/ExplainLayer:contributors = []", + "security/ExplainRecordAttribution:rules = []", + "security/FieldPermission:editable = false", + "security/FieldPermission:readable = true", + "security/ObjectPermission:allowCreate = false", + "security/ObjectPermission:allowDelete = false", + "security/ObjectPermission:allowEdit = false", + "security/ObjectPermission:allowPurge = false", + "security/ObjectPermission:allowRead = false", + "security/ObjectPermission:allowRestore = false", + "security/ObjectPermission:allowTransfer = false", + "security/ObjectPermission:modifyAllRecords = false", + "security/ObjectPermission:viewAllRecords = false", + "security/PermissionSet:isDefault = false", + "security/RowLevelSecurityPolicy:enabled = true", + "security/SharingRule:accessLevel = \"read\"", + "security/SharingRule:active = true" + ] +} diff --git a/packages/spec/authorable-defaults/shared.json b/packages/spec/authorable-defaults/shared.json new file mode 100644 index 0000000000..d0dbd302f0 --- /dev/null +++ b/packages/spec/authorable-defaults/shared.json @@ -0,0 +1,13 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "shared", + "defaults": [ + "shared/CorsConfig:credentials = false", + "shared/CorsConfig:enabled = true", + "shared/CorsConfig:origins = \"*\"", + "shared/HttpRequest:method = \"GET\"", + "shared/RateLimitConfig:enabled = false", + "shared/RateLimitConfig:maxRequests = 100", + "shared/RateLimitConfig:windowMs = 60000" + ] +} diff --git a/packages/spec/authorable-defaults/studio.json b/packages/spec/authorable-defaults/studio.json new file mode 100644 index 0000000000..ae27cd9833 --- /dev/null +++ b/packages/spec/authorable-defaults/studio.json @@ -0,0 +1,106 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "studio", + "defaults": [ + "studio/ActionContribution:metadataTypes = []", + "studio/ERDiagramConfig:autoFit = true", + "studio/ERDiagramConfig:clickToNavigate = true", + "studio/ERDiagramConfig:dragToConnect = true", + "studio/ERDiagramConfig:enabled = true", + "studio/ERDiagramConfig:exportFormats = [\"png\",\"svg\"]", + "studio/ERDiagramConfig:hideOrphans = false", + "studio/ERDiagramConfig:highlightOnHover = true", + "studio/ERDiagramConfig:layout = \"force\"", + "studio/ERDiagramConfig:maxZoom = 3", + "studio/ERDiagramConfig:minZoom = 0.1", + "studio/ERDiagramConfig:nodeDisplay = {\"maxFieldsVisible\":8,\"showDescription\":true,\"showFieldTypes\":true,\"showFields\":true,\"showIcon\":true,\"showRecordCount\":false,\"showRequiredIndicator\":true}", + "studio/ERDiagramConfig:showEdgeLabels = true", + "studio/ERDiagramConfig:showMinimap = true", + "studio/ERDiagramConfig:zoomControls = true", + "studio/ERNodeDisplay:maxFieldsVisible = 8", + "studio/ERNodeDisplay:showDescription = true", + "studio/ERNodeDisplay:showFieldTypes = true", + "studio/ERNodeDisplay:showFields = true", + "studio/ERNodeDisplay:showIcon = true", + "studio/ERNodeDisplay:showRecordCount = false", + "studio/ERNodeDisplay:showRequiredIndicator = true", + "studio/FieldEditorConfig:batchOperations = true", + "studio/FieldEditorConfig:dragReorder = true", + "studio/FieldEditorConfig:fieldGroups = []", + "studio/FieldEditorConfig:inlineEditing = true", + "studio/FieldEditorConfig:paginationThreshold = 50", + "studio/FieldEditorConfig:propertySections = [{\"defaultExpanded\":true,\"key\":\"basics\",\"label\":\"Basic Properties\",\"order\":0},{\"defaultExpanded\":true,\"key\":\"constraints\",\"label\":\"Constraints & Validation\",\"order\":10},{\"defaultExpanded\":true,\"key\":\"relationship\",\"label\":\"Relationship Config\",\"order\":20},{\"defaultExpanded\":false,\"key\":\"display\",\"label\":\"Display & UI\",\"order\":30},{\"defaultExpanded\":false,\"key\":\"security\",\"label\":\"Security & Compliance\",\"order\":40},{\"defaultExpanded\":false,\"key\":\"advanced\",\"label\":\"Advanced\",\"order\":50}]", + "studio/FieldEditorConfig:showFieldGroups = true", + "studio/FieldEditorConfig:showPropertyPanel = true", + "studio/FieldEditorConfig:showUsageStats = false", + "studio/FieldGroup:defaultExpanded = true", + "studio/FieldGroup:order = 0", + "studio/FieldPropertySection:defaultExpanded = true", + "studio/FieldPropertySection:order = 0", + "studio/FlowBuilderConfig:animateExecution = true", + "studio/FlowBuilderConfig:connectionValidation = true", + "studio/FlowBuilderConfig:layoutAlgorithm = \"dagre\"", + "studio/FlowBuilderConfig:layoutDirection = \"TB\"", + "studio/FlowBuilderConfig:showMinimap = true", + "studio/FlowBuilderConfig:showPalette = true", + "studio/FlowBuilderConfig:showPropertyPanel = true", + "studio/FlowBuilderConfig:snap = {\"enabled\":true,\"gridSize\":16,\"showGrid\":true}", + "studio/FlowBuilderConfig:undoLimit = 50", + "studio/FlowBuilderConfig:zoom = {\"default\":1,\"max\":3,\"min\":0.25,\"step\":0.1}", + "studio/FlowCanvasEdge:animated = false", + "studio/FlowCanvasEdge:color = \"#94a3b8\"", + "studio/FlowCanvasEdge:labelPosition = 0.5", + "studio/FlowCanvasEdge:style = \"solid\"", + "studio/FlowCanvasNode:collapsed = false", + "studio/FlowNodeRenderDescriptor:allowBoundaryEvents = false", + "studio/FlowNodeRenderDescriptor:borderColor = \"#94a3b8\"", + "studio/FlowNodeRenderDescriptor:defaultHeight = 60", + "studio/FlowNodeRenderDescriptor:defaultWidth = 120", + "studio/FlowNodeRenderDescriptor:fillColor = \"#ffffff\"", + "studio/MetadataViewerContribution:modes = [\"preview\"]", + "studio/MetadataViewerContribution:priority = 0", + "studio/ObjectDesignerConfig:defaultView = \"field-editor\"", + "studio/ObjectDesignerConfig:erDiagram = {\"autoFit\":true,\"clickToNavigate\":true,\"dragToConnect\":true,\"enabled\":true,\"exportFormats\":[\"png\",\"svg\"],\"hideOrphans\":false,\"highlightOnHover\":true,\"layout\":\"force\",\"maxZoom\":3,\"minZoom\":0.1,\"nodeDisplay\":{\"maxFieldsVisible\":8,\"showDescription\":true,\"showFieldTypes\":true,\"showFields\":true,\"showIcon\":true,\"showRecordCount\":false,\"showRequiredIndicator\":true},\"showEdgeLabels\":true,\"showMinimap\":true,\"zoomControls\":true}", + "studio/ObjectDesignerConfig:fieldEditor = {\"batchOperations\":true,\"dragReorder\":true,\"fieldGroups\":[],\"inlineEditing\":true,\"paginationThreshold\":50,\"propertySections\":[{\"defaultExpanded\":true,\"key\":\"basics\",\"label\":\"Basic Properties\",\"order\":0},{\"defaultExpanded\":true,\"key\":\"constraints\",\"label\":\"Constraints & Validation\",\"order\":10},{\"defaultExpanded\":true,\"key\":\"relationship\",\"label\":\"Relationship Config\",\"order\":20},{\"defaultExpanded\":false,\"key\":\"display\",\"label\":\"Display & UI\",\"order\":30},{\"defaultExpanded\":false,\"key\":\"security\",\"label\":\"Security & Compliance\",\"order\":40},{\"defaultExpanded\":false,\"key\":\"advanced\",\"label\":\"Advanced\",\"order\":50}],\"showFieldGroups\":true,\"showPropertyPanel\":true,\"showUsageStats\":false}", + "studio/ObjectDesignerConfig:objectManager = {\"defaultDisplayMode\":\"table\",\"defaultFilter\":{\"includeAbstract\":false,\"includeSystem\":true},\"defaultSortDirection\":\"asc\",\"defaultSortField\":\"label\",\"enableComparison\":false,\"showCreateAction\":true,\"showERDiagramToggle\":true,\"showFieldCount\":true,\"showQuickPreview\":true,\"showRelationshipCount\":true,\"showStatsSummary\":true}", + "studio/ObjectDesignerConfig:objectPreview = {\"defaultTab\":\"fields\",\"showBreadcrumbs\":true,\"showHeader\":true,\"tabs\":[{\"enabled\":true,\"icon\":\"list\",\"key\":\"fields\",\"label\":\"Fields\",\"order\":0},{\"enabled\":true,\"icon\":\"link\",\"key\":\"relationships\",\"label\":\"Relationships\",\"order\":10},{\"enabled\":true,\"icon\":\"zap\",\"key\":\"indexes\",\"label\":\"Indexes\",\"order\":20},{\"enabled\":true,\"icon\":\"shield-check\",\"key\":\"validations\",\"label\":\"Validations\",\"order\":30},{\"enabled\":true,\"icon\":\"settings\",\"key\":\"capabilities\",\"label\":\"Capabilities\",\"order\":40},{\"enabled\":true,\"icon\":\"table-2\",\"key\":\"data\",\"label\":\"Data\",\"order\":50},{\"enabled\":true,\"icon\":\"globe\",\"key\":\"api\",\"label\":\"API\",\"order\":60},{\"enabled\":true,\"icon\":\"code-2\",\"key\":\"code\",\"label\":\"Code\",\"order\":70}]}", + "studio/ObjectDesignerConfig:relationshipMapper = {\"displayConfig\":[{\"cardinalityLabel\":\"1:N\",\"color\":\"#0891b2\",\"highlightColor\":\"#06b6d4\",\"lineStyle\":\"dashed\",\"type\":\"lookup\"},{\"cardinalityLabel\":\"1:N\",\"color\":\"#ea580c\",\"highlightColor\":\"#f97316\",\"lineStyle\":\"solid\",\"type\":\"master_detail\"},{\"cardinalityLabel\":\"1:N\",\"color\":\"#8b5cf6\",\"highlightColor\":\"#a78bfa\",\"lineStyle\":\"dotted\",\"type\":\"tree\"}],\"showCascadeWarnings\":true,\"showReverseRelationships\":true,\"visualCreation\":true}", + "studio/ObjectFilter:includeAbstract = false", + "studio/ObjectFilter:includeSystem = true", + "studio/ObjectManagerConfig:defaultDisplayMode = \"table\"", + "studio/ObjectManagerConfig:defaultFilter = {\"includeAbstract\":false,\"includeSystem\":true}", + "studio/ObjectManagerConfig:defaultSortDirection = \"asc\"", + "studio/ObjectManagerConfig:defaultSortField = \"label\"", + "studio/ObjectManagerConfig:enableComparison = false", + "studio/ObjectManagerConfig:showCreateAction = true", + "studio/ObjectManagerConfig:showERDiagramToggle = true", + "studio/ObjectManagerConfig:showFieldCount = true", + "studio/ObjectManagerConfig:showQuickPreview = true", + "studio/ObjectManagerConfig:showRelationshipCount = true", + "studio/ObjectManagerConfig:showStatsSummary = true", + "studio/ObjectPreviewConfig:defaultTab = \"fields\"", + "studio/ObjectPreviewConfig:showBreadcrumbs = true", + "studio/ObjectPreviewConfig:showHeader = true", + "studio/ObjectPreviewConfig:tabs = [{\"enabled\":true,\"icon\":\"list\",\"key\":\"fields\",\"label\":\"Fields\",\"order\":0},{\"enabled\":true,\"icon\":\"link\",\"key\":\"relationships\",\"label\":\"Relationships\",\"order\":10},{\"enabled\":true,\"icon\":\"zap\",\"key\":\"indexes\",\"label\":\"Indexes\",\"order\":20},{\"enabled\":true,\"icon\":\"shield-check\",\"key\":\"validations\",\"label\":\"Validations\",\"order\":30},{\"enabled\":true,\"icon\":\"settings\",\"key\":\"capabilities\",\"label\":\"Capabilities\",\"order\":40},{\"enabled\":true,\"icon\":\"table-2\",\"key\":\"data\",\"label\":\"Data\",\"order\":50},{\"enabled\":true,\"icon\":\"globe\",\"key\":\"api\",\"label\":\"API\",\"order\":60},{\"enabled\":true,\"icon\":\"code-2\",\"key\":\"code\",\"label\":\"Code\",\"order\":70}]", + "studio/ObjectPreviewTab:enabled = true", + "studio/ObjectPreviewTab:order = 0", + "studio/PanelContribution:location = \"bottom\"", + "studio/RelationshipDisplay:cardinalityLabel = \"1:N\"", + "studio/RelationshipDisplay:color = \"#94a3b8\"", + "studio/RelationshipDisplay:highlightColor = \"#0891b2\"", + "studio/RelationshipDisplay:lineStyle = \"solid\"", + "studio/RelationshipMapperConfig:displayConfig = [{\"cardinalityLabel\":\"1:N\",\"color\":\"#0891b2\",\"highlightColor\":\"#06b6d4\",\"lineStyle\":\"dashed\",\"type\":\"lookup\"},{\"cardinalityLabel\":\"1:N\",\"color\":\"#ea580c\",\"highlightColor\":\"#f97316\",\"lineStyle\":\"solid\",\"type\":\"master_detail\"},{\"cardinalityLabel\":\"1:N\",\"color\":\"#8b5cf6\",\"highlightColor\":\"#a78bfa\",\"lineStyle\":\"dotted\",\"type\":\"tree\"}]", + "studio/RelationshipMapperConfig:showCascadeWarnings = true", + "studio/RelationshipMapperConfig:showReverseRelationships = true", + "studio/RelationshipMapperConfig:visualCreation = true", + "studio/SidebarGroupContribution:order = 100", + "studio/StudioPluginContributions:actions = []", + "studio/StudioPluginContributions:commands = []", + "studio/StudioPluginContributions:metadataIcons = []", + "studio/StudioPluginContributions:metadataViewers = []", + "studio/StudioPluginContributions:panels = []", + "studio/StudioPluginContributions:sidebarGroups = []", + "studio/StudioPluginManifest:contributes = {\"actions\":[],\"commands\":[],\"metadataIcons\":[],\"metadataViewers\":[],\"panels\":[],\"sidebarGroups\":[]}", + "studio/StudioPluginManifest:version = \"0.0.1\"" + ] +} diff --git a/packages/spec/authorable-defaults/system.json b/packages/spec/authorable-defaults/system.json new file mode 100644 index 0000000000..48c313222b --- /dev/null +++ b/packages/spec/authorable-defaults/system.json @@ -0,0 +1,285 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "system", + "defaults": [ + "system/AccessControlConfig:acl = \"private\"", + "system/AccessControlConfig:corsEnabled = false", + "system/AppCompatibilityCheck:issues = []", + "system/AppInstallRequest:skipSeedData = false", + "system/AppInstallResult:createdTables = []", + "system/AppInstallResult:installedObjects = []", + "system/AppInstallResult:seededRecords = 0", + "system/AppManifest:dependencies = []", + "system/AppManifest:flows = []", + "system/AppManifest:hasSeedData = false", + "system/AppManifest:objects = []", + "system/AppManifest:seedData = []", + "system/AppManifest:views = []", + "system/AuthConfig:uiBasePath = \"/_console\"", + "system/AuthPluginConfig:admin = false", + "system/AuthPluginConfig:deviceAuthorization = false", + "system/AuthPluginConfig:magicLink = false", + "system/AuthPluginConfig:oidcProvider = false", + "system/AuthPluginConfig:organization = true", + "system/AuthPluginConfig:passkeys = false", + "system/AuthPluginConfig:passwordRejectBreached = false", + "system/AuthPluginConfig:phoneNumber = false", + "system/AuthPluginConfig:twoFactor = false", + "system/BackupConfig:strategy = \"incremental\"", + "system/BackupConfig:verifyAfterBackup = true", + "system/BackupRetention:minCopies = 3", + "system/BatchProgress:failed = 0", + "system/BatchProgress:processed = 0", + "system/BatchProgress:succeeded = 0", + "system/BucketConfig:enabled = true", + "system/BucketConfig:pathStyle = false", + "system/BucketConfig:versioning = false", + "system/CacheConfig:compression = false", + "system/CacheConfig:enabled = false", + "system/CacheConfig:encryption = false", + "system/CacheConfig:prefetch = false", + "system/CacheTier:strategy = \"lru\"", + "system/CacheTier:ttl = 300", + "system/CacheTier:warmup = false", + "system/CacheWarmup:concurrency = 10", + "system/CacheWarmup:enabled = false", + "system/CacheWarmup:strategy = \"lazy\"", + "system/CollaborationSessionConfig:conflictResolution = \"ot\"", + "system/CollaborationSessionConfig:enableAwareness = true", + "system/CollaborationSessionConfig:enableCursorSharing = true", + "system/CollaborationSessionConfig:enablePresence = true", + "system/CollaborationSessionConfig:idleTimeout = 300000", + "system/CollaborationSessionConfig:persistence = true", + "system/CollaborativeCursor:isTyping = false", + "system/ComplianceAuditRequirement:alertOnMissing = true", + "system/ComplianceEncryptionRequirement:keyRotationMaxDays = 90", + "system/ComplianceEncryptionRequirement:minimumAlgorithm = \"aes-256-gcm\"", + "system/ConsoleDestinationConfig:colors = true", + "system/ConsoleDestinationConfig:prettyPrint = false", + "system/ConsoleDestinationConfig:stream = \"stdout\"", + "system/ConsumerConfig:autoOffsetReset = \"latest\"", + "system/ConsumerConfig:enableAutoCommit = true", + "system/ConsumerConfig:maxPollRecords = 500", + "system/CronSchedule:timezone = \"UTC\"", + "system/CursorStyle:opacity = 1", + "system/CursorStyle:pulseOnUpdate = true", + "system/CursorStyle:showLabel = true", + "system/DataClassificationPolicy:requireAudit = false", + "system/DataClassificationPolicy:requireEncryption = false", + "system/DataClassificationPolicy:requireMasking = false", + "system/DeadLetterQueue:enabled = false", + "system/DeadLetterQueue:maxRetries = 3", + "system/DeployBundle:flows = []", + "system/DeployBundle:objects = []", + "system/DeployBundle:permissions = []", + "system/DeployBundle:seedData = []", + "system/DeployBundle:views = []", + "system/DeployDiff:changes = []", + "system/DeployDiff:hasBreakingChanges = false", + "system/DeployManifest:flows = []", + "system/DeployManifest:objects = []", + "system/DeployManifest:permissions = []", + "system/DeployManifest:views = []", + "system/DeployValidationResult:errorCount = 0", + "system/DeployValidationResult:issues = []", + "system/DeployValidationResult:warningCount = 0", + "system/DisasterRecoveryPlan:enabled = false", + "system/DistributedCacheConfig:compression = false", + "system/DistributedCacheConfig:enabled = false", + "system/DistributedCacheConfig:encryption = false", + "system/DistributedCacheConfig:prefetch = false", + "system/EmailAndPasswordConfig:enabled = true", + "system/EmailServiceConfig:provider = \"log\"", + "system/EmailTemplateDefinition:active = true", + "system/EmailTemplateDefinition:category = \"custom\"", + "system/EmailTemplateDefinition:isSystem = false", + "system/EmailTemplateDefinition:locale = \"en-US\"", + "system/EmailTemplateDefinition:variables = []", + "system/EmailTemplateDefinitionVariable:required = false", + "system/EmailTemplateDefinitionVariable:type = \"string\"", + "system/EncryptionConfig:algorithm = \"aes-256-gcm\"", + "system/EncryptionConfig:deterministicEncryption = false", + "system/EncryptionConfig:enabled = false", + "system/EncryptionConfig:searchableEncryption = false", + "system/FacetConfig:maxValues = 10", + "system/FacetConfig:sort = \"count\"", + "system/FailoverConfig:autoFailover = true", + "system/FailoverConfig:failureThreshold = 3", + "system/FailoverConfig:healthCheckInterval = 30", + "system/FailoverConfig:mode = \"active_passive\"", + "system/Feature:type = \"boolean\"", + "system/FieldEncryption:indexable = false", + "system/FileDestinationConfig:append = true", + "system/FileDestinationConfig:encoding = \"utf8\"", + "system/HttpDestinationConfig:method = \"POST\"", + "system/HttpDestinationConfig:timeout = 30000", + "system/IncidentNotificationMatrix:escalationChain = []", + "system/IncidentNotificationMatrix:escalationTimeoutMinutes = 30", + "system/IncidentNotificationRule:notifyRegulators = false", + "system/IncidentResponsePolicy:enabled = true", + "system/IncidentResponsePolicy:regulatoryNotificationThreshold = \"high\"", + "system/IncidentResponsePolicy:requirePostIncidentReview = true", + "system/IncidentResponsePolicy:retentionDays = 2555", + "system/IncidentResponsePolicy:triageDeadlineHours = 1", + "system/Job:enabled = true", + "system/KeyRotationPolicy:autoRotate = true", + "system/KeyRotationPolicy:enabled = false", + "system/KeyRotationPolicy:frequencyDays = 90", + "system/KeyRotationPolicy:retainOldVersions = 3", + "system/LifecyclePolicyConfig:enabled = false", + "system/LifecyclePolicyConfig:rules = []", + "system/LifecyclePolicyRule:enabled = true", + "system/LogDestination:enabled = true", + "system/LogDestination:format = \"json\"", + "system/LogDestination:level = \"info\"", + "system/LogEnrichmentConfig:addCaller = false", + "system/LogEnrichmentConfig:addCorrelationIds = true", + "system/LogEnrichmentConfig:addEnvironment = true", + "system/LogEnrichmentConfig:addHostname = true", + "system/LogEnrichmentConfig:addProcessId = true", + "system/LoggerConfig:format = \"json\"", + "system/LoggerConfig:level = \"info\"", + "system/LoggerConfig:redact = [\"password\",\"token\",\"secret\",\"key\"]", + "system/LoggerConfig:sourceLocation = false", + "system/LoggingConfig:enabled = true", + "system/LoggingConfig:level = \"info\"", + "system/LoggingConfig:redact = [\"password\",\"passwordHash\",\"token\",\"apiKey\",\"secret\",\"creditCard\",\"ssn\",\"authorization\"]", + "system/MaskingVisibilityRule:auditUnmask = true", + "system/MaskingVisibilityRule:defaultMasked = true", + "system/MaskingVisibilityRule:requireApproval = false", + "system/MessageQueueConfig:ssl = false", + "system/MetadataHistoryQueryOptions:includeMetadata = true", + "system/MetadataHistoryRetentionPolicy:autoCleanup = false", + "system/MetadataHistoryRetentionPolicy:cleanupIntervalHours = 24", + "system/MetadataManagerConfig:fallback = \"none\"", + "system/MetadataManagerConfig:formats = [\"typescript\",\"json\",\"yaml\"]", + "system/MetadataManagerConfig:tableName = \"sys_metadata\"", + "system/MetadataManagerConfig:watch = false", + "system/MetadataRecord:namespace = \"default\"", + "system/MetadataRecord:scope = \"platform\"", + "system/MetadataRecord:state = \"active\"", + "system/MetadataRecord:strategy = \"merge\"", + "system/MetadataRecord:version = 1", + "system/MetadataSaveOptions:create = true", + "system/MetadataSaveOptions:overwrite = true", + "system/MetricDefinition:enabled = true", + "system/MetricDefinition:labelNames = []", + "system/MetricExportConfig:interval = 60", + "system/MetricsConfig:aggregations = []", + "system/MetricsConfig:collectionInterval = 15", + "system/MetricsConfig:defaultLabels = {}", + "system/MetricsConfig:enabled = true", + "system/MetricsConfig:exports = []", + "system/MetricsConfig:metrics = []", + "system/MetricsConfig:slos = []", + "system/MiddlewareConfig:enabled = true", + "system/MiddlewareConfig:order = 100", + "system/MigrationPlan:reversible = true", + "system/MigrationPlan:statements = []", + "system/MigrationStatement:reversible = true", + "system/MultipartUploadConfig:enabled = true", + "system/MultipartUploadConfig:maxConcurrent = 4", + "system/MultipartUploadConfig:maxParts = 10000", + "system/MultipartUploadConfig:partSize = 10485760", + "system/MultipartUploadConfig:threshold = 104857600", + "system/MutualTLSConfig:clientCertRequired = false", + "system/MutualTLSConfig:enabled = false", + "system/ORSetElement:removed = false", + "system/ObjectStorageConfig:buckets = []", + "system/ObjectStorageConfig:enabled = true", + "system/ObjectStorageConfig:scope = \"global\"", + "system/Plan:active = true", + "system/Plan:currency = \"USD\"", + "system/QueueConfig:concurrency = 5", + "system/QueueConfig:priority = 0", + "system/RPO:unit = \"minutes\"", + "system/RTO:unit = \"minutes\"", + "system/RegistryConfig:visibility = \"private\"", + "system/RegistryUpstream:syncPolicy = \"auto\"", + "system/RegistryUpstream:timeout = 30000", + "system/RetryPolicy:backoffMs = 1000", + "system/RetryPolicy:backoffMultiplier = 1", + "system/RetryPolicy:jitter = false", + "system/RetryPolicy:maxRetries = 0", + "system/RetryPolicy:maxRetryDelayMs = 30000", + "system/SearchConfig:typoTolerance = true", + "system/SearchIndexConfig:replicas = 1", + "system/SearchIndexConfig:shards = 1", + "system/SecurityContextConfig:enabled = true", + "system/SecurityContextConfig:enforceOnRead = true", + "system/SecurityContextConfig:enforceOnWrite = true", + "system/SecurityContextConfig:failOpen = false", + "system/SecurityEventCorrelation:correlationId = true", + "system/SecurityEventCorrelation:enabled = true", + "system/SecurityEventCorrelation:linkAuthToAudit = true", + "system/SecurityEventCorrelation:linkEncryptionToAudit = true", + "system/SecurityEventCorrelation:linkMaskingToAudit = true", + "system/ServerCapabilities:compression = true", + "system/ServerCapabilities:httpVersions = [\"1.1\"]", + "system/ServerCapabilities:middleware = true", + "system/ServerCapabilities:routeParams = true", + "system/ServerCapabilities:serverPush = false", + "system/ServerCapabilities:sse = false", + "system/ServerCapabilities:streaming = true", + "system/ServerCapabilities:websocket = false", + "system/ServerRateLimitConfig:enabled = false", + "system/ServerRateLimitConfig:maxRequests = 100", + "system/ServerRateLimitConfig:windowMs = 60000", + "system/ServiceLevelIndicator:enabled = true", + "system/ServiceLevelObjective:alerts = []", + "system/ServiceLevelObjective:enabled = true", + "system/SettingsManifest:readPermission = \"setup.access\"", + "system/SettingsManifest:scope = \"tenant\"", + "system/SettingsManifest:version = 1", + "system/SettingsManifest:writePermission = \"setup.write\"", + "system/Span:attributes = {}", + "system/Span:events = []", + "system/Span:kind = \"internal\"", + "system/Span:links = []", + "system/Specifier:required = false", + "system/StackServerConfig:trustProxy = false", + "system/StorageConnection:useSSL = true", + "system/SupplierSecurityPolicy:enabled = true", + "system/SupplierSecurityPolicy:formalAssessmentThreshold = \"medium\"", + "system/SupplierSecurityPolicy:monitorChanges = true", + "system/SupplierSecurityPolicy:reassessmentIntervalDays = 365", + "system/SupplierSecurityPolicy:requirePreOnboardingAssessment = true", + "system/SupplierSecurityPolicy:requiredCertifications = []", + "system/SupplierSecurityRequirement:mandatory = true", + "system/Task:attempts = 0", + "system/Task:priority = \"normal\"", + "system/Task:queue = \"default\"", + "system/Task:status = \"pending\"", + "system/TaskRetryPolicy:backoffMultiplier = 2", + "system/TaskRetryPolicy:backoffStrategy = \"exponential\"", + "system/TaskRetryPolicy:initialDelayMs = 1000", + "system/TaskRetryPolicy:maxDelayMs = 60000", + "system/TaskRetryPolicy:maxRetries = 3", + "system/TenantUsage:apiRequestsThisMinute = 0", + "system/TenantUsage:currentObjectCount = 0", + "system/TenantUsage:currentRecordCount = 0", + "system/TenantUsage:currentStorageBytes = 0", + "system/TenantUsage:currentUsers = 0", + "system/TenantUsage:deploymentsToday = 0", + "system/TopicConfig:compressionType = \"none\"", + "system/TopicConfig:partitions = 1", + "system/TopicConfig:replicationFactor = 1", + "system/TraceContext:remote = false", + "system/TraceContext:sampled = true", + "system/TraceContext:traceFlags = 1", + "system/TraceContextPropagation:extract = true", + "system/TraceContextPropagation:formats = [\"w3c\"]", + "system/TraceContextPropagation:inject = true", + "system/TraceSamplingConfig:rules = []", + "system/TracingConfig:enabled = true", + "system/TracingConfig:propagation = {\"extract\":true,\"formats\":[\"w3c\"],\"inject\":true}", + "system/TracingConfig:traceIdGenerator = \"random\"", + "system/TrainingCourse:mandatory = false", + "system/TrainingPlan:enabled = true", + "system/TrainingPlan:gracePeriodDays = 30", + "system/TrainingPlan:recertificationIntervalDays = 365", + "system/TrainingPlan:reminderDaysBefore = 14", + "system/TrainingPlan:sendReminders = true", + "system/TrainingPlan:trackCompletion = true" + ] +} diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json new file mode 100644 index 0000000000..6002edc7e1 --- /dev/null +++ b/packages/spec/authorable-defaults/ui.json @@ -0,0 +1,132 @@ +{ + "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", + "category": "ui", + "defaults": [ + "ui/AIChatWindowProps:mode = \"float\"", + "ui/Action:refreshAfter = false", + "ui/Action:type = \"script\"", + "ui/ActionAi:exposed = false", + "ui/ActionParam:required = false", + "ui/AddRecordConfig:enabled = true", + "ui/AddRecordConfig:mode = \"inline\"", + "ui/AddRecordConfig:position = \"bottom\"", + "ui/App:active = true", + "ui/App:isDefault = false", + "ui/AppContextSelector:allValue = \"\"", + "ui/AppContextSelector:persist = \"query\"", + "ui/AppearanceConfig:showDescription = true", + "ui/ChartAnnotation:axis = \"y\"", + "ui/ChartAnnotation:style = \"dashed\"", + "ui/ChartAnnotation:type = \"line\"", + "ui/ChartAxis:logarithmic = false", + "ui/ChartAxis:showGridLines = true", + "ui/ChartConfig:showDataLabels = false", + "ui/ChartConfig:showLegend = true", + "ui/ChartInteraction:brush = false", + "ui/ChartInteraction:tooltips = true", + "ui/ChartSeries:variant = \"primary\"", + "ui/ChartSeries:yAxis = \"left\"", + "ui/ColumnPrefix:type = \"text\"", + "ui/DashboardHeader:showDescription = true", + "ui/DashboardHeader:showTitle = true", + "ui/DashboardWidget:type = \"metric\"", + "ui/ElementButtonProps:disabled = false", + "ui/ElementButtonProps:iconPosition = \"left\"", + "ui/ElementButtonProps:size = \"medium\"", + "ui/ElementButtonProps:variant = \"primary\"", + "ui/ElementFilterProps:layout = \"inline\"", + "ui/ElementFilterProps:showSearch = true", + "ui/ElementFormProps:mode = \"create\"", + "ui/ElementImageProps:fit = \"cover\"", + "ui/ElementMetadataViewerProps:detail = \"business\"", + "ui/ElementTextInputProps:disabled = false", + "ui/ElementTextInputProps:inputType = \"text\"", + "ui/ElementTextInputProps:required = false", + "ui/ElementTextProps:align = \"left\"", + "ui/ElementTextProps:variant = \"body\"", + "ui/FormField:span = \"auto\"", + "ui/FormSection:collapsed = false", + "ui/FormSection:collapsible = false", + "ui/FormSection:columns = 1", + "ui/FormView:type = \"simple\"", + "ui/GalleryConfig:cardSize = \"medium\"", + "ui/GalleryConfig:coverFit = \"cover\"", + "ui/GlobalFilter:scope = \"dashboard\"", + "ui/GroupNavItem:expanded = false", + "ui/GroupingField:collapsed = false", + "ui/GroupingField:order = \"asc\"", + "ui/HttpRequest:method = \"GET\"", + "ui/InlineAction:refreshAfter = false", + "ui/InlineAction:type = \"script\"", + "ui/JoinedReportBlock:type = \"tabular\"", + "ui/ListChartConfig:chartType = \"bar\"", + "ui/ListView:type = \"grid\"", + "ui/LocaleConfig:direction = \"ltr\"", + "ui/NavigationConfig:mode = \"page\"", + "ui/NavigationConfig:openNewTab = false", + "ui/NavigationConfig:preventNavigation = false", + "ui/NavigationConfig:size = \"auto\"", + "ui/NavigationContribution:priority = 200", + "ui/NumberFormat:style = \"decimal\"", + "ui/ObjectListView:type = \"grid\"", + "ui/ObjectUserFilters:element = \"dropdown\"", + "ui/Page:isDefault = false", + "ui/Page:kind = \"full\"", + "ui/Page:template = \"default\"", + "ui/Page:type = \"record\"", + "ui/PageAccordionProps:allowMultiple = false", + "ui/PageCardProps:bordered = true", + "ui/PageComponent:properties = {}", + "ui/PageHeaderProps:breadcrumb = true", + "ui/PageTabsProps:position = \"top\"", + "ui/PageTabsProps:type = \"line\"", + "ui/PageVariable:type = \"string\"", + "ui/PaginationConfig:pageSize = 25", + "ui/RecordActivityProps:enableMentions = true", + "ui/RecordActivityProps:enableReactions = false", + "ui/RecordActivityProps:enableThreading = false", + "ui/RecordActivityProps:filterMode = \"all\"", + "ui/RecordActivityProps:limit = 20", + "ui/RecordActivityProps:showCommentInput = true", + "ui/RecordActivityProps:showCompleted = false", + "ui/RecordActivityProps:showFilterToggle = true", + "ui/RecordActivityProps:showSubscriptionToggle = true", + "ui/RecordActivityProps:unifiedTimeline = true", + "ui/RecordChatterProps:collapsible = true", + "ui/RecordChatterProps:defaultCollapsed = false", + "ui/RecordChatterProps:position = \"sidebar\"", + "ui/RecordDetailsProps:columns = \"2\"", + "ui/RecordDetailsProps:layout = \"auto\"", + "ui/RecordHighlightsProps:layout = \"horizontal\"", + "ui/RecordRelatedListProps:limit = 5", + "ui/RecordRelatedListProps:relationshipValueField = \"id\"", + "ui/RecordRelatedListProps:showViewAll = true", + "ui/Report:drilldown = true", + "ui/Report:type = \"tabular\"", + "ui/ReportChart:showDataLabels = false", + "ui/ReportChart:showLegend = true", + "ui/ReportSort:direction = \"asc\"", + "ui/SelectionConfig:type = \"none\"", + "ui/SharingConfig:allowAnonymous = false", + "ui/SharingConfig:enabled = false", + "ui/Theme:mode = \"light\"", + "ui/TimelineConfig:scale = \"week\"", + "ui/UrlNavItem:target = \"_self\"", + "ui/UserActionsConfig:addRecordForm = false", + "ui/UserActionsConfig:editInline = false", + "ui/UserActionsConfig:filter = true", + "ui/UserActionsConfig:refresh = true", + "ui/UserActionsConfig:rowHeight = true", + "ui/UserActionsConfig:search = true", + "ui/UserActionsConfig:sort = true", + "ui/UserFilters:element = \"dropdown\"", + "ui/ViewSharing:type = \"collaborative\"", + "ui/ViewTab:isDefault = false", + "ui/ViewTab:pinned = false", + "ui/ViewTab:visible = true", + "ui/WidgetEvent:bubbles = false", + "ui/WidgetEvent:cancelable = false", + "ui/WidgetManifest:category = \"custom\"", + "ui/WidgetProperty:required = false" + ] +} diff --git a/packages/spec/scripts/authorable-defaults.test.ts b/packages/spec/scripts/authorable-defaults.test.ts new file mode 100644 index 0000000000..486fd1b834 --- /dev/null +++ b/packages/spec/scripts/authorable-defaults.test.ts @@ -0,0 +1,362 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The default-value ratchet (#4666), pinned at the altitude where its verdicts + * are decidable without running the whole generator. + * + * ## Why this file exists in the shape it does + * + * The gate itself lives inside `build-schemas.ts`, whose end-to-end verdict is + * proved on every CI run by `check:authorable-surface`. What that run CANNOT + * prove is that the comparison still MEANS anything: a diff function that + * returned `[]` unconditionally, or a fingerprint that collapsed every value to + * the same string, would leave the gate green on every tree — including the one + * with the flipped default. #4642 is this repo's standing example of a pin that + * had quietly become a no-op, and the whole point of #4666 is that a green gate + * had been telling everyone a default was unchanged. + * + * So the assertions below are deliberately about DISCRIMINATION — this input + * produces a finding, that one does not — and the first of them replays the + * exact sabotage the issue was opened on: `maxRetries`, `0 → 3`, one character. + * + * The second thing pinned here is the direction-B boundary. The maintainer ruled + * that constraints are NOT fingerprinted, and a boundary that lives only in a + * comment erodes the first time someone "improves" the fingerprint. It is + * asserted as a property instead: two shapes differing ONLY in their bounds and + * prose must fingerprint identically. + */ + +import { describe, expect, it } from 'vitest'; +import { + NO_DEFAULT, + authorableDefaultsShardTexts, + authoriseDefaultChanges, + collectAuthorableDefaults, + defaultFingerprintOf, + diffAuthorableDefaults, + formatDefaultEntry, + parseDefaultEntries, + parseDefaultEntry, + type DeclaredDefaultChange, +} from './lib/authorable-defaults'; +import { DEFAULT_CHANGES_BY_MAJOR } from './lib/default-changes'; + +/** Authorable keys as the gate models them: `key -> isRetired`. */ +const live = (...keys: string[]) => new Map(keys.map((k) => [k, false] as const)); + +const RETRY = 'automation/RetryPolicy:maxRetries'; + +describe('defaultFingerprintOf — normalisation', () => { + it('records the value, and the value alone', () => { + expect(defaultFingerprintOf(0)).toBe('0'); + expect(defaultFingerprintOf(3)).toBe('3'); + expect(defaultFingerprintOf(false)).toBe('false'); + expect(defaultFingerprintOf('GET')).toBe('"GET"'); + expect(defaultFingerprintOf(null)).toBe('null'); + }); + + it('is insensitive to object key ORDER — a re-spelling is not a change', () => { + // The churn this rules out: a `.default({ a, b })` literal re-typed with its + // keys the other way round is the same default, and a ratchet that reddened + // on it would be reworded away within a week. + expect(defaultFingerprintOf({ mode: 'a', retries: 1 })).toBe( + defaultFingerprintOf({ retries: 1, mode: 'a' }), + ); + expect(defaultFingerprintOf({ z: { b: 1, a: 2 } })).toBe(defaultFingerprintOf({ z: { a: 2, b: 1 } })); + }); + + it('is SENSITIVE to array order — an array default’s order is observable', () => { + expect(defaultFingerprintOf(['a', 'b'])).not.toBe(defaultFingerprintOf(['b', 'a'])); + }); + + it('carries no whitespace, so re-formatting cannot move it', () => { + expect(defaultFingerprintOf({ a: 1, b: [2, 3] })).toBe('{"a":1,"b":[2,3]}'); + }); + + it('distinguishes values a loose comparison would fuse', () => { + // `0`/`false`/`""`/`null` are the defaults whose flips are most dangerous and + // most likely to be collapsed by a sloppy fingerprint. + const fps = [0, false, '', null, '0'].map(defaultFingerprintOf); + expect(new Set(fps).size).toBe(fps.length); + }); +}); + +describe('collectAuthorableDefaults — what the fingerprint reads', () => { + it('reads the emitted `default` and nothing else (direction B: constraints excluded)', () => { + // The maintainer ruling on #4666: a tightened bound REJECTS a document + // loudly, so it is a different and self-announcing class. If this ever + // fails, someone has widened the ratchet into direction A — which was + // considered and declined, not overlooked. + const loose = [ + ['system/Job', { properties: { maxRetries: { type: 'integer', minimum: 0, default: 0 } } }], + ] as const; + const tightened = [ + [ + 'system/Job', + { + properties: { + maxRetries: { + type: 'integer', + minimum: 1, + maximum: 10, + default: 0, + description: 'reworded prose', + }, + }, + }, + ], + ] as const; + expect([...collectAuthorableDefaults(tightened)]).toEqual([...collectAuthorableDefaults(loose)]); + }); + + it('records falsy defaults and skips keys that have none', () => { + const collected = collectAuthorableDefaults([ + [ + 'system/Job', + { + properties: { + enabled: { default: false }, + name: { type: 'string' }, + retries: { default: 0 }, + // `retiredKey()` is z.never() → `{ not: {} }`, which carries no default. + legacy: { not: {} }, + }, + }, + ], + ]); + expect([...collected]).toEqual([ + ['system/Job:enabled', 'false'], + ['system/Job:retries', '0'], + ]); + }); +}); + +describe('diffAuthorableDefaults — the discrimination the gate rests on', () => { + it('catches the #4661 sabotage: one character, one finding, named', () => { + const changes = diffAuthorableDefaults({ + baseline: new Map([[RETRY, '0']]), + current: new Map([[RETRY, '3']]), + baselineKeys: live(RETRY), + currentKeys: live(RETRY), + }); + expect(changes).toEqual([{ key: RETRY, kind: 'changed', from: '0', to: '3' }]); + }); + + it('reports nothing when nothing moved', () => { + expect( + diffAuthorableDefaults({ + baseline: new Map([[RETRY, '0']]), + current: new Map([[RETRY, '0']]), + baselineKeys: live(RETRY), + currentKeys: live(RETRY), + }), + ).toEqual([]); + }); + + it('reads a gained default as `added` and a lost one as `removed`', () => { + const gained = diffAuthorableDefaults({ + baseline: new Map(), + current: new Map([[RETRY, '3']]), + baselineKeys: live(RETRY), + currentKeys: live(RETRY), + }); + expect(gained).toEqual([{ key: RETRY, kind: 'added', from: NO_DEFAULT, to: '3' }]); + + const lost = diffAuthorableDefaults({ + baseline: new Map([[RETRY, '3']]), + current: new Map(), + baselineKeys: live(RETRY), + currentKeys: live(RETRY), + }); + expect(lost).toEqual([{ key: RETRY, kind: 'removed', from: '3', to: NO_DEFAULT }]); + }); + + it('does not charge a NEW key for having a default', () => { + // No deployed document could have omitted a key that did not exist, so its + // default cannot have changed anyone's behaviour. The authorable-surface + // ratchet records the addition; this one must stay quiet or every new key + // with a default would demand a declaration. + expect( + diffAuthorableDefaults({ + baseline: new Map(), + current: new Map([['system/Job:freshKey', '5']]), + baselineKeys: new Map(), + currentKeys: live('system/Job:freshKey'), + }), + ).toEqual([]); + }); + + it('leaves a removed or tombstoned key to the surface gate', () => { + // Both would otherwise read as "the default vanished" and demand a second + // declaration for one retirement — checks (a)/(b)/(c) already own these. + expect( + diffAuthorableDefaults({ + baseline: new Map([['system/Job:gone', '1']]), + current: new Map(), + baselineKeys: live('system/Job:gone'), + currentKeys: new Map(), + }), + ).toEqual([]); + expect( + diffAuthorableDefaults({ + baseline: new Map([['system/Job:tombstoned', '1']]), + current: new Map(), + baselineKeys: live('system/Job:tombstoned'), + currentKeys: new Map([['system/Job:tombstoned', true]]), + }), + ).toEqual([]); + }); +}); + +describe('authoriseDefaultChanges — the acknowledged-change exit stays honest', () => { + const change = { key: RETRY, kind: 'changed', from: '0', to: '3' } as const; + const declare = (over: Partial = {}): DeclaredDefaultChange => ({ + key: RETRY, + from: '0', + to: '3', + reason: 'retry became opt-out; write `maxRetries: 0` to keep the old behaviour', + ...over, + }); + + it('refuses a change nobody declared', () => { + const out = authoriseDefaultChanges([change], {}, 17, new Map([[RETRY, '3']]), live(RETRY)); + expect(out.unauthorised).toEqual([change]); + expect(out.authorised).toEqual([]); + }); + + it('accepts a declaration that pins BOTH endpoints, and carries its reason', () => { + const out = authoriseDefaultChanges( + [change], + { 17: [declare()] }, + 17, + new Map([[RETRY, '3']]), + live(RETRY), + ); + expect(out.unauthorised).toEqual([]); + expect(out.stale).toEqual([]); + expect(out.authorised[0].declared[0].reason).toContain('opt-out'); + }); + + it('refuses a declaration whose `from` is not where the baseline actually sits', () => { + // The endpoint the declaration's author does NOT control. Without this a + // single row would bless every future move of the same key. + const out = authoriseDefaultChanges( + [change], + { 17: [declare({ from: '1', to: '3' })] }, + 17, + new Map([[RETRY, '3']]), + live(RETRY), + ); + expect(out.unauthorised).toEqual([change]); + }); + + it('authorises nothing from a PAST major', () => { + const out = authoriseDefaultChanges( + [change], + { 16: [declare()] }, + 17, + new Map([[RETRY, '3']]), + live(RETRY), + ); + expect(out.unauthorised).toEqual([change]); + expect(out.stale).toEqual([]); + }); + + it('DIES when it stops being true: the chain tip must equal what the build emits', () => { + // The property that separates this from an allowlist. The default was moved + // again (or reverted) and the row was left behind — it now pre-approves a + // value nobody wrote down, so it goes red instead of covering it. + const reverted = authoriseDefaultChanges([], { 17: [declare()] }, 17, new Map([[RETRY, '0']]), live(RETRY)); + expect(reverted.stale).toEqual([ + { key: RETRY, claims: '3', emits: '0', why: 'chain-tip-mismatch' }, + ]); + + const movedAgain = authoriseDefaultChanges([], { 17: [declare()] }, 17, new Map([[RETRY, '5']]), live(RETRY)); + expect(movedAgain.stale[0]).toMatchObject({ claims: '3', emits: '5' }); + }); + + it('requires a multi-hop chain to MEET, and authorises from wherever the baseline forked', () => { + const chain = [declare({ from: '0', to: '3' }), declare({ from: '3', to: '5' })]; + const live5 = new Map([[RETRY, '5']]); + + // A branch forked before either hop: the whole chain covers 0 → 5. + const fromStart = authoriseDefaultChanges( + [{ key: RETRY, kind: 'changed', from: '0', to: '5' }], + { 17: chain }, + 17, + live5, + live(RETRY), + ); + expect(fromStart.unauthorised).toEqual([]); + expect(fromStart.authorised[0].declared).toHaveLength(2); + + // A branch forked between them: only the second hop is its transition. + const fromMiddle = authoriseDefaultChanges( + [{ key: RETRY, kind: 'changed', from: '3', to: '5' }], + { 17: chain }, + 17, + live5, + live(RETRY), + ); + expect(fromMiddle.authorised[0].declared).toHaveLength(1); + + // A gap in the chain hides a value that really shipped. + const gapped = authoriseDefaultChanges( + [], + { 17: [declare({ from: '0', to: '3' }), declare({ from: '4', to: '5' })] }, + 17, + live5, + live(RETRY), + ); + expect(gapped.stale.map((s) => s.why)).toContain('chain-not-contiguous'); + }); + + it('leaves a declaration about a key this build no longer emits alone', () => { + // The expected end state once the key is retired — the surface gate owns + // that verdict, exactly as check (b2) does for RETIRED_KEYS_BY_MAJOR. + const out = authoriseDefaultChanges([], { 17: [declare()] }, 17, new Map(), new Map()); + expect(out.stale).toEqual([]); + }); +}); + +describe('the recorded artifact round-trips', () => { + it('serialises and re-parses to the same fingerprints', () => { + const defaults = new Map([ + ['system/Job:retries', '0'], + ['ui/Theme:mode', '"light"'], + ['ui/Theme:tokens', '{"a":1,"b":[2,3]}'], + ]); + const parsed = parseDefaultEntries( + [...authorableDefaultsShardTexts(defaults).values()].flatMap( + (text) => (JSON.parse(text) as { defaults: string[] }).defaults, + ), + ); + expect(parsed).toEqual(defaults); + }); + + it('splits on the FIRST separator, so a fingerprint may contain one', () => { + // A string default really can be `" = "`. Splitting anywhere else would + // corrupt exactly the values hardest to notice. + const entry = formatDefaultEntry('ui/Theme:sep', '" = "'); + expect(parseDefaultEntry(entry)).toEqual({ key: 'ui/Theme:sep', fingerprint: '" = "' }); + }); + + it('refuses a line it cannot parse rather than guessing', () => { + expect(() => parseDefaultEntry('ui/Theme:sep')).toThrow(/cannot parse/); + }); +}); + +describe('DEFAULT_CHANGES_BY_MAJOR itself', () => { + it('holds no row that declares a non-change or an empty reason', () => { + // Guards the table the way the gate guards the schemas: a row with + // `from === to` acknowledges nothing, and an empty `reason` defeats the one + // thing a build prints when it accepts a flip. + for (const [major, rows] of Object.entries(DEFAULT_CHANGES_BY_MAJOR)) { + for (const row of rows) { + expect(row.from, `${major} ${row.key}`).not.toBe(row.to); + expect(row.reason.trim().length, `${major} ${row.key}`).toBeGreaterThan(20); + expect(row.key, `${major} ${row.key}`).toMatch(/^[a-z0-9-]+\/[A-Za-z0-9_]+:.+$/); + } + } + }); +}); diff --git a/packages/spec/scripts/build-schemas-check-mode.test.ts b/packages/spec/scripts/build-schemas-check-mode.test.ts index 1fd6f692fa..c069e8316f 100644 --- a/packages/spec/scripts/build-schemas-check-mode.test.ts +++ b/packages/spec/scripts/build-schemas-check-mode.test.ts @@ -68,7 +68,13 @@ import { authorableSurfaceShardTexts, schemaManifestShardTexts, writeShards, + type ShardArrayField, } from './lib/sharded-artifacts'; +import { + AUTHORABLE_DEFAULTS_DIR_NAME, + authorableDefaultsShardTexts, + parseDefaultEntries, +} from './lib/authorable-defaults'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const PKG = path.resolve(HERE, '..'); @@ -103,10 +109,25 @@ function writeManifestShards(dir: string, schemas: readonly string[]): string { } /** Aggregate a shard directory back into its sorted key set. */ -function readShardKeys(dir: string, field: 'keys' | 'schemas'): string[] { +function readShardKeys(dir: string, field: ShardArrayField): string[] { return aggregateCategoryShards(dir, field)?.entries ?? []; } +/** + * Write the recorded default fingerprints as canonical authorable-defaults + * shards (#4666). + * + * The sandbox has to carry this artifact for the same reason it carries the + * authorable-surface one: the default-value ratchet runs on every invocation, + * so a fixture tree without it is not a smaller version of a real tree — it is + * a tree whose generated record is missing, which `--check` reports as such. + * Seeding it keeps every fixture below judging the contract it means to judge. + */ +function writeDefaultsShards(dir: string, entries: readonly string[]): string { + writeShards(dir, authorableDefaultsShardTexts(parseDefaultEntries([...entries].sort()))); + return shardBytes(dir); +} + /** * Every run loads the entire spec surface and emits ~1700 JSON Schemas (~7s * alone, more under turbo's parallel test load). A timeout here should mean @@ -126,9 +147,11 @@ let sandbox: string; let script: string; let manifestDir: string; let surfaceDir: string; +let defaultsDir: string; let surfaceBasePath: string; let pristine: string[]; let pristineSurface: string[]; +let pristineDefaults: string[]; /** The generator's own description line for the in-tree anchor, so fixtures * written here are byte-canonical exactly the way `gen:schema` writes it — * a hand-rolled string would trip the anchor's own hand-edit check (#5235). */ @@ -165,8 +188,13 @@ const readSurfaceBase = () => fs.readFileSync(surfaceBasePath, 'utf8'); beforeAll(() => { pristine = readShardKeys(path.join(PKG, SCHEMA_MANIFEST_DIR_NAME), 'schemas'); pristineSurface = readShardKeys(path.join(PKG, AUTHORABLE_SURFACE_DIR_NAME), 'keys'); + pristineDefaults = readShardKeys(path.join(PKG, AUTHORABLE_DEFAULTS_DIR_NAME), 'defaults'); expect(pristine.length, `${SCHEMA_MANIFEST_DIR_NAME}/ is empty — it is a committed artifact`).toBeGreaterThan(0); expect(pristineSurface.length, `${AUTHORABLE_SURFACE_DIR_NAME}/ is empty — it is a committed artifact`).toBeGreaterThan(0); + expect( + pristineDefaults.length, + `${AUTHORABLE_DEFAULTS_DIR_NAME}/ is empty — it is a committed artifact (#4666)`, + ).toBeGreaterThan(0); const realBase = path.join(PKG, 'authorable-surface.base.json'); if (!fs.existsSync(realBase)) { throw new Error( @@ -184,15 +212,20 @@ beforeAll(() => { script = path.join(sandbox, 'scripts', 'build-schemas.ts'); manifestDir = path.join(sandbox, SCHEMA_MANIFEST_DIR_NAME); surfaceDir = path.join(sandbox, AUTHORABLE_SURFACE_DIR_NAME); + defaultsDir = path.join(sandbox, AUTHORABLE_DEFAULTS_DIR_NAME); surfaceBasePath = path.join(sandbox, 'authorable-surface.base.json'); // The authorable-surface ratchet runs after the manifest one; give it the // committed snapshot so a check that gets that far judges the same contract. writeSurfaceShards(surfaceDir, pristineSurface); + // Same for the default-value ratchet (#4666), which runs after both: without + // its committed record every fixture would fail on a missing artifact rather + // than on the thing it is testing. + writeDefaultsShards(defaultsDir, pristineDefaults); // Anchor for the #4650 deletion check: a git repo whose origin/main holds the // committed baseline. Only the baseline is tracked — src/node_modules stay // symlinked, untracked reads the same as any dirty worktree. git('init', '-q', '-b', 'main', '.'); - git('add', AUTHORABLE_SURFACE_DIR_NAME); + git('add', AUTHORABLE_SURFACE_DIR_NAME, AUTHORABLE_DEFAULTS_DIR_NAME); git('commit', '-q', '-m', `baseline: committed ${AUTHORABLE_SURFACE_DIR_NAME}/`); // The in-tree anchor (#5235), authentic by construction: it mirrors the // baseline at the commit just made, which stays reachable from origin/main for @@ -1827,12 +1860,16 @@ describe('build-schemas.ts — check (b) matches the exact retired key, not its writeManifestShards(path.join(box, SCHEMA_MANIFEST_DIR_NAME), pristine); boxSurfaceDir = path.join(box, AUTHORABLE_SURFACE_DIR_NAME); writeSurfaceShards(boxSurfaceDir, pristineSurface); + // The #4666 default ratchet runs on every invocation, so every box needs its + // committed record too — otherwise a fixture fails on a missing artifact + // instead of on the removal/rename it is actually testing. + writeDefaultsShards(path.join(box, AUTHORABLE_DEFAULTS_DIR_NAME), pristineDefaults); boxScript = path.join(box, 'scripts', 'build-schemas.ts'); boxRegistry = path.join(box, 'src', 'migrations', 'registry.ts'); pristineRegistry = fs.readFileSync(boxRegistry, 'utf8'); boxGit('init', '-q', '-b', 'main', '.'); - boxGit('add', AUTHORABLE_SURFACE_DIR_NAME); + boxGit('add', AUTHORABLE_SURFACE_DIR_NAME, AUTHORABLE_DEFAULTS_DIR_NAME); boxGit('commit', '-q', '-m', `baseline: committed ${AUTHORABLE_SURFACE_DIR_NAME}/`); fs.writeFileSync( path.join(box, 'authorable-surface.base.json'), @@ -2137,10 +2174,11 @@ describe('build-schemas.ts — a deleted manifest key must prove itself (#4725)' writeManifestShards(boxManifestDir, pristine); writeSurfaceShards(boxSurfaceDir, pristineSurface); + writeDefaultsShards(path.join(box, AUTHORABLE_DEFAULTS_DIR_NAME), pristineDefaults); boxGit('init', '-q', '-b', 'main', '.'); // BOTH artifacts tracked here: the merge-base manifest is what this gate // reads, and the surface baseline keeps the #4650 gate honest alongside it. - boxGit('add', SCHEMA_MANIFEST_DIR_NAME, AUTHORABLE_SURFACE_DIR_NAME); + boxGit('add', SCHEMA_MANIFEST_DIR_NAME, AUTHORABLE_SURFACE_DIR_NAME, AUTHORABLE_DEFAULTS_DIR_NAME); boxGit('commit', '-q', '-m', 'baseline: committed manifest + authorable surface'); fs.writeFileSync( path.join(box, 'authorable-surface.base.json'), @@ -2466,7 +2504,7 @@ describe('build-schemas.ts — check (c) dates a tombstone by its exact key (#58 * so those lines read as deleted by this build (the main sandbox's shape). */ const seedBoxBase = (...extra: string[]): void => { writeSurfaceShards(boxSurfaceDir, [...pristineSurface, ...extra].sort()); - boxGit('add', AUTHORABLE_SURFACE_DIR_NAME); + boxGit('add', AUTHORABLE_SURFACE_DIR_NAME, AUTHORABLE_DEFAULTS_DIR_NAME); boxGit('commit', '-q', '--allow-empty', '-m', 'base variant'); boxGit('update-ref', 'refs/remotes/origin/main', boxGit('rev-parse', 'HEAD')); writeSurfaceShards(boxSurfaceDir, pristineSurface); @@ -2495,12 +2533,16 @@ describe('build-schemas.ts — check (c) dates a tombstone by its exact key (#58 writeManifestShards(path.join(box, SCHEMA_MANIFEST_DIR_NAME), pristine); boxSurfaceDir = path.join(box, AUTHORABLE_SURFACE_DIR_NAME); writeSurfaceShards(boxSurfaceDir, pristineSurface); + // The #4666 default ratchet runs on every invocation, so every box needs its + // committed record too — otherwise a fixture fails on a missing artifact + // instead of on the removal/rename it is actually testing. + writeDefaultsShards(path.join(box, AUTHORABLE_DEFAULTS_DIR_NAME), pristineDefaults); boxScript = path.join(box, 'scripts', 'build-schemas.ts'); boxRegistry = path.join(box, 'src', 'migrations', 'registry.ts'); pristineRegistry = fs.readFileSync(boxRegistry, 'utf8'); boxGit('init', '-q', '-b', 'main', '.'); - boxGit('add', AUTHORABLE_SURFACE_DIR_NAME); + boxGit('add', AUTHORABLE_SURFACE_DIR_NAME, AUTHORABLE_DEFAULTS_DIR_NAME); boxGit('commit', '-q', '-m', `baseline: committed ${AUTHORABLE_SURFACE_DIR_NAME}/`); fs.writeFileSync( path.join(box, 'authorable-surface.base.json'), diff --git a/packages/spec/scripts/build-schemas.ts b/packages/spec/scripts/build-schemas.ts index 3f233b9eef..7f9c6b3b54 100644 --- a/packages/spec/scripts/build-schemas.ts +++ b/packages/spec/scripts/build-schemas.ts @@ -39,6 +39,19 @@ import { writeShards, type GitRun, } from './lib/sharded-artifacts'; +// The #4666 default-value ratchet: what an author gets when they OMIT a key. +// Its own module because the fingerprint's normalisation rules — and the +// direction-B boundary that keeps constraints out of them — are assertable +// without running the whole generator (scripts/authorable-defaults.test.ts). +import { + AUTHORABLE_DEFAULTS_DIR_NAME, + authorableDefaultsShardTexts, + authoriseDefaultChanges, + collectAuthorableDefaults, + diffAuthorableDefaults, + parseDefaultEntries, +} from './lib/authorable-defaults'; +import { DEFAULT_CHANGES_BY_MAJOR } from './lib/default-changes'; import { RETIRED_DEFS_BY_MAJOR, RETIRED_KEYS_BY_MAJOR } from '../src/migrations/registry'; import { getMetadataTypeSchema, @@ -1702,8 +1715,18 @@ function checkManifestRemovals(git: GitRun, baseRev: string | null): void { process.exit(1); } +/** + * The baseline this run resolved, kept for the default-value ratchet further + * down (#4666), which needs the same two facts check (c) needs: which upstream + * rev the comparison is anchored on, and which keys were AUTHORABLE there. It + * is one resolution, shared — a second `resolveSurfaceBase()` call would ask git + * the same question twice and could answer it differently. + */ +let resolvedSurfaceBase: { rev: string; doc: AuthorableSurface } | null = null; + { const base = resolveSurfaceBase(); + resolvedSurfaceBase = base; // Whole defs first: check (c) below waives every baseline line under a def this // build stopped emitting, on the grounds that this gate adjudicates it. Running // it first is what makes that deferral true rather than circular. @@ -2021,6 +2044,254 @@ if (surfaceChanged && !CHECK) { ); } +// ─── The default-value ratchet (#4666) ─────────────────────────────── +// +// The three ratchets above all measure the SHAPE of the contract: which schemas +// are published, which keys they carry, whether a key is live or tombstoned. +// None of them can see what a key MEANS WHEN THE AUTHOR OMITS IT — and that is +// the one change in this file's whole subject matter that alters the behaviour +// of already-deployed metadata with no error, no warning and no diff on the +// author's side. +// +// Measured, not theorised: on #4661's branch, moving `retryPolicyShape()`'s +// `maxRetries` default from 0 to 3 — one character — left this entire script +// green ("✅ Successfully generated 1703 schemas."). The only thing that caught +// it was a runtime pin an author had remembered to hand-write. Every recording +// channel missed it for a different reason: the authorable-surface comparison +// reads key NAMES, `retiredKey()` tombstones fire on live → retired, and +// spec-changes.json is a projection of the ADR-0087 registries, which a default +// change need not touch at all. +// +// Why defaults specifically, and why now: omitting optional keys is the normal +// mode of AI-authored metadata (ADR-0033), so a default covers far more live +// behaviour than it did in the hand-written era. `maxRetries`, `enabled`, +// `required`, any `allow*` — flipping one of those is a reliability or security +// event, and a silent one. +// +// ⛔ CONSTRAINTS ARE DELIBERATELY NOT RECORDED HERE (maintainer ruling on #4666, +// direction B). The asymmetry that decided it: tightening `.max()` REJECTS an +// existing document — loud, diagnosable, discovered from CI — while a default +// flip rejects nothing and is discovered from a customer incident. Recording +// both (direction A) covers more but makes this ratchet markedly noisier, and +// #4535 §1 is already complaining that the authorable surface over-collects. +// The exclusion is structural rather than a matter of discipline: the +// fingerprint reads exactly one field of the emitted schema, `default`, so a +// bound or a `.describe()` cannot move it even by accident. +const AUTHORABLE_DEFAULTS_DIR = path.resolve(__dirname, `../${AUTHORABLE_DEFAULTS_DIR_NAME}`); +const DEFAULTS_DIR_LABEL = `${AUTHORABLE_DEFAULTS_DIR_NAME}/`; + +const currentDefaults = collectAuthorableDefaults(generatedSchemas); + +let defaultsTexts: Map | null = null; +let committedDefaults: Map | null = null; +try { + const read = aggregateCategoryShards(AUTHORABLE_DEFAULTS_DIR, 'defaults'); + if (read) { + defaultsTexts = new Map(read.shards.map((s) => [s.name, s.raw])); + committedDefaults = parseDefaultEntries(read.entries); + } +} catch (error) { + console.error(`\n❌ Failed to read ${DEFAULTS_DIR_LABEL}: ${error instanceof Error ? error.message : error}`); + process.exit(1); +} + +// The baseline, in strict preference order. Both halves of a baseline — the +// recorded defaults AND the authorable keys they belong to — always come from +// the SAME anchor, or "this key is new" would be decided against one commit +// while its default was read from another. +// +// `upstream` — authorable-defaults/ at the merge base with origin/main, the +// commit under test cannot rewrite it. This is the anchor that matters, and +// it is the one every run gets once this ratchet exists upstream. +// `in-tree` — the committed authorable-defaults/ in THIS tree. Weaker: the +// commit under test owns these bytes. Used only where the upstream read has +// nothing to return — a merge base that predates this ratchet (every branch +// in flight when it lands), and the offline builds #5235 describes, which +// are immutable already-merged trees with no "what did this PR change +// relative to main" question to ask. It is strictly additive: it can only +// ADD findings, never remove one the upstream anchor would have made. +// +// With NEITHER available the comparison is skipped — and that skip is not the +// #4690 hole, because it is not reachable by deleting anything: the artifact's +// staleness check below runs unconditionally, so a tree with no +// authorable-defaults/ fails there in `--check` mode whatever this block +// decided. Which anchor was used is PRINTED, every run. +let defaultsBaseline: { defaults: Map; keys: Map; label: string } | null = + null; +if (resolvedSurfaceBase) { + const upstream = readSurfaceKeysAtRev( + gitInPackage, + resolvedSurfaceBase.rev, + AUTHORABLE_DEFAULTS_DIR_NAME, + 'defaults', + `authorable-defaults change check (#4666)`, + ); + if (upstream) { + const keys = new Map(); + for (const entry of resolvedSurfaceBase.doc.keys ?? []) { + keys.set(carryAuthorableKey(entry.replace(RETIRED_MARK, '')), entry.endsWith(RETIRED_MARK)); + } + defaultsBaseline = { + defaults: parseDefaultEntries(upstream.entries), + keys, + label: `upstream ${resolvedSurfaceBase.rev.slice(0, 12)}`, + }; + } +} +if (!defaultsBaseline && committedDefaults && surfaceDoc) { + const keys = new Map(); + for (const entry of surfaceDoc.keys) { + keys.set(carryAuthorableKey(entry.replace(RETIRED_MARK, '')), entry.endsWith(RETIRED_MARK)); + } + defaultsBaseline = { + defaults: committedDefaults, + keys, + label: 'in-tree (this commit owns these bytes — no upstream baseline was reachable)', + }; +} + +if (!defaultsBaseline) { + console.log( + `\nℹ️ ${DEFAULTS_DIR_LABEL} has no baseline to compare against — neither the merge base nor\n` + + ` this tree carries one, so this run RECORDS the defaults rather than adjudicating them\n` + + ` (#4666). The artifact check below still runs: a tree missing ${DEFAULTS_DIR_LABEL} fails\n` + + ` \`check:authorable-surface\` regardless of what this block decided.`, + ); +} else { + const changes = diffAuthorableDefaults({ + baseline: defaultsBaseline.defaults, + current: currentDefaults, + baselineKeys: defaultsBaseline.keys, + currentKeys, + }); + const { authorised, unauthorised, stale } = authoriseDefaultChanges( + changes, + DEFAULT_CHANGES_BY_MAJOR, + CURRENT_MAJOR, + currentDefaults, + currentKeys, + ); + + // A stale declaration is judged whether or not anything changed today: it is + // the property that stops this table decaying into an allowlist. Reported + // FIRST, because a chain that no longer describes reality also explains why a + // change below looks unauthorised. + if (stale.length > 0) { + console.error( + `\n❌ ${stale.length} DEFAULT_CHANGES_BY_MAJOR declaration(s) at major ${CURRENT_MAJOR} no longer describe reality:`, + ); + for (const s of stale) { + console.error( + s.why === 'chain-tip-mismatch' + ? ` - ${s.key}: declared to end at ${s.claims}, but this build emits ${s.emits}` + : ` - ${s.key}: declared hops do not meet (${s.claims})`, + ); + } + console.error( + `\n A declared default change is a claim about a value this build EMITS, re-checked on\n` + + ` every run — that is what keeps it from becoming an allowlist nobody re-reads. The\n` + + ` default moved again (or was reverted) and the declaration was left behind, so it now\n` + + ` pre-approves a value nobody wrote down.\n\n` + + ` Append the new hop to the key's chain in scripts/lib/default-changes.ts (the chain\n` + + ` must be contiguous: each hop's \`from\` is the previous hop's \`to\`), or — if the\n` + + ` default should not have moved at all — restore it in the schema.`, + ); + process.exit(1); + } + + if (authorised.length > 0) { + // Printed in full, every run. An acknowledged flip that passes in silence is + // the failure #4690 names; this is the exit announcing itself. + console.log(`\n📌 ${authorised.length} declared default change(s) accepted (#4666):`); + for (const { change, declared } of authorised) { + console.log(` - ${change.key}: ${change.from} → ${change.to}`); + for (const hop of declared) console.log(` ${hop.reason}`); + } + } + + if (unauthorised.length > 0) { + console.error( + `\n❌ ${unauthorised.length} authorable key(s) changed the DEFAULT they apply when the author omits them:`, + ); + for (const c of unauthorised) console.error(` - ${c.key}: ${c.from} → ${c.to} (${c.kind})`); + console.error( + `\n Baseline: ${defaultsBaseline.label}\n\n` + + ` A default decides what already-deployed metadata does when it does NOT write the key,\n` + + ` and omitting optional keys is the normal mode of AI-authored metadata (ADR-0033). So\n` + + ` this change reaches every document that stayed silent about ${unauthorised.length === 1 ? 'this key' : 'these keys'} — with no\n` + + ` parse error, no warning, and nothing in the author's diff. Unlike a tightened\n` + + ` constraint, which rejects the document loudly, there is no moment where anyone finds\n` + + ` out (#4666, measured on #4661).\n\n` + + ` If the change is intended, declare it — exactly, by \`\${defKey}:\${name}\` — in\n` + + ` DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts), under \`${CURRENT_MAJOR}: [ … ]\`:\n\n` + + unauthorised + .map( + (c) => + ` {\n` + + ` key: '${c.key}',\n` + + ` from: '${c.from}',\n` + + ` to: '${c.to}',\n` + + ` reason: '…what changes for a consumer who relied on ${c.from}, and what they should write to keep it…',\n` + + ` },\n`, + ) + .join('') + + `\n \`reason\` is printed by every build that accepts the change, so write it for the\n` + + ` consumer who is about to be surprised. Both endpoints are re-checked on every run\n` + + ` against sources you do not control — \`from\` against the baseline, \`to\` against what\n` + + ` the build emits — so the declaration dies the moment it stops being true.\n\n` + + ` If the default was NOT meant to move, restore it in the schema. And if the value\n` + + ` genuinely has to change for existing documents too, add a \`semantic\` entry to this\n` + + ` major's step in src/migrations/registry.ts so it reaches spec-changes.json, the\n` + + ` upgrade guide and \`os migrate meta\`.`, + ); + process.exit(1); + } + + if (changes.length === 0) { + console.log( + `\n🔒 ${DEFAULTS_DIR_LABEL} verified against ${defaultsBaseline.label} — ` + + `${currentDefaults.size} default(s) unchanged (#4666).`, + ); + } +} + +// The artifact itself, on the same byte-for-byte terms as its two siblings: a +// generated file whose every difference must come from the generator (#4662). +// Reached only after the adjudication above, so `gen:schema` can never absorb an +// undeclared change into the record it is supposed to be evidence for. +const canonicalDefaultsTexts = authorableDefaultsShardTexts(currentDefaults); +const staleDefaultsShards = [...canonicalDefaultsTexts] + .filter(([name, text]) => defaultsTexts?.get(name) !== text) + .map(([name]) => name); +const orphanDefaultsShards = [...(defaultsTexts?.keys() ?? [])].filter( + (name) => !canonicalDefaultsTexts.has(name), +); +const defaultsChanged = + defaultsTexts === null || staleDefaultsShards.length > 0 || orphanDefaultsShards.length > 0; +if (defaultsChanged && CHECK) { + console.error(`\n❌ ${DEFAULTS_DIR_LABEL} is out of date or hand-edited (#4666).`); + for (const name of staleDefaultsShards) { + console.error(` ~ ${AUTHORABLE_DEFAULTS_DIR_NAME}/${name}.json (stale)`); + } + for (const name of orphanDefaultsShards) { + console.error(` - ${AUTHORABLE_DEFAULTS_DIR_NAME}/${name}.json (no default in this category)`); + } + console.error( + `\n Any CHANGE to an existing key's default already exited above, so reaching here means\n` + + ` the record is behind on a NEW key's default, or its bytes are not what the generator\n` + + ` writes. Run \`pnpm --filter @objectstack/spec gen:schema\` and commit the result.`, + ); + process.exit(1); +} +if (defaultsChanged && !CHECK) { + const { written, removed } = writeShards(AUTHORABLE_DEFAULTS_DIR, canonicalDefaultsTexts); + console.log( + `\n🎚️ ${DEFAULTS_DIR_LABEL} ${defaultsTexts ? 'updated' : 'created'} (${currentDefaults.size} defaults) — commit it.` + + (written.length > 0 ? `\n touched: ${written.map((n) => `${n}.json`).join(', ')}` : '') + + (removed.length > 0 ? `\n removed: ${removed.map((n) => `${n}.json`).join(', ')}` : ''), + ); +} + // ─── Generate Bundled Schema ───────────────────────────────────────── // Single-file bundled schema containing all generated schemas for IDE autocomplete diff --git a/packages/spec/scripts/check-generated.ts b/packages/spec/scripts/check-generated.ts index 076db3c020..4797e0330c 100644 --- a/packages/spec/scripts/check-generated.ts +++ b/packages/spec/scripts/check-generated.ts @@ -84,7 +84,7 @@ const GATED: ReadonlyArray<{ { check: 'check:authorable-surface', gen: 'gen:schema', - artifact: 'authorable-surface/ (+ its .base.json anchor) + JSON schemas', + artifact: 'authorable-surface/ + authorable-defaults/ (+ its .base.json anchor) + JSON schemas', }, // Reads the BUILT `dist/*.d.ts`, not the source. On a stale dist it reports // every export added since the last build as a "breaking removal" — a phantom diff --git a/packages/spec/scripts/lib/authorable-defaults.ts b/packages/spec/scripts/lib/authorable-defaults.ts new file mode 100644 index 0000000000..2720ef872f --- /dev/null +++ b/packages/spec/scripts/lib/authorable-defaults.ts @@ -0,0 +1,403 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * authorable-defaults.ts — the DEFAULT-VALUE ratchet (#4666). + * + * ## The blind spot this closes + * + * `authorable-surface/` ratchets the SET of keys an author may write. Nothing + * ratcheted what those keys *mean when the author omits them*. The same key's + * `.default()` could move and every channel stayed quiet: + * + * - the authorable-surface comparison sees key NAMES only; + * - `retiredKey()` tombstones fire on live → retired, not on a value change; + * - `spec-changes.json` / the upgrade guide are projections of the ADR-0087 + * conversion + migration registries, and a default change does not + * necessarily carry a conversion. + * + * Measured on #4661's branch: flipping `retryPolicyShape()`'s `maxRetries` + * default from `0` to `3` — one character — left `check:authorable-surface` + * fully green. The only thing that caught it was a hand-written runtime pin + * that an author had remembered to write. + * + * Why this is the dangerous half of the surface. A default decides what happens + * when the author writes NOTHING, and omitting optional keys is the normal mode + * of AI-authored metadata (ADR-0033), so defaults cover far more deployed + * behaviour than they did in the hand-written era. Flip `maxRetries`, + * `enabled`, `required` or an `allow*` and every already-deployed document that + * omitted the key changes behaviour — with no error, nothing to grep, and + * nothing in the changelog. + * + * ## Direction B: defaults ONLY, constraints deliberately excluded + * + * Maintainer ruling on #4666 (2026-08-07). Constraints (`.min()` / `.max()` / + * `.positive()`) are NOT fingerprinted, and the exclusion is structural rather + * than a matter of discipline: {@link defaultFingerprintOf} reads exactly one + * field of the emitted JSON Schema, `default`, so a tightened bound or a + * reworded `.describe()` cannot move a fingerprint even by accident. + * + * The asymmetry that decided it: tightening a constraint REJECTS an existing + * document — loud, diagnosable, and discovered from CI. Flipping a default + * rejects nothing; the customer just finds out from an incident. Direction A + * (defaults + constraints) covers more but makes this ratchet markedly noisier, + * and #4535 §1 is already complaining that the authorable surface + * over-collects. + * + * ## Why the fingerprint reads the EMITTED schema, not the Zod node + * + * The value recorded here is the `default` of the generated JSON Schema — the + * same document the key set is collected from, and the thing the published + * contract actually advertises to authors and IDEs. Reading `_zod.def` instead + * would tie this ratchet to which Zod wrapper produced the value + * (`.default()` vs `.prefault()` vs `.meta({ default })`), when what an author + * experiences is only the published result. One source, one truth, and no + * internals to track across a Zod major. + */ + +import { categoryOfDefKey, serializeShard } from './sharded-artifacts.js'; + +/** `packages/spec/authorable-defaults/.json` — the #4666 default ratchet. */ +export const AUTHORABLE_DEFAULTS_DIR_NAME = 'authorable-defaults'; + +/** + * How a MISSING default is spelled in an acknowledgement's `from` / `to`. + * + * Unambiguous against every real fingerprint, because a fingerprint is always + * valid JSON and this is not (the JSON string `"(none)"` serializes WITH its + * quotes). Gaining or losing a default is a behaviour change exactly like + * moving one, so both endpoints need a spelling. + */ +export const NO_DEFAULT = '(none)'; + +/** Separator between the key and its fingerprint in a recorded entry. */ +const ENTRY_SEP = ' = '; + +/** + * The description carried by EVERY authorable-defaults shard — repeated per + * file for the same reason the authorable-surface description is: the reader it + * exists for is the one who just opened a shard to change a number in it. + * + * Compared byte-for-byte by the ratchet, so editing it restates the procedure + * in every shard, deliberately. + */ +export const AUTHORABLE_DEFAULTS_DESCRIPTION = + 'Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — ' + + 'what a metadata author gets when they omit the key, which for AI-authored metadata is most ' + + 'of the time. Sharded by category like authorable-surface/; the gate reads the whole ' + + 'authorable-defaults/ directory as ONE set. Each line is ": = ". ' + + 'Additions (a NEW key that ships with a default) are auto-recorded — commit the change. ' + + 'CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: ' + + 'it silently alters the behaviour of already-deployed metadata, so it fails ' + + 'check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR ' + + '(scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a ' + + 'tightened bound REJECTS a document loudly, which is a different and self-announcing class ' + + '(maintainer ruling on #4666, direction B). See #4666, #4661.'; + +/** One category's slice of the default-value ratchet. */ +export interface AuthorableDefaultsShard { + description: string; + category: string; + defaults: string[]; +} + +/** + * Canonical fingerprint of one default value. + * + * Normalisation, and why each rule is the way it is: + * + * - **object keys are sorted, recursively.** A JS object literal's key order + * is not part of its meaning, so re-spelling `{ mode: 'a', retries: 1 }` + * with the keys the other way round must not churn the ratchet. Sorting is + * what makes "the fingerprint moved" mean "the VALUE moved". + * - **array order is preserved.** An array default's order IS observable to + * the author (`schemas: ['urn:…:Group']`), so re-ordering it is a real + * change and must be caught. + * - **no whitespace.** `JSON.stringify` without an indent, so a + * re-formatting pass cannot move a fingerprint. + * - **nothing but the value.** No type, no constraints, no description. The + * direction-B boundary lives here and nowhere else. + * + * A value JSON cannot represent (a function, a `Date`, a `bigint`) has no + * published default either — `z.toJSONSchema` does not emit one — so this is + * only ever called on JSON the emitter already produced. + */ +export function defaultFingerprintOf(value: unknown): string { + return JSON.stringify(sortKeysDeep(value)) ?? 'null'; +} + +function sortKeysDeep(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortKeysDeep); + if (value && typeof value === 'object') { + const out: Record = {}; + for (const key of Object.keys(value as Record).sort()) { + out[key] = sortKeysDeep((value as Record)[key]); + } + return out; + } + return value; +} + +/** `"data/Foo:bar"` + `"3"` → `"data/Foo:bar = 3"`. */ +export function formatDefaultEntry(key: string, fingerprint: string): string { + return `${key}${ENTRY_SEP}${fingerprint}`; +} + +/** + * Inverse of {@link formatDefaultEntry}, splitting on the FIRST separator: a + * def key can never contain ` = `, while a fingerprint routinely can + * (`{"a":1} = ` is a perfectly ordinary string default). Splitting anywhere + * else would corrupt exactly the values that are hardest to notice. + */ +export function parseDefaultEntry(entry: string): { key: string; fingerprint: string } { + const at = entry.indexOf(ENTRY_SEP); + if (at <= 0) { + throw new Error( + `cannot parse authorable-defaults entry ${JSON.stringify(entry)}: every line is ` + + `":${ENTRY_SEP}" (#4666)`, + ); + } + return { key: entry.slice(0, at), fingerprint: entry.slice(at + ENTRY_SEP.length) }; +} + +/** Parse a whole recorded shard set into `key -> fingerprint`. */ +export function parseDefaultEntries(entries: readonly string[]): Map { + const out = new Map(); + for (const entry of entries) { + const { key, fingerprint } = parseDefaultEntry(entry); + out.set(key, fingerprint); + } + return out; +} + +/** + * Every authorable key of this build that HAS a published default, fingerprinted. + * + * Keys with no default are deliberately absent rather than recorded as + * `(none)`: 1327 of 7943 authorable keys carry a default, so recording only + * those keeps the artifact at a sixth of the size — and presence is not lost, + * because gaining a default makes a line APPEAR and losing one makes it + * VANISH, both of which {@link diffAuthorableDefaults} reads as changes. + * + * `retiredKey()` needs no special case: it is `z.never()`, which carries no + * default and so never produces a line. + */ +export function collectAuthorableDefaults( + schemasByDefKey: Iterable<[string, unknown]>, +): Map { + const out = new Map(); + for (const [defKey, schema] of schemasByDefKey) { + const props = (schema as { properties?: Record }).properties; + if (!props || typeof props !== 'object') continue; + for (const [name, prop] of Object.entries(props)) { + if (!prop || typeof prop !== 'object') continue; + if (!('default' in (prop as Record))) continue; + out.set(`${defKey}:${name}`, defaultFingerprintOf((prop as Record).default)); + } + } + return out; +} + +/** Split fingerprints into ` -> canonical shard bytes`. */ +export function authorableDefaultsShardTexts(defaults: ReadonlyMap): Map { + const grouped = new Map(); + for (const [key, fingerprint] of defaults) { + const category = categoryOfDefKey(key); + const bucket = grouped.get(category); + const line = formatDefaultEntry(key, fingerprint); + if (bucket) bucket.push(line); + else grouped.set(category, [line]); + } + const out = new Map(); + for (const category of [...grouped.keys()].sort()) { + out.set( + category, + serializeShard({ + description: AUTHORABLE_DEFAULTS_DESCRIPTION, + category, + defaults: grouped.get(category)!.sort(), + } satisfies AuthorableDefaultsShard), + ); + } + return out; +} + +// ─── Comparison ─────────────────────────────────────────────────────── + +/** How a key's default moved between the baseline and this build. */ +export type DefaultChangeKind = 'changed' | 'added' | 'removed'; + +export interface ObservedDefaultChange { + key: string; + kind: DefaultChangeKind; + /** Baseline fingerprint, or {@link NO_DEFAULT}. */ + from: string; + /** This build's fingerprint, or {@link NO_DEFAULT}. */ + to: string; +} + +export interface DefaultDiffInput { + /** `key -> fingerprint` recorded at the baseline. */ + baseline: ReadonlyMap; + /** `key -> fingerprint` this build emits. */ + current: ReadonlyMap; + /** Authorable keys at the baseline — `key -> isRetired`. */ + baselineKeys: ReadonlyMap; + /** Authorable keys this build emits — `key -> isRetired`. */ + currentKeys: ReadonlyMap; +} + +/** + * The default changes that are this gate's business. + * + * Three exclusions, each because ANOTHER gate already owns the fact and would + * otherwise report it twice under a worse name: + * + * 1. a key that is NEW to the authorable surface. Its default cannot have + * changed the behaviour of deployed metadata, because no deployed document + * could omit a key that did not exist. Adding the key is what the + * authorable-surface ratchet records, and it records it already. + * 2. a key that has LEFT the surface (absent from this build). Its default + * going with it is the removal, not a separate event — checks (a)/(c) of + * the surface gate adjudicate that, with the tombstone prescription. + * 3. a key that is now RETIRED. `retiredKey()` is `z.never()` and carries no + * default by construction, so reading the loss as a "removed default" + * would demand a second declaration for one retirement — check (b) + * already demands the real one. + */ +export function diffAuthorableDefaults(input: DefaultDiffInput): ObservedDefaultChange[] { + const { baseline, current, baselineKeys, currentKeys } = input; + const changes: ObservedDefaultChange[] = []; + const keys = new Set([...baseline.keys(), ...current.keys()]); + for (const key of [...keys].sort()) { + // (2)/(3): gone, or tombstoned — someone else's verdict. + if (!currentKeys.has(key) || currentKeys.get(key) === true) continue; + const from = baseline.get(key); + const to = current.get(key); + if (from === to) continue; + if (from === undefined) { + // (1): a key the baseline never had is not a default CHANGE. + if (!baselineKeys.has(key)) continue; + changes.push({ key, kind: 'added', from: NO_DEFAULT, to: to! }); + } else if (to === undefined) { + changes.push({ key, kind: 'removed', from, to: NO_DEFAULT }); + } else { + changes.push({ key, kind: 'changed', from, to }); + } + } + return changes; +} + +// ─── Authorisation ──────────────────────────────────────────────────── + +/** One declared hop of a key's default, as written in `DEFAULT_CHANGES_BY_MAJOR`. */ +export interface DeclaredDefaultChange { + /** Exact `${defKey}:${name}` — never a leaf name (#4659's lesson). */ + key: string; + /** Fingerprint before the change, or {@link NO_DEFAULT}. */ + from: string; + /** Fingerprint after it, or {@link NO_DEFAULT}. */ + to: string; + /** Why the flip is correct, and what a consumer relying on the old value must do. */ + reason: string; +} + +export interface StaleDeclaration { + key: string; + /** What the chain's last hop claims the default now is. */ + claims: string; + /** What this build actually emits. */ + emits: string; + why: 'chain-tip-mismatch' | 'chain-not-contiguous'; +} + +export interface DefaultAuthorisation { + /** Changes covered by a declared chain — printed, never silent. */ + authorised: Array<{ change: ObservedDefaultChange; declared: DeclaredDefaultChange[] }>; + /** Changes with no declaration that covers them. Fatal. */ + unauthorised: ObservedDefaultChange[]; + /** Declarations at the current major that no longer describe reality. Fatal. */ + stale: StaleDeclaration[]; +} + +/** + * Adjudicate observed changes against the declared ones. + * + * ## What makes a declaration honest rather than an allowlist + * + * The failure mode named in the dispatch — and in #4690 — is an exemption list + * nobody re-checks, which quietly blesses whatever comes later. Four properties + * stop that here, and all four are re-derived on EVERY run from sources the + * declaration cannot edit: + * + * 1. **Both endpoints are pinned.** A declaration covers the transition + * `from → to` and nothing else. `from` is compared against the BASELINE, + * which is read out of git at the merge base (or, before this ratchet + * exists upstream, the committed artifact); `to` is compared against what + * THIS BUILD emits. Neither is a field the declaration's author can set. + * 2. **The chain tip must still be true.** For every key declared at the + * current major, the last hop's `to` must equal the live fingerprint. Move + * the default again — or revert it — and the declaration goes RED instead + * of silently covering the new value. This is the property that makes a + * declaration die when it stops being true, and it is the exact mirror of + * the surface gate's check (b2). + * 3. **The chain must be contiguous.** Two hops of one key must meet + * (`hop[i].to === hop[i+1].from`), so a chain cannot skip a value that was + * really shipped in between. + * 4. **Only the CURRENT major authorises.** Past majors' entries are the + * historical record and cover nothing, so an authorisation cannot outlive + * the major it was written for. By the time a major rolls, the baseline + * already carries the new value, so nothing is left needing cover. + * + * A declaration naming a key this build does not emit at all is NOT an error: + * that is the expected end state once the key is retired, and the surface gate + * owns that verdict (same disposition as check (b2)). + */ +export function authoriseDefaultChanges( + changes: readonly ObservedDefaultChange[], + declaredByMajor: Readonly>, + currentMajor: number, + current: ReadonlyMap, + currentKeys: ReadonlyMap, +): DefaultAuthorisation { + const chains = new Map(); + for (const declared of declaredByMajor[currentMajor] ?? []) { + const chain = chains.get(declared.key); + if (chain) chain.push(declared); + else chains.set(declared.key, [declared]); + } + + const stale: StaleDeclaration[] = []; + for (const [key, chain] of chains) { + // A declaration about a key this build no longer emits is history, not a claim. + if (!currentKeys.has(key)) continue; + for (let i = 1; i < chain.length; i++) { + if (chain[i - 1].to !== chain[i].from) { + stale.push({ + key, + claims: `${chain[i - 1].to} → ${chain[i].from}`, + emits: current.get(key) ?? NO_DEFAULT, + why: 'chain-not-contiguous', + }); + } + } + const tip = chain[chain.length - 1].to; + const live = current.get(key) ?? NO_DEFAULT; + if (tip !== live) { + stale.push({ key, claims: tip, emits: live, why: 'chain-tip-mismatch' }); + } + } + + const authorised: DefaultAuthorisation['authorised'] = []; + const unauthorised: ObservedDefaultChange[] = []; + for (const change of changes) { + const chain = chains.get(change.key); + // The chain must START where the baseline sits, and END where this build + // does. A branch that forked mid-chain matches at a later hop. + const at = chain?.findIndex((hop) => hop.from === change.from) ?? -1; + const covers = + chain !== undefined && at >= 0 && chain[chain.length - 1].to === change.to; + if (covers) authorised.push({ change, declared: chain!.slice(at) }); + else unauthorised.push(change); + } + return { authorised, unauthorised, stale }; +} diff --git a/packages/spec/scripts/lib/default-changes.ts b/packages/spec/scripts/lib/default-changes.ts new file mode 100644 index 0000000000..8417152ff3 --- /dev/null +++ b/packages/spec/scripts/lib/default-changes.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Declared changes to the DEFAULT VALUE of an already-authorable key (#4666). + * + * ## What this table is for + * + * `authorable-defaults/` records what an author gets when they omit each key. + * Moving one of those values silently changes the behaviour of metadata that is + * ALREADY DEPLOYED — the author wrote nothing, so there is nothing to reject + * and nothing to warn about. `check:authorable-surface` therefore refuses any + * such move until it is declared here, by its exact `${defKey}:${name}` and its + * exact before/after fingerprints. + * + * This is the "acknowledged change" exit, and it is deliberately NOT a bare + * allowlist. Every field is re-derived on every run from something the entry's + * author does not control (see `authoriseDefaultChanges` in + * `authorable-defaults.ts` for the four properties in full): + * + * - `from` must equal the baseline fingerprint — read out of git at the merge + * base with origin/main, which the commit under test cannot rewrite; + * - `to` must equal what the build EMITS. The moment the default moves again, + * or is reverted, this entry stops matching and the gate goes red naming + * it. An entry cannot outlive the fact it describes; + * - only entries at the CURRENT protocol major authorise anything. Older + * majors' rows are the historical record; + * - `reason` is printed in full by every build that consumes the entry, so an + * acknowledged default flip announces itself in the log rather than passing + * in silence (#4690: a gate that quietly exits 0 is worse than no gate). + * + * ## What belongs here, and what does not + * + * - **Belongs**: the default of a key that already existed moved, gained a + * value where it had none, or lost one. + * - **Does not**: a NEW key that ships with a default (no deployed document + * could have omitted a key that did not exist — the authorable-surface + * ratchet records the addition); a tightened or loosened CONSTRAINT + * (`.min()` / `.max()`), which is deliberately outside this ratchet because + * it REJECTS the offending document loudly — maintainer ruling on #4666, + * direction B; a retirement (`retiredKey()`), which goes through + * RETIRED_KEYS_BY_MAJOR. + * + * ## Writing an entry + * + * The gate prints a copy-pasteable block naming the exact key and both + * fingerprints. Fill in `reason` with what changes for a consumer who was + * relying on the OLD value and what they should write to keep it — that + * sentence is the entire human record of a change no error message will ever + * carry, and it is what a build prints when it accepts the flip. + * + * A default change that also warrants a migration TODO should get one: add a + * `semantic` entry to the major's step in `src/migrations/registry.ts` so it + * reaches `spec-changes.json`, the generated upgrade guide and + * `os migrate meta`. This table records that the change was DECLARED; the + * migration chain is the prescription a consumer follows. + */ + +import type { DeclaredDefaultChange } from './authorable-defaults.js'; + +/** + * Declared default changes, keyed by the protocol major that shipped them. + * + * Empty at major 17: this ratchet lands with no default change to declare, and + * that emptiness is the gate's own proof — `check:authorable-surface` is green + * on origin/main with the table holding nothing, which means every default in + * the tree matches its recorded fingerprint. The first entry will be written by + * whoever first needs to move one. + */ +export const DEFAULT_CHANGES_BY_MAJOR: Readonly> = {}; diff --git a/packages/spec/scripts/lib/sharded-artifacts.ts b/packages/spec/scripts/lib/sharded-artifacts.ts index fbfa9037a6..9f980415ba 100644 --- a/packages/spec/scripts/lib/sharded-artifacts.ts +++ b/packages/spec/scripts/lib/sharded-artifacts.ts @@ -66,6 +66,18 @@ export const LEGACY_MONOLITH_NAMES = Object.freeze({ [API_SURFACE_DIR_NAME]: 'api-surface.json', } as Record); +/** + * The array property a category-sharded ratchet keeps its entries in. + * + * `defaults` is the #4666 default-value ratchet + * (`authorable-defaults/.json`). Its entries are + * `": = "` rather than a bare key, which the two + * readers below do not care about: both route on `categoryOfDefKey`, and that + * reads up to the first slash — the same way it already tolerates the + * `[RETIRED]` suffix riding on an authorable-surface entry. + */ +export type ShardArrayField = 'keys' | 'schemas' | 'defaults'; + /** Shard basename for the root entry point (`"."`), which has no path segment. */ export const ROOT_ENTRY_SHARD = 'root'; @@ -339,7 +351,7 @@ function shardTextsByCategory( */ export function aggregateCategoryShards( dir: string, - field: 'keys' | 'schemas', + field: ShardArrayField, ): { entries: string[]; shards: ShardOnDisk>[] } | null { const shards = readShards>(dir); if (shards.length === 0 && !fs.existsSync(dir)) return null; @@ -434,7 +446,7 @@ export function readShardedKeysAtRev( git: GitRun, rev: string, dirName: string, - field: 'keys' | 'schemas', + field: ShardArrayField, ): { entries: string[]; layout: BaselineLayout } | { error: string } | null { // `ls-tree` run inside the package prints paths relative to the CWD, which is // the same spelling `git show :./` wants — so the two compose @@ -462,6 +474,11 @@ export function readShardedKeysAtRev( } const legacyName = LEGACY_MONOLITH_NAMES[dirName]; + // A ratchet born AFTER the #5837 split (authorable-defaults/, #4666) never had + // a single-file layout, so there is nothing to fall back to and "absent" is the + // honest answer. Said explicitly rather than left to `git show :./undefined` + // failing its way to the same result — an accident that reads as a decision. + if (legacyName === undefined) return null; const show = git('show', `${rev}:./${legacyName}`); if (show.status !== 0) { if (/does not exist in|exists on disk, but not in/.test(show.stderr)) return null; diff --git a/scripts/regen-artifacts.mjs b/scripts/regen-artifacts.mjs index 6f61408cd9..227ec2eae0 100644 --- a/scripts/regen-artifacts.mjs +++ b/scripts/regen-artifacts.mjs @@ -57,6 +57,13 @@ export const REGEN_ARTIFACTS = Object.freeze([ // one commit for the whole surface, which a per-shard copy would let drift. { path: 'packages/spec/authorable-surface.base.json', gen: 'gen:schema', check: 'check:authorable-surface' }, { path: 'packages/spec/json-schema.manifest/**', gen: 'gen:schema', check: 'check:authorable-surface' }, + // The #4666 default-value ratchet — what an author gets when they OMIT a key. + // Same producer, same gate and the same sorted-array-per-category shape as its + // authorable-surface sibling, so it merges the same way. A CHANGED default is + // never resolved by regenerating: `check:authorable-surface` adjudicates it + // against the merge base before this file is written at all, so the regen here + // can only ever pick up a NEW key's default. + { path: 'packages/spec/authorable-defaults/**', gen: 'gen:schema', check: 'check:authorable-surface' }, // `gen:api-surface` reads the BUILT `dist/*.d.ts`, never the source. On a // stale dist it does not fail — it emits a *plausible* surface missing every // export added since the last build, and `gen:docs` will ratchet a baseline