Contribution 계약
모든 SDK contribution 인터페이스와 필수 메서드, 그리고 contributor 클래스가 발견되기 위해 만족해야 하는 규칙입니다.
Contribution 계약
Manifest 탐색 대상 클래스에서 인터페이스를 구현해 App 기능을 게시합니다. 아래 예시는 Resource 식별자 등록이고, 다른 기능의 인터페이스는 목록에서 찾을 수 있습니다. 클래스의 명시적인 implements와 App Manifest 계약의 탐색 위치가 모두 있어야 발견됩니다.
최소 식별자 Contribution
먼저 Resource 만들기로
Amuzcorp\Nexia\Workshop\Models\Note를 생성하세요. 아래 완전한 클래스는
Resource 식별자 인터페이스만 보여 주며 기반 클래스나 trait가 필요하지 않습니다.
<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\Workshop\Contribution\Resources;
use Amuzcorp\Nexia\Workshop\Models\Note;
use Nexia\Contribution\Contracts\ResourceCatalogContribution;
final class NoteCatalog implements ResourceCatalogContribution
{
public static function resourceKey(): string
{
return 'workshop.note';
}
public static function resourceModelClass(): string
{
return Note::class;
}
}생성된 NoteModule에는 이 식별자와 함께 permission, 권한, descriptor,
내비게이션이 있습니다. 실제 App에서는 생성된 module을 유지하세요. 위
NoteCatalog를 함께 등록하거나 이 식별자 예시로 전체 module을 교체하지 않습니다.
Resource key에는 소유자가 하나여야 합니다.
클래스의 인터페이스 선언과 Manifest 탐색 위치가 모두 있어야 발견됩니다. 메서드를 제공하는 trait만으로 인터페이스가 선언되지는 않습니다.
Contribution 인터페이스
| 인터페이스 | 네임스페이스 | 필수 메서드 |
|---|---|---|
ResourceCatalogContribution | Nexia\Contribution\Contracts | resourceKey(), resourceModelClass() |
ResourceAuthorizationContribution | Nexia\Contribution\Contracts | resourceAuthorization() |
ShellResourceContribution | Nexia\AppRuntime\Contracts | shellResource() |
PermissionContribution | Nexia\Permission\Contracts | catalogPermissionDefinitions() |
RolePresetContribution | Nexia\Permission\Contracts | rolePresets() |
LegalEntityMemberSelfAccessContribution | Nexia\Permission\Contracts | legalEntityMemberSelfAccessKeys() |
NavigationContribution | Nexia\Navigation\Contracts | navigationItems() |
PaletteCommandContribution | Nexia\Palette\Contracts | paletteCommandItems() |
InheritedSearchVisibilityContribution | Nexia\Contribution\Contracts | inheritedSearchVisibilityRelation()과 Resource Catalog 정체성 |
DashboardWidgetContribution | Nexia\Dashboard\Contracts | dashboardWidgets() |
DashboardQuerySourceContribution | Nexia\Dashboard\Contracts | dashboardQuerySources() |
AgentToolContribution | Nexia\Agent\Contracts | agentTools() |
AppDescriptorContribution | Nexia\AppDescriptors\Contracts | appDescriptors(): AppDescriptorSet |
ResourceSummaryContribution | Nexia\AppDescriptors\Contracts | resourceKey(), summarize() |
BatchResourceSummaryContribution | Nexia\AppDescriptors\Contracts | resourceKey(), summarize(), summarizeMany() |
ResourceReferenceResolutionContribution | Nexia\ResourceReference\Contracts | resourceKey(), resolve() |
ResourceReferenceBatchResolutionContribution | Nexia\ResourceReference\Contracts | resolution 메서드와 resolveMany() |
ResourceReferenceSearchContribution | Nexia\ResourceReference\Contracts | search(), 그리고 해석 메서드들 |
ResourceReferenceEffectiveRangeSearchContribution | Nexia\ResourceReference\Contracts | search·resolution 메서드와 searchEffectiveRange() |
ResourceReferencePagedSearchContribution | Nexia\ResourceReference\Contracts | resourceKey(), resolve(), search(), authorized(), searchPage() |
CompositionQueryContribution | Nexia\ResourceComposition\Contracts | compositionQueryProvider(), resourceKey(), resourceModelClass() |
ResourceTransferExportSourceContribution | Nexia\ResourceTransfer\Contracts | resourceTransferExportSources() |
ImportRecipeContribution | Nexia\ResourceImport\Contracts | resourceImportRecipes() |
ResourceImportPipelineContribution | Nexia\ResourceImport\Contracts | resourceImportPipelines() |
SignatureDocumentDataSourceContribution | Nexia\Signature\Contracts | appDescriptors(), signatureDocumentDataSourceProviders() |
SignatureBulkBindingContribution | Nexia\Signature\Contracts | appDescriptors(), signatureBulkBindingProviders() |
CopyableTemplateContribution | Nexia\Templates\Contracts | templates(), apply() |
FixtureContribution | Nexia\Fixture\Contracts | appKey(), fixtureKeys(), seed() |
SetupTaskContribution | Nexia\Setup\Contracts | tasks() |
FeatureGuideContribution | Nexia\Guidance\Contracts | ownerKey(), guides() |
SelfWorkContextContribution | Nexia\SelfService\Contracts | workContexts() |
SelfServiceActionItemContribution | Nexia\SelfService\Contracts | selfServiceActions() |
Contribution은 labelKey, titleKey, descriptionKey 같은 정확한 카탈로그 키만 담고, 호스트가 payload 경계에서 요청 로케일에 맞춰 해석합니다. 백엔드 contribution은 로케일 맵을 불러오는 대신 키를 제공합니다.
대부분의 메서드가 static입니다. 모든 typed descriptor는 하나의 static publication 경계인 AppDescriptorContribution::appDescriptors()에서 immutable set으로 게시하며, 그 set에는 Resource, Slot Widget, Approval, Signature, Process, Decision, Reporting descriptor를 함께 담을 수 있습니다. Descriptor key는 비어 있지 않아야 하고 앞뒤 공백이 없어야 합니다. Core는 contributor FQCN 순서와 각 set의 선언 순서를 차례로 보존하고, 호스트 전용 테스트 등록은 호출 순서대로 그 뒤에 둡니다. ResourceSummaryContribution, ResourceReference 인터페이스, CopyableTemplateContribution, FixtureContribution은 요청마다 또는 실행마다 해석·적용·시딩하므로 인스턴스 메서드를 씁니다. SetupTaskContribution도 인스턴스 기반입니다. 호스트가 컨테이너에서 contributor를 해석하고 Manifest 발견 기록에서 소유자를 추론한 뒤 반환된 선언을 등록합니다. 두 Signature contribution은 appDescriptors()로 정적 descriptor를, 두 번째 메서드로 instance runtime provider를 게시합니다. Core는 descriptor/provider의 정확한 identity를 연결하고 provider가 없으면 fail-closed합니다.
ResourceSummaryContribution은 Spotlight 같은 compact 호스트 화면에서 쓰는 레코드
표현 경계이기도 합니다. 소유 App은 권한 검사를 마친 display, 선택적 route, 선택적
Shell 아이콘 이름, typed field를 반환합니다. Field에 ResourceSummaryFieldRole::Subtitle,
Meta, Status를
지정하면 compact consumer가 의미상 배치 위치를 알 수 있지만, React 컴포넌트와 간격,
말줄임, 접근성은 계속 호스트가 소유합니다. 여러 resource ID를 한 번의 제한된 쿼리로
요약할 수 있으면 BatchResourceSummaryContribution을 구현합니다. 기존 provider에는
Core가 단건 호출 fallback을 유지합니다. Spotlight는 검색 후보를 DB에서 다시 조회하고
권한을 확인한 뒤에만 ResourceVisibility::Search를 전달하며, 소유자는 여전히 레코드
가시성을 검사하고 보이지 않는 레코드에는 null을 반환해야 합니다.
App 소유 Agent 도구
일반 Resource 데이터, Dashboard query source, 기존 화면 action으로 표현할 수
없는 기능에만 AgentToolContribution을 사용합니다. App은 권한이 적용된 평범한
Laravel route를 소유하고 불변 metadata만 게시합니다.
use Nexia\Agent\Contracts\AgentToolContribution;
use Nexia\Agent\AgentToolDeclaration;
use Nexia\Agent\AgentToolLane;
use Nexia\Agent\AgentToolRouting;
use Nexia\Agent\AgentToolSurface;
use Nexia\Agent\AgentToolTier;
final class TimeAbsenceAgentTools implements AgentToolContribution
{
public static function agentTools(): array
{
return [new AgentToolDeclaration(
key: 'time-absence.personal_time_leave.read',
tier: AgentToolTier::Auto,
routing: AgentToolRouting::read(
AgentToolLane::DirectData,
AgentToolSurface::Data,
),
description: 'Read the current user personal leave balance.',
method: 'GET',
path: '/api/legal-entities/{legalEntity:public_id}/time-absence/personal-leave',
permissions: ['time-absence.leave_balance.read'],
)];
}
}Key의 첫 segment는 안정적인 App namespace이며 kebab-case를 쓸 수 있습니다. 뒤의
dotted segment는 모두 소문자 영숫자와 underscore만 사용합니다. permissions는
비어 있을 수 없고 실제 route guard를 정확히 나타내야 합니다. inputSchema를
선언하면 비어 있지 않은 JSON Schema object여야 합니다. Laravel scoped placeholder인
{legalEntity:public_id}는 route 문법 그대로 선언합니다. Gateway가 호출 전에 현재
Legal Entity 또는 placeholder 이름과 정확히 일치하는 인자로 치환합니다. 일반
id 인자는 {user}, {ticket} 등 다른 이름의 placeholder를 대신하지 않습니다.
Precondition은 자유 형식 tag가 아니라 Router가 해석하는 정확한 capability입니다.
legal_entity_selected는 신뢰된 delegation context에 활성 Legal Entity가 있을 때만
충족되며, 없으면 Router가 도구를 노출하지 않습니다. 지원하지 않는 precondition도
도구를 계속 비노출 상태로 둡니다.
발견 과정은 contributor를 Core container에서 해석하지 않고 static 메서드를
호출합니다. 선언할 수 있는 것은 Laravel route binding, routing의 lane/effect/surface,
기본 확인 tier, permission metadata, invalidation hint뿐입니다. Gateway handler,
Closure, Core Action, recipe, host widget, browser tool은 게시할 수 없습니다. External
effect에는 AgentToolTier::Confirm과 external-share facet도 필요합니다.
이 확장은 고유한 업무 기능에 사용합니다. 일반 Resource CRUD와 선언된 업무 동작은 Resource descriptor와 고정된 범용 데이터 도구를 사용하며 AgentToolContribution이 필요하지 않습니다.
인터페이스 상속
네 인터페이스가 ResourceCatalogContribution을 확장하므로, 그것들을 구현하면 resourceKey()와 resourceModelClass()도 필요합니다.
| 인터페이스 | 추가로 필요한 것 |
|---|---|
ResourceAuthorizationContribution | resourceKey(), resourceModelClass() |
ShellResourceContribution | resourceKey(), resourceModelClass() |
DashboardWidgetContribution | resourceKey(), resourceModelClass() |
InheritedSearchVisibilityContribution | resourceKey(), resourceModelClass() |
따라서 대시보드 위젯 contributor는 설계상 Resource에 묶입니다.
메서드를 공급하는 트레이트
| 트레이트 | 네임스페이스 | 구현하는 것 | 읽어오는 곳 |
|---|---|---|---|
ContributesResourcePermissions | Nexia\Permission\Concerns | catalogPermissionDefinitions() | permissionResources() |
ContributesNavigationDestination | Nexia\Navigation\Concerns | navigationItems() | $navigation |
ContributesNavigationDestinationActions | Nexia\Navigation\Concerns | 에이전트 동작 면 | agentNavigationActionDefinitions() |
HasShellResource | Nexia\AppRuntime\Concerns | shellResource() | Catalog 정체성 |
인터페이스는 여전히 여러분이 선언합니다. implements PermissionContribution 없이 ContributesResourcePermissions를 쓰면 메서드는 동작하지만 발견이 절대 선택하지 않는 클래스가 되고, doctor가 permission contributors: 0 classes를 보고합니다.
Contributor가 만족해야 하는 요건
| 요건 | 이유 |
|---|---|
선언된 contributionLocations() root 안 | manifest 생성이 그 directory만 색인 |
abstract가 아님 | 추상 클래스는 건너뜀 |
| 파일 이름이 클래스 이름과 일치 | PSR-4 오토로딩 |
| 키가 App 접두를 달고 안정적임 | 키가 권한 행과 번역, 저장된 테넌트 상태에 나타남 |
| 라벨이 App 로케일 카탈로그에서 옴 | 리터럴 문자열은 번역되지 않은 채로 배포됨 |
| 발견 중에 테넌트 쓰기 없음 | 레지스트리는 행위자나 테넌트 컨텍스트가 존재하기 전에 조립됨 |
| 반복 호출해도 안전 | 소비 registry가 선언을 두 번 이상 읽을 수 있음 |
마지막 둘이 가장 중요합니다. SetupTaskContribution::tasks()를 포함한 선언 메서드는 레지스트리가 조립되는 동안 실행됩니다. 이때는 초기화된 테넌트도 없고 알려진 행위자도 없습니다. 데이터베이스를 읽거나 현재 사용자를 해석하거나 무엇이든 쓰면 실패하거나 레지스트리를 망가뜨립니다. Setup evaluator는 별도 단계입니다. 호스트가 테넌트 plan을 만들 때만 evaluate()를 호출하고 불투명한 테넌트·행위자·로케일·시간 컨텍스트를 제공합니다. Contributor는 여전히 발견 단계에서 테넌트 작업을 하지 않습니다.
결과 또는 반환: Descriptor 값 객체
AppDescriptorContribution은 타입 있는 객체의 immutable AppDescriptorSet을 돌려주며, 객체는 전부 Nexia\AppDescriptors에 있습니다. 호스트의 AppDescriptorCatalog는 이 단일 계약을 발견하고 contributor provenance를 보존한 뒤 소비자가 필요한 descriptor class만 요청하게 합니다.
| 클래스 | 필수 생성자 인자 |
|---|---|
ResourceDescriptor | key, version; 리소스 자체를 가리키는 선택적 labelKey |
EventDescriptor | 전체 Event key, 양의 정수 schemaVersion, aggregateType, payloadSchema |
SlotWidgetDescriptor | key, version, slot, component, slotApiVersion |
ApprovalFormBindingDescriptor | appKey, resourceKey, actionKey, labelKey, entryModes, formWidgetKey, documentSchemaKey |
ApprovalDocumentSchema | appKey, resourceKey, sections |
ApprovalBusinessTemplatePresetDescriptor | App 소유 preset key와 business-template 필드 |
ApprovalRoutePolicyPresetDescriptor | App 소유 preset key와 route-policy 필드 |
SignatureTemplateBindingDescriptor | App 소유 signature-template binding 필드 |
ProcessStartBindingDescriptor | appKey, bindingKey, definitionKey, processId, resourceKey, startPermissionKey, labelKey |
ProcessWorkActionDescriptor | kind, appKey, actionKey, labelKey, topic; 선택 payload/input, approvalTask, outputContract 메타데이터 |
ProcessApprovalTaskConfiguration | bindingKey, outcomeTopic; 선택 outcomes, payload 키, businessTemplatePresetKey, routePolicyPresetKey |
ProcessUserTaskFormDescriptor | key, appKey, labelKey |
ProcessTemplateDescriptor | key, version, appKey(nullable이지만 기본값 없음), labelKey, category, structure |
DecisionResultTemplateDescriptor | key, version, labelKey, fields; 선택 descriptionKey, decisionTable, hitPolicy |
ReportingViewDescriptor | key, version, viewName, columns |
버전이 있는 최상위 descriptor는 status: DescriptorStatus를 가지며 보통
Active가 기본값이고 다른 상태는 Deprecated와 Removed입니다. 대부분 문자열
version을 쓰지만 EventDescriptor는 EventEnvelope에 실리는 양의 정수
schemaVersion을 요구합니다.
이 descriptor type들은 계속 서로 구분되는 typed 값입니다. generic set은 각각의 discovery interface와 registry 진입점만 대체하며 Approval preset, Signature binding, Public Event 계약, 어떤 Process/DMN 기능도 제거하지 않습니다.
Descriptor 생성자는 자기 값을 즉시 검증합니다. 엄격한 호스트 게이트도 각 set의
내용을 검사해 선언한 descriptor 타입, App 소유 key prefix, 계열별 key 유일성을
검사합니다. 독립 공개 integration Event는 EventDescriptor로 선언하고, 공개
Resource 라이프사이클 Event는 ResourceDescriptor 아래에 둔 채 두 번째로 선언하지
않습니다. EventPayloadSchema는 지원하는 scalar·array·object type만 받고,
required: false가 아니면 필드를 필수로 보며, 선언하지 않은 payload 필드를
거부합니다. 조합 가능한 payload schema에는 PublicEventPayloadSchema의 라벨 규칙도
적용합니다. 경로별 번역 키를 파생할 수 있다면
PublicEventPayloadSchema::withLabelKeys()를 쓰세요.
선택 App의 잘못된 contribution은 레지스트리 조립 중 격리되어 관련 없는 App의 부팅을 막지 않습니다. 하지만 이것은 승인 게이트가 아닙니다. nexia-apps:validate-app-descriptors, App 활성화, 테넌트 설치는 모두 엄격 검증을 실행하고, 동기화나 테넌트 상태 쓰기 전에 잘못된 App을 거부합니다.
오류
| 증상 | 원인 | 해결 |
|---|---|---|
WARN contribution locations: no package contribution location is registered | 디렉터리 미선언 또는 네임스페이스 불일치 | contributionLocations() 수정 |
WARN permission contributors: 0 classes | 인터페이스 선언 없이 트레이트만 사용 | implements PermissionContribution 추가 |
WARN permissions discovered: 0 permission definitions | 발견된 어떤 contributor도 권한을 선언하지 않음 | 인터페이스 확인, 그다음 클래스 위치 확인 |
descriptor의 InvalidArgumentException | Descriptor 생성자 검증 | 메시지에 표시된 잘못된 값을 수정 |
검증·활성화·설치 중 DescriptorValidationException | Contribution 자체가 잘못됐거나 다른 contribution과 모순됨 | 보고된 descriptor 경로를 모두 고친 뒤 nexia-apps:validate-app-descriptors 재실행 |
| 일부 프로세스에서만 contribution이 나타남 | 오래 도는 프로세스의 낡은 오토로더 | 클래스 추가 후 워커와 Vite 재시작 |
App에 적용
Manifest가 반환하는 디렉터리에 Contribution 클래스를 두고 인터페이스를 명시적으로 구현하세요. Trait만 사용하면 등록되지 않습니다.
관련 문서
- 확장 모델 — 발견과 네 관문, 호스트 전용 경계
- Resource 계약 — Resource Catalog 계약 심화
- 업무 프로세스 계약 — Process descriptor 세부
- 전자 결재 계약 — Approval 바인딩과 스키마 세부
- Setup 태스크 기여 — 선언, evaluator, 의존성, 운영 상태 규칙
- Contribution 발견 문제 해결 — 위 오류 각각을 진단으로