message
+) {
+ /**
+ * Operation type for prefix updates.
+ */
+ public enum Operation {
+ PUT, // Replace all prefixes
+ PATCH, // Add/update selected prefixes
+ DELETE // Remove selected prefixes
+ }
+
+ /**
+ * Creates an update prefixes command.
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @param author the commit author
+ * @param newPrefixes the prefix mappings
+ * @param operation the operation type
+ * @param message optional commit message
+ */
+ public UpdatePrefixesCommand {
+ newPrefixes = Map.copyOf(newPrefixes);
+ }
+}
+```
+
+**2.2 Create `UpdatePrefixesCommandHandler.java`**
+
+```java
+package org.chucc.vcserver.command;
+
+import java.util.Map;
+import org.apache.jena.rdfpatch.RDFPatch;
+import org.apache.jena.rdfpatch.RDFPatchOps;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.chucc.vcserver.domain.CommitMetadata;
+import org.chucc.vcserver.exception.BranchNotFoundException;
+import org.chucc.vcserver.repository.BranchRepository;
+import org.chucc.vcserver.repository.MaterializedBranchRepository;
+import org.springframework.stereotype.Component;
+
+/**
+ * Handles prefix update commands by generating RDFPatch with PA/PD directives.
+ *
+ * This handler delegates to CreateCommitCommandHandler - no new events needed.
+ */
+@Component
+public class UpdatePrefixesCommandHandler {
+
+ private final MaterializedBranchRepository materializedBranchRepository;
+ private final BranchRepository branchRepository;
+ private final CreateCommitCommandHandler createCommitCommandHandler;
+
+ /**
+ * Creates an update prefixes command handler.
+ *
+ * @param materializedBranchRepository the materialized branch repository
+ * @param branchRepository the branch repository
+ * @param createCommitCommandHandler the commit creation handler
+ */
+ public UpdatePrefixesCommandHandler(
+ MaterializedBranchRepository materializedBranchRepository,
+ BranchRepository branchRepository,
+ CreateCommitCommandHandler createCommitCommandHandler) {
+ this.materializedBranchRepository = materializedBranchRepository;
+ this.branchRepository = branchRepository;
+ this.createCommitCommandHandler = createCommitCommandHandler;
+ }
+
+ /**
+ * Handles prefix update command.
+ *
+ * @param cmd the update prefixes command
+ * @return commit metadata
+ * @throws BranchNotFoundException if branch doesn't exist
+ */
+ public CommitMetadata handle(UpdatePrefixesCommand cmd) {
+ // 1. Validate branch exists
+ branchRepository.findByDatasetAndName(cmd.dataset(), cmd.branch())
+ .orElseThrow(() -> new BranchNotFoundException(cmd.dataset(), cmd.branch()));
+
+ // 2. Get current prefixes from materialized branch
+ DatasetGraph currentDsg = materializedBranchRepository
+ .getMaterializedBranch(cmd.dataset(), cmd.branch());
+ Map oldPrefixes = currentDsg.getDefaultGraph()
+ .getPrefixMapping()
+ .getNsPrefixMap();
+
+ // 3. Generate RDFPatch with PA/PD directives
+ RDFPatch patch = buildPrefixPatch(oldPrefixes, cmd.newPrefixes(), cmd.operation());
+
+ // 4. Create commit via existing handler
+ String message = cmd.message().orElseGet(() -> generateDefaultMessage(cmd));
+ CreateCommitCommand commitCmd = new CreateCommitCommand(
+ cmd.dataset(),
+ cmd.branch(),
+ message,
+ cmd.author(),
+ RDFPatchOps.str(patch)
+ );
+
+ return createCommitCommandHandler.handle(commitCmd);
+ }
+
+ /**
+ * Builds RDFPatch with PA/PD directives.
+ *
+ * @param oldPrefixes current prefix mappings
+ * @param newPrefixes new prefix mappings
+ * @param operation the operation type
+ * @return RDFPatch with prefix directives
+ */
+ RDFPatch buildPrefixPatch(
+ Map oldPrefixes,
+ Map newPrefixes,
+ UpdatePrefixesCommand.Operation operation) {
+
+ StringBuilder patchStr = new StringBuilder("TX .\n");
+
+ switch (operation) {
+ case PUT -> {
+ // Remove all old prefixes
+ oldPrefixes.forEach((prefix, iri) ->
+ patchStr.append("PD ").append(prefix).append(": .\n"));
+ // Add all new prefixes
+ newPrefixes.forEach((prefix, iri) ->
+ patchStr.append("PA ").append(prefix).append(": <").append(iri).append("> .\n"));
+ }
+ case PATCH -> {
+ // Add/update only specified prefixes
+ newPrefixes.forEach((prefix, iri) ->
+ patchStr.append("PA ").append(prefix).append(": <").append(iri).append("> .\n"));
+ }
+ case DELETE -> {
+ // Remove specified prefixes
+ newPrefixes.keySet().forEach(prefix ->
+ patchStr.append("PD ").append(prefix).append(": .\n"));
+ }
+ }
+
+ patchStr.append("TC .");
+ return RDFPatchOps.read(patchStr.toString());
+ }
+
+ /**
+ * Generates default commit message.
+ *
+ * @param cmd the command
+ * @return default message
+ */
+ private String generateDefaultMessage(UpdatePrefixesCommand cmd) {
+ return switch (cmd.operation()) {
+ case PUT -> "Replace prefix map";
+ case PATCH -> "Add prefixes: " + String.join(", ", cmd.newPrefixes().keySet());
+ case DELETE -> "Remove prefixes: " + String.join(", ", cmd.newPrefixes().keySet());
+ };
+ }
+}
+```
+
+---
+
+### Step 3: Create REST Controller (60 minutes)
+
+**3.1 Create `PrefixManagementController.java`**
+
+```java
+package org.chucc.vcserver.controller;
+
+import jakarta.validation.Valid;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.chucc.vcserver.command.UpdatePrefixesCommand;
+import org.chucc.vcserver.command.UpdatePrefixesCommand.Operation;
+import org.chucc.vcserver.command.UpdatePrefixesCommandHandler;
+import org.chucc.vcserver.domain.Branch;
+import org.chucc.vcserver.domain.CommitMetadata;
+import org.chucc.vcserver.dto.CommitResponse;
+import org.chucc.vcserver.dto.PrefixResponse;
+import org.chucc.vcserver.dto.UpdatePrefixesRequest;
+import org.chucc.vcserver.exception.BranchNotFoundException;
+import org.chucc.vcserver.repository.BranchRepository;
+import org.chucc.vcserver.repository.MaterializedBranchRepository;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * REST controller for Prefix Management Protocol (PMP).
+ *
+ * Implements version-aware prefix management with commit creation.
+ *
+ * @see PMP Specification
+ */
+@RestController
+@RequestMapping("/version/datasets/{dataset}")
+public class PrefixManagementController {
+
+ private final MaterializedBranchRepository materializedBranchRepository;
+ private final BranchRepository branchRepository;
+ private final UpdatePrefixesCommandHandler updatePrefixesCommandHandler;
+
+ /**
+ * Creates a prefix management controller.
+ *
+ * @param materializedBranchRepository the materialized branch repository
+ * @param branchRepository the branch repository
+ * @param updatePrefixesCommandHandler the prefix update handler
+ */
+ public PrefixManagementController(
+ MaterializedBranchRepository materializedBranchRepository,
+ BranchRepository branchRepository,
+ UpdatePrefixesCommandHandler updatePrefixesCommandHandler) {
+ this.materializedBranchRepository = materializedBranchRepository;
+ this.branchRepository = branchRepository;
+ this.updatePrefixesCommandHandler = updatePrefixesCommandHandler;
+ }
+
+ /**
+ * Retrieves prefix mappings for a branch.
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @return prefix response
+ * @throws BranchNotFoundException if branch doesn't exist
+ */
+ @GetMapping("/branches/{branch}/prefixes")
+ public ResponseEntity getCurrentPrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch) {
+
+ // Get branch (validate exists)
+ Branch branchObj = branchRepository
+ .findByDatasetAndName(dataset, branch)
+ .orElseThrow(() -> new BranchNotFoundException(dataset, branch));
+
+ // Read prefixes from materialized branch
+ DatasetGraph dsg = materializedBranchRepository
+ .getMaterializedBranch(dataset, branch);
+ Map prefixes = dsg.getDefaultGraph()
+ .getPrefixMapping()
+ .getNsPrefixMap();
+
+ PrefixResponse response = new PrefixResponse(
+ dataset,
+ branch,
+ branchObj.headCommitId(),
+ prefixes
+ );
+
+ return ResponseEntity
+ .ok()
+ .eTag(branchObj.headCommitId())
+ .body(response);
+ }
+
+ /**
+ * Replaces entire prefix map (creates commit).
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @param author the commit author
+ * @param request the update request
+ * @return commit response
+ */
+ @PutMapping("/branches/{branch}/prefixes")
+ public ResponseEntity replacePrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch,
+ @RequestHeader("SPARQL-VC-Author") String author,
+ @Valid @RequestBody UpdatePrefixesRequest request) {
+
+ UpdatePrefixesCommand cmd = new UpdatePrefixesCommand(
+ dataset,
+ branch,
+ author,
+ request.prefixes(),
+ Operation.PUT,
+ Optional.ofNullable(request.message())
+ );
+
+ CommitMetadata commit = updatePrefixesCommandHandler.handle(cmd);
+
+ URI location = URI.create(
+ "/version/datasets/" + dataset + "/commits/" + commit.id()
+ );
+
+ CommitResponse response = CommitResponse.from(dataset, branch, commit);
+
+ return ResponseEntity
+ .created(location)
+ .eTag(commit.id())
+ .body(response);
+ }
+
+ /**
+ * Adds or updates selected prefixes (creates commit).
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @param author the commit author
+ * @param request the update request
+ * @return commit response
+ */
+ @PatchMapping("/branches/{branch}/prefixes")
+ public ResponseEntity updatePrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch,
+ @RequestHeader("SPARQL-VC-Author") String author,
+ @Valid @RequestBody UpdatePrefixesRequest request) {
+
+ UpdatePrefixesCommand cmd = new UpdatePrefixesCommand(
+ dataset,
+ branch,
+ author,
+ request.prefixes(),
+ Operation.PATCH,
+ Optional.ofNullable(request.message())
+ );
+
+ CommitMetadata commit = updatePrefixesCommandHandler.handle(cmd);
+
+ URI location = URI.create(
+ "/version/datasets/" + dataset + "/commits/" + commit.id()
+ );
+
+ CommitResponse response = CommitResponse.from(dataset, branch, commit);
+
+ return ResponseEntity
+ .created(location)
+ .eTag(commit.id())
+ .body(response);
+ }
+
+ /**
+ * Removes specified prefixes (creates commit).
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @param prefixNames the prefix names to remove
+ * @param message optional commit message
+ * @param author the commit author
+ * @return commit response
+ */
+ @DeleteMapping("/branches/{branch}/prefixes")
+ public ResponseEntity deletePrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch,
+ @RequestParam("prefix") List prefixNames,
+ @RequestParam(required = false) String message,
+ @RequestHeader("SPARQL-VC-Author") String author) {
+
+ // Convert prefix list to map (value doesn't matter for DELETE)
+ Map prefixesToDelete = prefixNames.stream()
+ .collect(java.util.stream.Collectors.toMap(p -> p, p -> ""));
+
+ UpdatePrefixesCommand cmd = new UpdatePrefixesCommand(
+ dataset,
+ branch,
+ author,
+ prefixesToDelete,
+ Operation.DELETE,
+ Optional.ofNullable(message)
+ );
+
+ CommitMetadata commit = updatePrefixesCommandHandler.handle(cmd);
+
+ URI location = URI.create(
+ "/version/datasets/" + dataset + "/commits/" + commit.id()
+ );
+
+ CommitResponse response = CommitResponse.from(dataset, branch, commit);
+
+ return ResponseEntity
+ .created(location)
+ .eTag(commit.id())
+ .body(response);
+ }
+}
+```
+
+---
+
+### Step 4: Write Integration Tests (90 minutes)
+
+**4.1 Create `PrefixManagementIT.java`**
+
+```java
+package org.chucc.vcserver.integration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.chucc.vcserver.dto.CommitResponse;
+import org.chucc.vcserver.dto.PrefixResponse;
+import org.chucc.vcserver.dto.UpdatePrefixesRequest;
+import org.chucc.vcserver.testutil.IntegrationTestFixture;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+
+/**
+ * Integration tests for Prefix Management Protocol (PMP).
+ *
+ * Tests API layer only (projector disabled by default).
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("it")
+class PrefixManagementIT extends IntegrationTestFixture {
+
+ @Test
+ void getPrefixes_shouldReturnEmptyMap_whenNoPrefix() {
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes",
+ HttpMethod.GET,
+ null,
+ PrefixResponse.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().prefixes()).isEmpty();
+ assertThat(response.getHeaders().getETag()).isNotNull();
+ }
+
+ @Test
+ void putPrefixes_shouldReturn201Created() {
+ // Arrange
+ UpdatePrefixesRequest request = new UpdatePrefixesRequest(
+ "Add RDF prefixes",
+ Map.of(
+ "rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "rdfs", "http://www.w3.org/2000/01/rdf-schema#"
+ )
+ );
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set("SPARQL-VC-Author", "TestUser ");
+
+ HttpEntity httpEntity = new HttpEntity<>(request, headers);
+
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes",
+ HttpMethod.PUT,
+ httpEntity,
+ CommitResponse.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().commitId()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo("Add RDF prefixes");
+ assertThat(response.getHeaders().getLocation()).isNotNull();
+ assertThat(response.getHeaders().getETag()).isNotNull();
+
+ // Note: Repository updates handled by ReadModelProjector (disabled in this test)
+ }
+
+ @Test
+ void patchPrefixes_shouldReturn201Created() {
+ // Arrange
+ UpdatePrefixesRequest request = new UpdatePrefixesRequest(
+ "Add FOAF prefix",
+ Map.of("foaf", "http://xmlns.com/foaf/0.1/")
+ );
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set("SPARQL-VC-Author", "TestUser ");
+
+ HttpEntity httpEntity = new HttpEntity<>(request, headers);
+
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes",
+ HttpMethod.PATCH,
+ httpEntity,
+ CommitResponse.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo("Add FOAF prefix");
+ }
+
+ @Test
+ void deletePrefixes_shouldReturn201Created() {
+ // Arrange
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("SPARQL-VC-Author", "TestUser ");
+
+ HttpEntity httpEntity = new HttpEntity<>(headers);
+
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes?prefix=temp&prefix=test",
+ HttpMethod.DELETE,
+ httpEntity,
+ CommitResponse.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).contains("Remove prefixes");
+ }
+
+ @Test
+ void putPrefixes_shouldReturn400_whenAuthorMissing() {
+ // Arrange
+ UpdatePrefixesRequest request = new UpdatePrefixesRequest(
+ null,
+ Map.of("ex", "http://example.org/")
+ );
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ // No SPARQL-VC-Author header
+
+ HttpEntity httpEntity = new HttpEntity<>(request, headers);
+
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes",
+ HttpMethod.PUT,
+ httpEntity,
+ String.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ }
+
+ @Test
+ void getPrefixes_shouldReturn404_whenBranchNotFound() {
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/nonexistent/prefixes",
+ HttpMethod.GET,
+ null,
+ String.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ }
+}
+```
+
+Add 4-5 more tests:
+- `putPrefixes_shouldReplaceAllPrefixes()`
+- `patchPrefixes_shouldPreserveExistingPrefixes()`
+- `deletePrefixes_shouldBeIdempotent()`
+- `putPrefixes_shouldValidatePrefixNames()`
+- `putPrefixes_shouldValidateAbsoluteIris()`
+
+---
+
+### Step 5: Write Unit Tests (30 minutes)
+
+**5.1 Create `UpdatePrefixesCommandHandlerTest.java`**
+
+```java
+package org.chucc.vcserver.command;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+import java.util.Optional;
+import org.apache.jena.rdfpatch.RDFPatch;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for UpdatePrefixesCommandHandler.
+ */
+class UpdatePrefixesCommandHandlerTest {
+
+ @Test
+ void buildPrefixPatch_shouldGeneratePaDirectives_forPatchOperation() {
+ // Arrange
+ UpdatePrefixesCommandHandler handler = new UpdatePrefixesCommandHandler(
+ mock(MaterializedBranchRepository.class),
+ mock(BranchRepository.class),
+ mock(CreateCommitCommandHandler.class)
+ );
+
+ Map oldPrefixes = Map.of();
+ Map newPrefixes = Map.of(
+ "foaf", "http://xmlns.com/foaf/0.1/",
+ "geo", "http://www.opengis.net/ont/geosparql#"
+ );
+
+ // Act
+ RDFPatch patch = handler.buildPrefixPatch(
+ oldPrefixes,
+ newPrefixes,
+ UpdatePrefixesCommand.Operation.PATCH
+ );
+
+ // Assert
+ String patchStr = patch.toString();
+ assertThat(patchStr).contains("PA foaf: ");
+ assertThat(patchStr).contains("PA geo: ");
+ assertThat(patchStr).doesNotContain("PD"); // No deletions
+ }
+
+ @Test
+ void buildPrefixPatch_shouldGeneratePdThenPa_forPutOperation() {
+ // Arrange
+ UpdatePrefixesCommandHandler handler = new UpdatePrefixesCommandHandler(
+ mock(MaterializedBranchRepository.class),
+ mock(BranchRepository.class),
+ mock(CreateCommitCommandHandler.class)
+ );
+
+ Map oldPrefixes = Map.of(
+ "old1", "http://example.org/old1/",
+ "old2", "http://example.org/old2/"
+ );
+ Map newPrefixes = Map.of(
+ "new", "http://example.org/new/"
+ );
+
+ // Act
+ RDFPatch patch = handler.buildPrefixPatch(
+ oldPrefixes,
+ newPrefixes,
+ UpdatePrefixesCommand.Operation.PUT
+ );
+
+ // Assert
+ String patchStr = patch.toString();
+ assertThat(patchStr).contains("PD old1:");
+ assertThat(patchStr).contains("PD old2:");
+ assertThat(patchStr).contains("PA new: ");
+ }
+
+ @Test
+ void buildPrefixPatch_shouldGeneratePdDirectives_forDeleteOperation() {
+ // Arrange
+ UpdatePrefixesCommandHandler handler = new UpdatePrefixesCommandHandler(
+ mock(MaterializedBranchRepository.class),
+ mock(BranchRepository.class),
+ mock(CreateCommitCommandHandler.class)
+ );
+
+ Map oldPrefixes = Map.of();
+ Map prefixesToDelete = Map.of(
+ "temp", "",
+ "test", ""
+ );
+
+ // Act
+ RDFPatch patch = handler.buildPrefixPatch(
+ oldPrefixes,
+ prefixesToDelete,
+ UpdatePrefixesCommand.Operation.DELETE
+ );
+
+ // Assert
+ String patchStr = patch.toString();
+ assertThat(patchStr).contains("PD temp:");
+ assertThat(patchStr).contains("PD test:");
+ assertThat(patchStr).doesNotContain("PA"); // No additions
+ }
+}
+```
+
+---
+
+### Step 6: Run Tests and Fix Issues (30 minutes)
+
+**6.1 Run static analysis:**
+```bash
+mvn -q compile checkstyle:check spotbugs:check pmd:check
+```
+
+**6.2 Run tests:**
+```bash
+mvn -q test -Dtest=PrefixManagementIT
+mvn -q test -Dtest=UpdatePrefixesCommandHandlerTest
+```
+
+**6.3 Fix any issues:**
+- Checkstyle violations
+- SpotBugs warnings (defensive copying, etc.)
+- Test failures
+
+---
+
+### Step 7: Verify CQRS Compliance (15 minutes)
+
+After implementation, invoke the specialized agent:
+
+```
+@cqrs-compliance-checker
+
+Please verify UpdatePrefixesCommandHandler follows CQRS patterns:
+- Command handler should delegate to CreateCommitCommandHandler
+- No new events should be created
+- No direct repository writes
+```
+
+---
+
+## Success Criteria
+
+### Functional
+- ✅ GET returns prefixes from materialized branch
+- ✅ PUT creates commit with PD (old) + PA (new) directives
+- ✅ PATCH creates commit with PA directives only
+- ✅ DELETE creates commit with PD directives
+- ✅ All operations return 201 Created with commit metadata
+- ✅ Location header points to created commit
+- ✅ ETag contains commit ID
+
+### Quality
+- ✅ All tests pass (10+ integration tests)
+- ✅ Zero Checkstyle violations
+- ✅ Zero SpotBugs warnings
+- ✅ Zero PMD violations
+- ✅ Zero compiler warnings
+
+### CQRS Compliance
+- ✅ No new events created (reuses CommitCreatedEvent)
+- ✅ No direct repository writes
+- ✅ Command handler delegates to CreateCommitCommandHandler
+- ✅ Projector applies PA/PD directives automatically
+
+---
+
+## Files to Create
+
+```
+src/main/java/org/chucc/vcserver/
+ ├── dto/
+ │ ├── PrefixResponse.java # NEW
+ │ ├── UpdatePrefixesRequest.java # NEW
+ │ └── CommitResponse.java # NEW (reusable)
+ ├── command/
+ │ ├── UpdatePrefixesCommand.java # NEW
+ │ └── UpdatePrefixesCommandHandler.java # NEW
+ └── controller/
+ └── PrefixManagementController.java # NEW
+
+src/test/java/org/chucc/vcserver/
+ ├── integration/
+ │ └── PrefixManagementIT.java # NEW
+ └── command/
+ └── UpdatePrefixesCommandHandlerTest.java # NEW
+```
+
+---
+
+## Common Issues and Solutions
+
+### Issue 1: RDFPatch Parsing Fails
+**Symptom:** `RDFPatchOps.read()` throws exception
+
+**Solution:** Ensure proper format:
+```
+TX .
+PA foaf: .
+PD temp: .
+TC .
+```
+(Note: space before period, no space after colon in PD)
+
+### Issue 2: Prefixes Not Updated
+**Symptom:** GET returns old prefixes after PUT
+
+**Solution:** This is EXPECTED with projector disabled (test isolation pattern). The projector would apply PA/PD directives in production.
+
+### Issue 3: Missing Author Header Returns 500
+**Symptom:** Should return 400 Bad Request
+
+**Solution:** Add validation in controller:
+```java
+@RequestHeader("SPARQL-VC-Author") String author
+```
+Spring will return 400 if header missing.
+
+---
+
+## Next Steps
+
+After completing this session:
+
+1. ✅ Mark session as completed in [README.md](./README.md)
+2. ✅ Commit changes with message:
+ ```
+ feat(pmp): implement core prefix management operations
+
+ - Add GET/PUT/PATCH/DELETE endpoints for prefix management
+ - Create UpdatePrefixesCommandHandler (reuses CreateCommitCommandHandler)
+ - Add PrefixManagementController
+ - Add integration tests (10+ tests)
+ - Add unit tests for command handler
+
+ Prefix changes now create commits with RDFPatch PA/PD directives.
+ All operations version-controlled via existing CQRS infrastructure.
+
+ 🤖 Generated with Claude Code
+
+ Co-Authored-By: Claude
+ ```
+3. ✅ Proceed to [Session 2: Time-Travel Support](./session-2-time-travel-support.md)
+
+---
+
+## References
+
+- [Prefix Management Protocol v1.0](../../protocol/Prefix_Management_Protocol.md)
+- [CHUCC Implementation Guide](../../docs/api/prefix-management.md)
+- [RDFPatch PA/PD Directives](https://afs.github.io/rdf-patch/#prefix-directives)
+- [CreateCommitCommandHandler.java](../../src/main/java/org/chucc/vcserver/command/CreateCommitCommandHandler.java)
+
+---
+
+**Ready to start!** Begin with Step 1 (Create DTOs) and work through sequentially.
diff --git a/.tasks/pmp/session-2-time-travel-support.md b/.tasks/pmp/session-2-time-travel-support.md
new file mode 100644
index 0000000..31cb7fe
--- /dev/null
+++ b/.tasks/pmp/session-2-time-travel-support.md
@@ -0,0 +1,270 @@
+# Session 2: Time-Travel Prefix Queries
+
+**Status:** Not Started
+**Estimated Time:** 2-3 hours
+**Priority:** Medium
+**Dependencies:** Session 1 (Core Implementation)
+
+---
+
+## Overview
+
+Add ability to query prefixes at specific commits (historical queries). Enables users to see what prefixes existed at any point in history.
+
+**Goal:** Support `GET /version/datasets/{name}/commits/{id}/prefixes` for time-travel queries.
+
+---
+
+## Requirements
+
+### Endpoint to Implement
+
+```http
+GET /version/datasets/{dataset}/commits/{commitId}/prefixes
+Accept: application/json
+
+→ 200 OK
+{
+ "dataset": "mydata",
+ "commitId": "01JCDN2XYZ...",
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+**Note:** No `branch` field (this is a commit query, not branch query).
+
+---
+
+## Implementation Steps
+
+### Step 1: Add Endpoint to Controller (30 minutes)
+
+```java
+/**
+ * Retrieves prefix mappings at a specific commit (time-travel).
+ *
+ * @param dataset the dataset name
+ * @param commitId the commit ID
+ * @return prefix response
+ * @throws CommitNotFoundException if commit doesn't exist
+ */
+@GetMapping("/commits/{commitId}/prefixes")
+public ResponseEntity getPrefixesAtCommit(
+ @PathVariable String dataset,
+ @PathVariable String commitId) {
+
+ // Rebuild dataset at specific commit
+ DatasetGraph dsg = materializedViewRebuildService
+ .rebuildAtCommit(dataset, commitId);
+
+ Map prefixes = dsg.getDefaultGraph()
+ .getPrefixMapping()
+ .getNsPrefixMap();
+
+ PrefixResponse response = new PrefixResponse(
+ dataset,
+ null, // No branch (commit query)
+ commitId,
+ prefixes
+ );
+
+ return ResponseEntity
+ .ok()
+ .eTag(commitId)
+ .body(response);
+}
+```
+
+**Key insight:** Reuses existing `MaterializedViewRebuildService` - no new code needed!
+
+---
+
+### Step 2: Add Dependency Injection (5 minutes)
+
+```java
+private final MaterializedViewRebuildService materializedViewRebuildService;
+
+public PrefixManagementController(
+ MaterializedBranchRepository materializedBranchRepository,
+ BranchRepository branchRepository,
+ UpdatePrefixesCommandHandler updatePrefixesCommandHandler,
+ MaterializedViewRebuildService materializedViewRebuildService) { // NEW
+ this.materializedBranchRepository = materializedBranchRepository;
+ this.branchRepository = branchRepository;
+ this.updatePrefixesCommandHandler = updatePrefixesCommandHandler;
+ this.materializedViewRebuildService = materializedViewRebuildService; // NEW
+}
+```
+
+---
+
+### Step 3: Write Integration Tests (60 minutes)
+
+**Add to `PrefixManagementIT.java`:**
+
+```java
+@Test
+void getPrefixesAtCommit_shouldReturnHistoricalPrefixes() {
+ // Arrange: Create commit with specific prefixes
+ UpdatePrefixesRequest request = new UpdatePrefixesRequest(
+ "Add historical prefixes",
+ Map.of("old", "http://example.org/old/")
+ );
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set("SPARQL-VC-Author", "TestUser ");
+ HttpEntity httpEntity = new HttpEntity<>(request, headers);
+
+ ResponseEntity createResponse = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes",
+ HttpMethod.PUT,
+ httpEntity,
+ CommitResponse.class
+ );
+
+ String commitId = createResponse.getBody().commitId();
+
+ // Act: Query prefixes at that commit
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/commits/" + commitId + "/prefixes",
+ HttpMethod.GET,
+ null,
+ PrefixResponse.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().commitId()).isEqualTo(commitId);
+ assertThat(response.getBody().branch()).isNull(); // No branch (commit query)
+ assertThat(response.getBody().prefixes()).containsKey("old");
+}
+
+@Test
+void getPrefixesAtCommit_shouldReturn404_whenCommitNotFound() {
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/commits/nonexistent-commit-id/prefixes",
+ HttpMethod.GET,
+ null,
+ String.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+}
+
+@Test
+void getPrefixesAtCommit_shouldReturnDifferentPrefixes_forDifferentCommits() {
+ // Arrange: Create two commits with different prefixes
+ // Commit 1: Add "v1" prefix
+ UpdatePrefixesRequest request1 = new UpdatePrefixesRequest(
+ "Version 1 prefixes",
+ Map.of("v1", "http://example.org/v1/")
+ );
+ String commitId1 = createPrefixCommit(request1);
+
+ // Commit 2: Add "v2" prefix
+ UpdatePrefixesRequest request2 = new UpdatePrefixesRequest(
+ "Version 2 prefixes",
+ Map.of("v2", "http://example.org/v2/")
+ );
+ String commitId2 = createPrefixCommit(request2);
+
+ // Act: Query both commits
+ PrefixResponse response1 = getPrefixesAtCommit(commitId1);
+ PrefixResponse response2 = getPrefixesAtCommit(commitId2);
+
+ // Assert
+ assertThat(response1.prefixes()).containsKey("v1");
+ assertThat(response1.prefixes()).doesNotContainKey("v2");
+
+ assertThat(response2.prefixes()).containsKey("v1"); // Inherited
+ assertThat(response2.prefixes()).containsKey("v2"); // Added
+}
+```
+
+Add 2-3 more tests:
+- `getPrefixesAtCommit_shouldUseCacheForRecentCommits()`
+- `getPrefixesAtCommit_shouldRebuildForOldCommits()`
+
+---
+
+### Step 4: Performance Testing (30 minutes)
+
+**Verify caching behavior:**
+
+```java
+@Test
+void getPrefixesAtCommit_shouldBeFast_whenCommitCached() {
+ // Arrange: Create commit
+ String commitId = createPrefixCommit(...);
+
+ // Prime cache by accessing commit
+ materializedViewRebuildService.rebuildAtCommit("default", commitId);
+
+ // Act: Time subsequent access
+ long start = System.currentTimeMillis();
+ getPrefixesAtCommit(commitId);
+ long duration = System.currentTimeMillis() - start;
+
+ // Assert: Should be fast (<50ms for cached)
+ assertThat(duration).isLessThan(50);
+}
+
+@Test
+void getPrefixesAtCommit_shouldRebuildOnDemand_whenNotCached() {
+ // Arrange: Create old commit that might be evicted
+ String oldCommitId = createPrefixCommit(...);
+
+ // Act: Query (may require rebuild)
+ ResponseEntity response = getPrefixesAtCommit(oldCommitId);
+
+ // Assert: Should work (even if slower)
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ // Typically ~1s for rebuild with 100 commits
+}
+```
+
+---
+
+## Success Criteria
+
+### Functional
+- ✅ GET `/commits/{id}/prefixes` returns historical prefixes
+- ✅ Response includes `commitId`, no `branch`
+- ✅ Works for any commit (cached or not)
+- ✅ Returns 404 if commit doesn't exist
+
+### Performance
+- ✅ Cached commits: <50ms
+- ✅ Uncached commits: <2s typical (rebuild)
+- ✅ No memory leaks (cache eviction works)
+
+### Quality
+- ✅ All tests pass (5+ new tests)
+- ✅ Zero quality violations
+
+---
+
+## Files to Modify
+
+```
+src/main/java/org/chucc/vcserver/controller/
+ └── PrefixManagementController.java # Add getPrefixesAtCommit() method
+
+src/test/java/org/chucc/vcserver/integration/
+ └── PrefixManagementIT.java # Add 5+ tests
+```
+
+---
+
+## Next Steps
+
+After completing:
+1. ✅ Commit changes
+2. ✅ Proceed to [Session 3: Suggested Prefixes](./session-3-suggested-prefixes.md)
diff --git a/.tasks/pmp/session-3-suggested-prefixes.md b/.tasks/pmp/session-3-suggested-prefixes.md
new file mode 100644
index 0000000..3cdda6e
--- /dev/null
+++ b/.tasks/pmp/session-3-suggested-prefixes.md
@@ -0,0 +1,452 @@
+# Session 3: Suggested Prefixes (Namespace Discovery)
+
+**Status:** Not Started
+**Estimated Time:** 2-3 hours
+**Priority:** Low (Nice-to-Have)
+**Dependencies:** Session 1 (Core Implementation)
+
+---
+
+## Overview
+
+Analyze dataset to discover common namespaces and suggest conventional prefixes. Helps users after importing RDF/XML or discovering new ontologies.
+
+**Goal:** Support `GET /version/datasets/{name}/branches/{branch}/prefixes/suggested`
+
+---
+
+## Requirements
+
+### Endpoint to Implement
+
+```http
+GET /version/datasets/{dataset}/branches/{branch}/prefixes/suggested
+Accept: application/json
+
+→ 200 OK
+{
+ "dataset": "mydata",
+ "branch": "main",
+ "suggestions": [
+ {
+ "prefix": "foaf",
+ "iri": "http://xmlns.com/foaf/0.1/",
+ "frequency": 42,
+ "status": "already_defined"
+ },
+ {
+ "prefix": "schema",
+ "iri": "http://schema.org/",
+ "frequency": 38,
+ "status": "suggested"
+ },
+ {
+ "prefix": "dbo",
+ "iri": "http://dbpedia.org/ontology/",
+ "frequency": 15,
+ "status": "suggested"
+ }
+ ]
+}
+```
+
+---
+
+## Implementation Steps
+
+### Step 1: Create DTOs (20 minutes)
+
+```java
+package org.chucc.vcserver.dto;
+
+/**
+ * Suggested prefix for a namespace.
+ */
+public record PrefixSuggestion(
+ String prefix,
+ String iri,
+ int frequency, // How many times namespace appears
+ Status status
+) {
+ public enum Status {
+ SUGGESTED, // Not yet defined
+ ALREADY_DEFINED // Already in prefix map
+ }
+}
+
+/**
+ * Response containing prefix suggestions.
+ */
+public record SuggestedPrefixesResponse(
+ String dataset,
+ String branch,
+ List suggestions
+) {
+ public SuggestedPrefixesResponse {
+ suggestions = List.copyOf(suggestions);
+ }
+}
+```
+
+---
+
+### Step 2: Create Conventional Prefix Database (30 minutes)
+
+```java
+package org.chucc.vcserver.service;
+
+import java.util.Map;
+
+/**
+ * Database of conventional prefixes for common namespaces.
+ *
+ * Subset of prefix.cc database.
+ */
+public class ConventionalPrefixes {
+
+ private static final Map CONVENTIONAL_PREFIXES = Map.ofEntries(
+ // Core RDF/OWL/RDFS
+ Map.entry("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf"),
+ Map.entry("http://www.w3.org/2000/01/rdf-schema#", "rdfs"),
+ Map.entry("http://www.w3.org/2002/07/owl#", "owl"),
+ Map.entry("http://www.w3.org/2001/XMLSchema#", "xsd"),
+
+ // Popular ontologies
+ Map.entry("http://xmlns.com/foaf/0.1/", "foaf"),
+ Map.entry("http://purl.org/dc/terms/", "dct"),
+ Map.entry("http://purl.org/dc/elements/1.1/", "dc"),
+ Map.entry("http://schema.org/", "schema"),
+ Map.entry("http://www.w3.org/2004/02/skos/core#", "skos"),
+
+ // Geospatial
+ Map.entry("http://www.opengis.net/ont/geosparql#", "geo"),
+ Map.entry("http://www.opengis.net/ont/sf#", "sf"),
+ Map.entry("http://www.w3.org/2003/01/geo/wgs84_pos#", "wgs84"),
+
+ // DBpedia
+ Map.entry("http://dbpedia.org/ontology/", "dbo"),
+ Map.entry("http://dbpedia.org/resource/", "dbr"),
+ Map.entry("http://dbpedia.org/property/", "dbp"),
+
+ // PROV
+ Map.entry("http://www.w3.org/ns/prov#", "prov"),
+
+ // Time
+ Map.entry("http://www.w3.org/2006/time#", "time"),
+
+ // Add more as needed...
+ );
+
+ /**
+ * Gets conventional prefix for a namespace.
+ *
+ * @param namespace the namespace IRI
+ * @return the conventional prefix, or null if unknown
+ */
+ public static String getConventionalPrefix(String namespace) {
+ return CONVENTIONAL_PREFIXES.get(namespace);
+ }
+
+ /**
+ * Checks if namespace has a conventional prefix.
+ *
+ * @param namespace the namespace IRI
+ * @return true if conventional prefix exists
+ */
+ public static boolean hasConventionalPrefix(String namespace) {
+ return CONVENTIONAL_PREFIXES.containsKey(namespace);
+ }
+}
+```
+
+---
+
+### Step 3: Create PrefixSuggestionService (60 minutes)
+
+```java
+package org.chucc.vcserver.service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.chucc.vcserver.dto.PrefixSuggestion;
+import org.chucc.vcserver.dto.PrefixSuggestion.Status;
+import org.chucc.vcserver.repository.MaterializedBranchRepository;
+import org.springframework.stereotype.Service;
+
+/**
+ * Service for suggesting prefix mappings based on dataset analysis.
+ */
+@Service
+public class PrefixSuggestionService {
+
+ private final MaterializedBranchRepository materializedBranchRepository;
+
+ public PrefixSuggestionService(MaterializedBranchRepository materializedBranchRepository) {
+ this.materializedBranchRepository = materializedBranchRepository;
+ }
+
+ /**
+ * Analyzes dataset and suggests conventional prefixes.
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @return list of prefix suggestions, sorted by frequency descending
+ */
+ public List analyzeBranch(String dataset, String branch) {
+ DatasetGraph dsg = materializedBranchRepository.getMaterializedBranch(dataset, branch);
+
+ // 1. Get current prefixes
+ Map currentPrefixes = dsg.getDefaultGraph()
+ .getPrefixMapping()
+ .getNsPrefixMap();
+
+ // Invert map for lookup (IRI → prefix)
+ Map iriToPrefixMap = currentPrefixes.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
+
+ // 2. Scan dataset for namespace patterns
+ Map namespaceFrequency = scanForNamespaces(dsg);
+
+ // 3. Match against conventional prefixes
+ List suggestions = new ArrayList<>();
+
+ for (Map.Entry entry : namespaceFrequency.entrySet()) {
+ String namespace = entry.getKey();
+ int frequency = entry.getValue();
+
+ String conventionalPrefix = ConventionalPrefixes.getConventionalPrefix(namespace);
+ if (conventionalPrefix != null) {
+ Status status = iriToPrefixMap.containsKey(namespace)
+ ? Status.ALREADY_DEFINED
+ : Status.SUGGESTED;
+
+ suggestions.add(new PrefixSuggestion(
+ conventionalPrefix,
+ namespace,
+ frequency,
+ status
+ ));
+ }
+ }
+
+ // 4. Sort by frequency descending
+ suggestions.sort(Comparator.comparingInt(PrefixSuggestion::frequency).reversed());
+
+ return suggestions;
+ }
+
+ /**
+ * Scans dataset for namespace patterns.
+ *
+ * @param dsg the dataset graph
+ * @return map of namespace → frequency
+ */
+ private Map scanForNamespaces(DatasetGraph dsg) {
+ Map namespaceFrequency = new HashMap<>();
+
+ // Scan all graphs
+ dsg.listGraphNodes().forEachRemaining(graphName -> {
+ dsg.getGraph(graphName).find().forEachRemaining(triple -> {
+ extractNamespace(triple.getSubject()).ifPresent(ns ->
+ namespaceFrequency.merge(ns, 1, Integer::sum));
+ extractNamespace(triple.getPredicate()).ifPresent(ns ->
+ namespaceFrequency.merge(ns, 1, Integer::sum));
+ extractNamespace(triple.getObject()).ifPresent(ns ->
+ namespaceFrequency.merge(ns, 1, Integer::sum));
+ });
+ });
+
+ // Scan default graph
+ dsg.getDefaultGraph().find().forEachRemaining(triple -> {
+ extractNamespace(triple.getSubject()).ifPresent(ns ->
+ namespaceFrequency.merge(ns, 1, Integer::sum));
+ extractNamespace(triple.getPredicate()).ifPresent(ns ->
+ namespaceFrequency.merge(ns, 1, Integer::sum));
+ extractNamespace(triple.getObject()).ifPresent(ns ->
+ namespaceFrequency.merge(ns, 1, Integer::sum));
+ });
+
+ return namespaceFrequency;
+ }
+
+ /**
+ * Extracts namespace from a node (if it's a URI).
+ *
+ * @param node the RDF node
+ * @return optional namespace
+ */
+ private Optional extractNamespace(Node node) {
+ if (!node.isURI()) {
+ return Optional.empty();
+ }
+
+ String uri = node.getURI();
+
+ // Extract namespace (everything up to last # or /)
+ int hashIndex = uri.lastIndexOf('#');
+ int slashIndex = uri.lastIndexOf('/');
+ int splitIndex = Math.max(hashIndex, slashIndex);
+
+ if (splitIndex > 0) {
+ return Optional.of(uri.substring(0, splitIndex + 1));
+ }
+
+ return Optional.empty();
+ }
+}
+```
+
+---
+
+### Step 4: Add Controller Endpoint (20 minutes)
+
+```java
+/**
+ * Suggests prefix mappings based on dataset analysis.
+ *
+ * @param dataset the dataset name
+ * @param branch the branch name
+ * @return prefix suggestions
+ */
+@GetMapping("/branches/{branch}/prefixes/suggested")
+public ResponseEntity suggestPrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch) {
+
+ List suggestions = prefixSuggestionService
+ .analyzeBranch(dataset, branch);
+
+ SuggestedPrefixesResponse response = new SuggestedPrefixesResponse(
+ dataset,
+ branch,
+ suggestions
+ );
+
+ return ResponseEntity.ok(response);
+}
+```
+
+---
+
+### Step 5: Write Tests (40 minutes)
+
+```java
+@Test
+void suggestPrefixes_shouldReturnSuggestions() {
+ // Arrange: Add triples with known namespaces
+ String rdfPatch = """
+ TX .
+ A "Alice" .
+ A "Bob" .
+ TC .
+ """;
+ createCommitWithPatch(rdfPatch);
+
+ // Act
+ ResponseEntity response = restTemplate.exchange(
+ "/version/datasets/default/branches/main/prefixes/suggested",
+ HttpMethod.GET,
+ null,
+ SuggestedPrefixesResponse.class
+ );
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().suggestions())
+ .anyMatch(s -> s.prefix().equals("foaf")
+ && s.iri().equals("http://xmlns.com/foaf/0.1/")
+ && s.status() == Status.SUGGESTED);
+}
+
+@Test
+void suggestPrefixes_shouldMarkAlreadyDefined() {
+ // Arrange: Define foaf prefix
+ definePrefixes(Map.of("foaf", "http://xmlns.com/foaf/0.1/"));
+
+ // Add triples using foaf namespace
+ createCommitWithTriples(...);
+
+ // Act
+ SuggestedPrefixesResponse response = getSuggestions();
+
+ // Assert
+ assertThat(response.suggestions())
+ .anyMatch(s -> s.prefix().equals("foaf")
+ && s.status() == Status.ALREADY_DEFINED);
+}
+
+@Test
+void suggestPrefixes_shouldSortByFrequency() {
+ // Arrange: Add triples with multiple namespaces
+ // 10x foaf, 5x schema, 2x dbo
+ createCommitWithMultipleNamespaces();
+
+ // Act
+ List suggestions = getSuggestions().suggestions();
+
+ // Assert
+ assertThat(suggestions.get(0).prefix()).isEqualTo("foaf"); // Most frequent
+ assertThat(suggestions.get(1).prefix()).isEqualTo("schema");
+ assertThat(suggestions.get(2).prefix()).isEqualTo("dbo");
+}
+```
+
+---
+
+## Success Criteria
+
+### Functional
+- ✅ Analyzes dataset for namespace patterns
+- ✅ Suggests conventional prefixes (prefix.cc subset)
+- ✅ Marks already-defined prefixes
+- ✅ Sorts by frequency descending
+- ✅ Returns empty list if no suggestions
+
+### Performance
+- ✅ Completes in <500ms for typical datasets
+- ✅ No memory issues for large datasets
+
+### Quality
+- ✅ All tests pass (8+ new tests)
+- ✅ Zero quality violations
+
+---
+
+## Files to Create
+
+```
+src/main/java/org/chucc/vcserver/
+ ├── dto/
+ │ ├── PrefixSuggestion.java # NEW
+ │ └── SuggestedPrefixesResponse.java # NEW
+ └── service/
+ ├── ConventionalPrefixes.java # NEW
+ └── PrefixSuggestionService.java # NEW
+
+src/main/java/org/chucc/vcserver/controller/
+ └── PrefixManagementController.java # MODIFY (add endpoint)
+
+src/test/java/org/chucc/vcserver/integration/
+ └── PrefixManagementIT.java # MODIFY (add 8+ tests)
+```
+
+---
+
+## Optional Enhancements
+
+1. **Threshold filtering**: Only suggest namespaces with frequency ≥ N
+2. **Custom prefixes**: Allow users to define their own conventional prefixes
+3. **Prefix.cc API**: Fetch live data from prefix.cc instead of static map
+4. **Conflict detection**: Warn if suggested prefix already used for different namespace
+
+---
+
+## Next Steps
+
+After completing:
+1. ✅ Commit changes
+2. ✅ Proceed to [Session 4: OpenAPI and Tests](./session-4-openapi-and-tests.md)
diff --git a/.tasks/pmp/session-4-openapi-and-tests.md b/.tasks/pmp/session-4-openapi-and-tests.md
new file mode 100644
index 0000000..3cae06f
--- /dev/null
+++ b/.tasks/pmp/session-4-openapi-and-tests.md
@@ -0,0 +1,207 @@
+# Session 4: OpenAPI Documentation and Comprehensive Testing
+
+**Status:** Not Started
+**Estimated Time:** 2-3 hours
+**Priority:** Medium
+**Dependencies:** Sessions 1-3
+
+---
+
+## Overview
+
+Complete OpenAPI documentation, add comprehensive error handling tests, and validate cross-protocol integration.
+
+**Goal:** Production-ready documentation and test coverage.
+
+---
+
+## Tasks
+
+### 1. OpenAPI Documentation (60 minutes)
+
+Add to existing OpenAPI spec:
+
+```yaml
+/version/datasets/{dataset}/branches/{branch}/prefixes:
+ get:
+ summary: Retrieve prefix mappings for a branch
+ operationId: getPrefixes
+ tags: [Prefix Management]
+ parameters:
+ - $ref: '#/components/parameters/DatasetParam'
+ - $ref: '#/components/parameters/BranchParam'
+ responses:
+ '200':
+ description: Prefix map retrieved
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PrefixResponse'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ put:
+ summary: Replace entire prefix map (creates commit)
+ operationId: replacePrefixes
+ tags: [Prefix Management]
+ parameters:
+ - $ref: '#/components/parameters/DatasetParam'
+ - $ref: '#/components/parameters/BranchParam'
+ - $ref: '#/components/parameters/AuthorHeader'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdatePrefixesRequest'
+ responses:
+ '201':
+ description: Commit created
+ headers:
+ Location:
+ schema:
+ type: string
+ ETag:
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CommitResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '403':
+ $ref: '#/components/responses/ProtectedBranch'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ # Add PATCH, DELETE similarly...
+
+components:
+ schemas:
+ PrefixResponse:
+ type: object
+ required: [dataset, commitId, prefixes]
+ properties:
+ dataset:
+ type: string
+ branch:
+ type: string
+ nullable: true
+ commitId:
+ type: string
+ prefixes:
+ type: object
+ additionalProperties:
+ type: string
+
+ UpdatePrefixesRequest:
+ type: object
+ required: [prefixes]
+ properties:
+ message:
+ type: string
+ prefixes:
+ type: object
+ additionalProperties:
+ type: string
+```
+
+---
+
+### 2. Error Handling Tests (60 minutes)
+
+```java
+@Test
+void putPrefixes_shouldReturn403_whenBranchProtected() {
+ // Arrange: Protect main branch
+ protectBranch("default", "main");
+
+ // Act
+ ResponseEntity response = putPrefixes(...);
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+}
+
+@Test
+void putPrefixes_shouldReturn400_whenInvalidPrefixName() {
+ UpdatePrefixesRequest request = new UpdatePrefixesRequest(
+ null,
+ Map.of("1invalid", "http://example.org/") // Starts with number
+ );
+
+ // Act & Assert
+ assertThat(putPrefixes(request).getStatusCode())
+ .isEqualTo(HttpStatus.BAD_REQUEST);
+}
+
+@Test
+void putPrefixes_shouldReturn400_whenRelativeIRI() {
+ UpdatePrefixesRequest request = new UpdatePrefixesRequest(
+ null,
+ Map.of("ex", "../relative") // Not absolute
+ );
+
+ // Act & Assert
+ assertThat(putPrefixes(request).getStatusCode())
+ .isEqualTo(HttpStatus.BAD_REQUEST);
+}
+```
+
+---
+
+### 3. Cross-Protocol Integration Tests (30 minutes)
+
+```java
+@Test
+void prefixesShouldPersistAfterGraphOperations() {
+ // Arrange: Define prefixes
+ definePrefixes(Map.of("foaf", "http://xmlns.com/foaf/0.1/"));
+
+ // Act: Perform GSP operation
+ putGraph("http://example.org/g1", "");
+
+ // Assert: Prefixes still present
+ assertThat(getPrefixes().prefixes()).containsKey("foaf");
+}
+
+@Test
+void prefixesShouldMergeAutomatically() {
+ // Arrange: Create branch with different prefixes
+ createBranch("dev");
+ definePrefixesOnBranch("dev", Map.of("geo", "http://..."));
+
+ // Act: Merge
+ mergeBranch("dev", "main");
+
+ // Assert: Both prefix sets present
+ PrefixResponse mainPrefixes = getPrefixesOnBranch("main");
+ assertThat(mainPrefixes.prefixes()).containsKey("geo");
+}
+```
+
+---
+
+## Files to Modify
+
+```
+docs/openapi/
+ └── prefix-management.yaml # NEW or add to existing
+
+src/test/java/org/chucc/vcserver/integration/
+ └── PrefixManagementIT.java # Add 10+ validation tests
+```
+
+---
+
+## Success Criteria
+
+- ✅ Complete OpenAPI documentation
+- ✅ All error cases covered (400, 403, 404)
+- ✅ Cross-protocol tests pass
+- ✅ Validation tests pass
+
+---
+
+**Next:** [Session 5: Merge Conflict Handling](./session-5-merge-conflict-handling.md) (Optional)
diff --git a/.tasks/pmp/session-5-merge-conflict-handling.md b/.tasks/pmp/session-5-merge-conflict-handling.md
new file mode 100644
index 0000000..6f6a4c1
--- /dev/null
+++ b/.tasks/pmp/session-5-merge-conflict-handling.md
@@ -0,0 +1,211 @@
+# Session 5: Merge Conflict Handling (Optional Enhancement)
+
+**Status:** Deferred
+**Estimated Time:** 3-4 hours
+**Priority:** Low (Future Enhancement)
+**Dependencies:** Sessions 1-4, Merge API
+
+---
+
+## Overview
+
+Enhance merge conflict detection to specifically identify and report prefix conflicts. Currently, prefix conflicts are handled as generic patch conflicts.
+
+**Goal:** Specialized conflict reporting for prefix mismatches.
+
+---
+
+## Current Behavior
+
+When same prefix points to different IRIs in two branches:
+
+```
+Branch main: PA foaf: .
+Branch dev: PA foaf: .
+
+Merge dev → main → Generic conflict
+```
+
+**Response:**
+```json
+{
+ "type": "/problems/merge-conflict",
+ "conflicts": [
+ {
+ "graph": "urn:x-arq:DefaultGraph",
+ "conflictingQuads": 1
+ }
+ ]
+}
+```
+
+---
+
+## Enhanced Behavior (This Session)
+
+**Response with prefix-specific conflict:**
+```json
+{
+ "type": "/problems/merge-conflict",
+ "conflicts": [
+ {
+ "type": "prefix",
+ "prefix": "foaf",
+ "ours": "http://xmlns.com/foaf/0.1/",
+ "theirs": "http://example.org/my-foaf#"
+ }
+ ]
+}
+```
+
+---
+
+## Implementation Tasks
+
+### 1. Detect Prefix Conflicts (90 minutes)
+
+Add to `MergeCommandHandler`:
+
+```java
+private List detectPrefixConflicts(
+ DatasetGraph base,
+ DatasetGraph ours,
+ DatasetGraph theirs) {
+
+ Map basePrefixes = extractPrefixes(base);
+ Map ourPrefixes = extractPrefixes(ours);
+ Map theirPrefixes = extractPrefixes(theirs);
+
+ List conflicts = new ArrayList<>();
+
+ for (String prefix : theirPrefixes.keySet()) {
+ String theirIRI = theirPrefixes.get(prefix);
+ String ourIRI = ourPrefixes.get(prefix);
+ String baseIRI = basePrefixes.get(prefix);
+
+ // Conflict: Both changed prefix to different values
+ if (ourIRI != null && !ourIRI.equals(theirIRI) && !ourIRI.equals(baseIRI)) {
+ conflicts.add(new PrefixConflict(prefix, ourIRI, theirIRI));
+ }
+ }
+
+ return conflicts;
+}
+```
+
+---
+
+### 2. Update MergeConflict DTO (20 minutes)
+
+```java
+public record MergeConflict(
+ ConflictType type,
+ String graph, // For quad conflicts
+ int conflictingQuads, // For quad conflicts
+ String prefix, // For prefix conflicts
+ String ours, // For prefix conflicts
+ String theirs // For prefix conflicts
+) {
+ public enum ConflictType {
+ QUAD,
+ PREFIX
+ }
+}
+```
+
+---
+
+### 3. Resolution Strategies (60 minutes)
+
+Support prefix-specific resolution:
+
+```json
+{
+ "sourceBranch": "dev",
+ "resolutions": {
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/" // Resolve to this IRI
+ }
+ }
+}
+```
+
+---
+
+### 4. Tests (60 minutes)
+
+```java
+@Test
+void merge_shouldDetectPrefixConflict() {
+ // Arrange: Create conflicting prefixes
+ definePrefixesOnBranch("main", Map.of("foaf", "http://xmlns.com/foaf/0.1/"));
+ definePrefixesOnBranch("dev", Map.of("foaf", "http://example.org/my-foaf#"));
+
+ // Act
+ ResponseEntity response = mergeBranch("dev", "main");
+
+ // Assert
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
+ assertThat(response.getBody()).contains("\"type\":\"prefix\"");
+ assertThat(response.getBody()).contains("\"prefix\":\"foaf\"");
+}
+
+@Test
+void merge_shouldResolvePrefixConflict_withOursStrategy() {
+ // Arrange: Conflict
+ createPrefixConflict();
+
+ // Act: Merge with resolution
+ MergeRequest request = new MergeRequest(
+ "dev",
+ "ours", // Keep our prefix
+ null
+ );
+ mergeBranch(request);
+
+ // Assert: Our prefix kept
+ assertThat(getPrefixes().prefixes().get("foaf"))
+ .isEqualTo("http://xmlns.com/foaf/0.1/");
+}
+```
+
+---
+
+## Files to Modify
+
+```
+src/main/java/org/chucc/vcserver/
+ ├── command/
+ │ └── MergeCommandHandler.java # Add prefix conflict detection
+ ├── dto/
+ │ └── MergeConflict.java # Add PREFIX type
+ └── util/
+ └── MergeUtil.java # Add prefix merge logic
+
+src/test/java/org/chucc/vcserver/integration/
+ └── MergeOperationsIT.java # Add prefix conflict tests
+```
+
+---
+
+## Success Criteria
+
+- ✅ Prefix conflicts detected and reported separately
+- ✅ Resolution strategies work for prefixes
+- ✅ Manual resolution supported
+- ✅ Tests pass (6+ new tests)
+
+---
+
+## Why Deferred?
+
+1. **Generic conflict handling works:** Current merge already detects conflicts (as quad-level)
+2. **Low priority:** Prefix conflicts are rare in practice
+3. **Complex implementation:** Requires changes to merge algorithm
+4. **Better DX:** Nice-to-have for better error messages, not critical
+
+**Recommendation:** Implement only if users report confusion with generic conflict messages.
+
+---
+
+**Status:** Optional enhancement - defer until after core PMP features deployed and user feedback collected.
diff --git a/docs/api/prefix-management.md b/docs/api/prefix-management.md
new file mode 100644
index 0000000..8e12330
--- /dev/null
+++ b/docs/api/prefix-management.md
@@ -0,0 +1,1068 @@
+# Prefix Management in CHUCC Server
+
+**Implementation Guide for [Prefix Management Protocol (PMP) v1.0](../../protocol/Prefix_Management_Protocol.md)**
+
+---
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [URL Structure](#url-structure)
+3. [Version Control Integration](#version-control-integration)
+4. [Operations](#operations)
+5. [Time-Travel Queries](#time-travel-queries)
+6. [Merge Behavior](#merge-behavior)
+7. [RDFPatch Integration](#rdfpatch-integration)
+8. [IDE Integration](#ide-integration)
+9. [Implementation Architecture](#implementation-architecture)
+10. [Examples](#examples)
+
+---
+
+## Overview
+
+CHUCC Server implements the [Prefix Management Protocol (PMP)](../../protocol/Prefix_Management_Protocol.md) with **version control integration**. Prefix changes are treated as first-class commits, enabling:
+
+✅ **Version history** - Track who changed prefixes and when
+✅ **Time-travel** - View prefixes at any commit
+✅ **Merge support** - Prefix changes merge automatically with branches
+✅ **Audit trail** - Full history via event sourcing
+
+**Key Principle:** Prefix modifications create commits (not in-place updates). This follows CHUCC's CQRS + Event Sourcing architecture.
+
+---
+
+## URL Structure
+
+### Base Protocol Compatibility
+
+CHUCC provides **both** generic and version-explicit endpoints:
+
+```
+# Generic endpoint (base protocol)
+/datasets/{dataset}/prefixes
+
+# Version-explicit endpoints (CHUCC-specific)
+/version/datasets/{dataset}/branches/{branch}/prefixes
+/version/datasets/{dataset}/commits/{commitId}/prefixes
+```
+
+### Generic Endpoint Behavior
+
+The generic endpoint operates on the **HEAD of the default branch** (`main`):
+
+```http
+GET /datasets/mydata/prefixes
+↓ (internally resolves to)
+GET /version/datasets/mydata/branches/main/prefixes
+```
+
+**Use case:** Generic SPARQL clients that don't need version control awareness.
+
+### Version-Explicit Endpoints (Recommended)
+
+For full version control capabilities, use explicit endpoints:
+
+```http
+# Read prefixes from specific branch
+GET /version/datasets/{dataset}/branches/{branch}/prefixes
+
+# Modify prefixes on specific branch (creates commit)
+PUT /version/datasets/{dataset}/branches/{branch}/prefixes
+PATCH /version/datasets/{dataset}/branches/{branch}/prefixes
+DELETE /version/datasets/{dataset}/branches/{branch}/prefixes?prefix=...
+
+# Time-travel: Read prefixes at specific commit
+GET /version/datasets/{dataset}/commits/{commitId}/prefixes
+```
+
+---
+
+## Version Control Integration
+
+### Prefix Changes Create Commits
+
+**Unlike non-versioned servers** (which modify prefixes in-place), CHUCC treats prefix changes as **commit-creating operations**.
+
+**Example:**
+```http
+PATCH /version/datasets/mydata/branches/main/prefixes
+Content-Type: application/json
+SPARQL-VC-Author: Alice
+
+{
+ "message": "Add geospatial prefixes",
+ "prefixes": {
+ "geo": "http://www.opengis.net/ont/geosparql#",
+ "sf": "http://www.opengis.net/ont/sf#"
+ }
+}
+```
+
+**Response:**
+```http
+201 Created
+Location: /version/datasets/mydata/commits/01JCDN4XYZ...
+ETag: "01JCDN4XYZ..."
+Content-Type: application/json
+
+{
+ "dataset": "mydata",
+ "branch": "main",
+ "commitId": "01JCDN4XYZ...",
+ "message": "Add geospatial prefixes",
+ "author": "Alice ",
+ "timestamp": "2025-11-06T10:30:00Z"
+}
+```
+
+**What happened internally:**
+1. Current prefixes retrieved from materialized branch
+2. RDFPatch generated with `PA` directives
+3. `CommitCreatedEvent` published to Kafka
+4. ReadModelProjector updates branch HEAD
+5. Response returned immediately (eventual consistency)
+
+### Required Headers
+
+All write operations **MUST** include:
+
+```http
+SPARQL-VC-Author: Alice
+```
+
+This is the commit author (same as other CHUCC version control operations).
+
+### Branch Protection
+
+Prefix modifications respect branch protection rules:
+
+```http
+PUT /version/datasets/mydata/branches/main/prefixes
+→ 403 Forbidden (if main is protected)
+
+{
+ "type": "/problems/protected-branch",
+ "title": "Protected Branch",
+ "detail": "Branch 'main' is protected. Create a feature branch instead."
+}
+```
+
+**Workaround:** Create feature branch, modify prefixes, merge via pull request.
+
+---
+
+## Operations
+
+### GET - Retrieve Prefix Map
+
+**Read current branch:**
+```http
+GET /version/datasets/mydata/branches/main/prefixes
+Accept: application/json
+```
+
+**Response:**
+```json
+{
+ "dataset": "mydata",
+ "branch": "main",
+ "commitId": "01JCDN3KXQ...",
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "foaf": "http://xmlns.com/foaf/0.1/",
+ "dct": "http://purl.org/dc/terms/"
+ }
+}
+```
+
+**Implementation:**
+```java
+// Read from materialized branch (LRU cache)
+DatasetGraph dsg = materializedBranchRepository
+ .getMaterializedBranch("mydata", "main");
+PrefixMapping pm = dsg.getDefaultGraph().getPrefixMapping();
+Map prefixes = pm.getNsPrefixMap();
+```
+
+---
+
+### PUT - Replace Entire Prefix Map
+
+**Replace all prefixes:**
+```http
+PUT /version/datasets/mydata/branches/main/prefixes
+Content-Type: application/json
+SPARQL-VC-Author: Alice
+
+{
+ "message": "Simplify prefix map",
+ "prefixes": {
+ "ex": "http://example.org/",
+ "schema": "http://schema.org/"
+ }
+}
+```
+
+**Generated RDFPatch:**
+```
+TX .
+# Delete old prefixes
+PD rdf: .
+PD rdfs: .
+PD foaf: .
+PD dct: .
+# Add new prefixes
+PA ex: .
+PA schema: .
+TC .
+```
+
+**Response:**
+```http
+201 Created
+Location: /version/datasets/mydata/commits/01JCDN5ABC...
+ETag: "01JCDN5ABC..."
+
+{
+ "commitId": "01JCDN5ABC...",
+ "message": "Simplify prefix map"
+}
+```
+
+---
+
+### PATCH - Add/Update Selected Prefixes
+
+**Merge-update (non-destructive):**
+```http
+PATCH /version/datasets/mydata/branches/main/prefixes
+Content-Type: application/json
+SPARQL-VC-Author: Bob
+
+{
+ "message": "Add Dublin Core terms",
+ "prefixes": {
+ "dct": "http://purl.org/dc/terms/",
+ "dcmi": "http://purl.org/dc/dcmitype/"
+ }
+}
+```
+
+**Generated RDFPatch:**
+```
+TX .
+PA dct: .
+PA dcmi: .
+# Existing prefixes (ex, schema) unchanged
+TC .
+```
+
+**Use case:** Add new prefixes without affecting existing ones.
+
+---
+
+### DELETE - Remove Prefixes
+
+**Remove specific prefixes:**
+```http
+DELETE /version/datasets/mydata/branches/main/prefixes?prefix=temp&prefix=test
+SPARQL-VC-Author: Alice
+```
+
+**Query parameters:**
+- `prefix=temp` - Remove prefix `temp`
+- `prefix=test` - Remove prefix `test`
+- `message=Cleanup+temporary+prefixes` - Optional commit message (URL-encoded)
+
+**Generated RDFPatch:**
+```
+TX .
+PD temp: .
+PD test: .
+TC .
+```
+
+**Response:**
+```http
+201 Created
+Location: /version/datasets/mydata/commits/01JCDN6DEF...
+
+{
+ "commitId": "01JCDN6DEF...",
+ "message": "Cleanup temporary prefixes"
+}
+```
+
+---
+
+## Time-Travel Queries
+
+### View Prefixes at Specific Commit
+
+```http
+GET /version/datasets/mydata/commits/01JCDN2XYZ.../prefixes
+```
+
+**Response:**
+```json
+{
+ "dataset": "mydata",
+ "commitId": "01JCDN2XYZ...",
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+**Implementation:**
+```java
+// Rebuild dataset at specific commit
+DatasetGraph dsg = materializedViewRebuildService
+ .rebuildAtCommit("mydata", "01JCDN2XYZ...");
+PrefixMapping pm = dsg.getDefaultGraph().getPrefixMapping();
+```
+
+**Performance:**
+- If commit is cached → instant (materialized view already built)
+- If not cached → ~1s rebuild time (typical for 100 commits)
+
+### Compare Prefixes Between Commits
+
+```bash
+# Use standard diff endpoint with ?prefixes-only flag
+GET /version/datasets/mydata/commits/01JCDN2.../diff/01JCDN5...?prefixes-only=true
+```
+
+**Response:**
+```json
+{
+ "added": {
+ "geo": "http://www.opengis.net/ont/geosparql#"
+ },
+ "removed": {
+ "temp": "http://example.org/temp/"
+ },
+ "modified": {
+ "foaf": {
+ "old": "http://xmlns.com/foaf/0.1/",
+ "new": "http://example.org/my-foaf#"
+ }
+ }
+}
+```
+
+**Note:** This endpoint is planned but not yet implemented. See `.tasks/pmp/05-diff-support.md`.
+
+---
+
+## Merge Behavior
+
+### Automatic Merge
+
+Prefix changes **merge automatically** via RDFPatch replay:
+
+**Scenario:**
+```
+main: PA rdf: <...> . PA foaf: <...> .
+dev: PA geo: <...> . PA dct: <...> .
+
+Merge dev → main
+Result: PA rdf: <...> . PA foaf: <...> . PA geo: <...> . PA dct: <...> .
+```
+
+**No conflicts** - Prefixes are additive.
+
+### Prefix Conflicts
+
+**Conflict occurs when same prefix has different IRIs:**
+
+```
+main: PA foaf: .
+dev: PA foaf: .
+
+Merge dev → main → CONFLICT!
+```
+
+**Merge response:**
+```http
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "dev",
+ "message": "Merge dev into main"
+}
+
+→ 409 Conflict
+
+{
+ "type": "/problems/merge-conflict",
+ "title": "Merge Conflict",
+ "conflicts": [
+ {
+ "type": "prefix",
+ "prefix": "foaf",
+ "ours": "http://xmlns.com/foaf/0.1/",
+ "theirs": "http://example.org/my-foaf#"
+ }
+ ]
+}
+```
+
+### Conflict Resolution
+
+**Option 1: Choose resolution strategy**
+```http
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "dev",
+ "resolutionStrategy": "ours"
+}
+
+→ 201 Created (keeps main's foaf prefix)
+```
+
+**Option 2: Manual resolution**
+```http
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "dev",
+ "resolutions": {
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+ }
+}
+```
+
+**Option 3: Resolve via separate commit**
+```http
+# After failed merge, fix prefixes manually
+PATCH /version/datasets/mydata/branches/main/prefixes
+{
+ "message": "Resolve foaf prefix conflict",
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+
+# Then retry merge
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "dev"
+}
+```
+
+---
+
+## RDFPatch Integration
+
+### PA/PD Directives
+
+CHUCC uses [RDFPatch](https://afs.github.io/rdf-patch/) to represent all changes, including prefix modifications:
+
+**Directives:**
+- `PA prefix: ` - Add prefix (or update if exists)
+- `PD prefix:` - Delete prefix
+
+### Example Commit
+
+**Commit metadata:**
+```json
+{
+ "commitId": "01JCDN4XYZ...",
+ "message": "Add FOAF prefix",
+ "author": "Alice ",
+ "timestamp": "2025-11-06T10:30:00Z",
+ "patch": "TX .\nPA foaf: .\nTC ."
+}
+```
+
+**RDFPatch contents:**
+```
+TX .
+PA foaf: .
+TC .
+```
+
+### Mixed Operations
+
+Prefixes can be modified **together with RDF data** in a single commit:
+
+```
+TX .
+# Add prefix
+PA ex: .
+# Add triples using that prefix
+A "Alice" .
+TC .
+```
+
+**Use case:** Import RDF/XML with namespace declarations → single commit contains both prefixes and data.
+
+---
+
+## IDE Integration
+
+### Use Case: SPARQL Query Editor
+
+**Workflow:**
+1. User opens SPARQL editor
+2. Editor fetches prefixes from CHUCC
+3. Editor inserts `PREFIX` declarations into query template
+
+**JavaScript example:**
+```javascript
+async function loadPrefixesIntoEditor(dataset, branch) {
+ const response = await fetch(
+ `http://chucc-server/version/datasets/${dataset}/branches/${branch}/prefixes`,
+ { headers: { 'Accept': 'application/json' } }
+ );
+
+ const data = await response.json();
+
+ // Generate PREFIX block
+ const prefixBlock = Object.entries(data.prefixes)
+ .map(([prefix, iri]) => `PREFIX ${prefix}: <${iri}>`)
+ .join('\n');
+
+ // Insert into editor
+ editor.setValue(`${prefixBlock}\n\nSELECT * WHERE {\n ?s ?p ?o .\n}\nLIMIT 10`);
+}
+```
+
+### Use Case: RDF/XML Import with Prefix Suggestion
+
+**Workflow:**
+1. User uploads RDF/XML file
+2. CHUCC extracts namespace declarations
+3. IDE suggests adding prefixes via PATCH
+
+**Example:**
+```xml
+
+
+
+
+```
+
+**Suggested action:**
+```http
+PATCH /version/datasets/mydata/branches/main/prefixes
+{
+ "message": "Add prefixes from imported RDF/XML",
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/",
+ "schema": "http://schema.org/"
+ }
+}
+```
+
+**VSCode Extension Example:**
+```typescript
+// Show quickfix: "Add discovered prefixes to dataset?"
+const action = await vscode.window.showInformationMessage(
+ 'Found 2 new namespace prefixes in RDF/XML',
+ 'Add to Dataset',
+ 'Ignore'
+);
+
+if (action === 'Add to Dataset') {
+ await fetch(`${chucc}/version/datasets/${dataset}/branches/${branch}/prefixes`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'SPARQL-VC-Author': getUserEmail()
+ },
+ body: JSON.stringify({
+ message: 'Add prefixes from imported RDF/XML',
+ prefixes: discoveredPrefixes
+ })
+ });
+}
+```
+
+---
+
+## Implementation Architecture
+
+### Command Handler
+
+**`UpdatePrefixesCommandHandler`** (to be implemented):
+
+```java
+@Component
+public class UpdatePrefixesCommandHandler {
+
+ private final MaterializedBranchRepository materializedBranchRepository;
+ private final CreateCommitCommandHandler createCommitCommandHandler;
+
+ public CommitMetadata handle(UpdatePrefixesCommand cmd) {
+ // 1. Get current prefixes from materialized branch
+ DatasetGraph currentDsg = materializedBranchRepository
+ .getMaterializedBranch(cmd.dataset(), cmd.branch());
+ Map oldPrefixes = currentDsg.getDefaultGraph()
+ .getPrefixMapping().getNsPrefixMap();
+
+ // 2. Generate RDFPatch with PA/PD directives
+ RDFPatch patch = buildPrefixPatch(
+ oldPrefixes,
+ cmd.newPrefixes(),
+ cmd.operation()
+ );
+
+ // 3. Create commit via existing handler
+ CreateCommitCommand commitCmd = new CreateCommitCommand(
+ cmd.dataset(),
+ cmd.branch(),
+ cmd.message().orElse(generateDefaultMessage(cmd)),
+ cmd.author(),
+ patch
+ );
+
+ return createCommitCommandHandler.handle(commitCmd);
+ }
+
+ private RDFPatch buildPrefixPatch(
+ Map oldPrefixes,
+ Map newPrefixes,
+ Operation operation) {
+
+ RDFPatchBuilder builder = RDFPatchBuilder.create();
+ builder.txnBegin();
+
+ switch (operation) {
+ case PUT -> {
+ // Remove all old prefixes
+ oldPrefixes.forEach((prefix, iri) -> builder.prefixDelete(prefix));
+ // Add all new prefixes
+ newPrefixes.forEach((prefix, iri) -> builder.prefixAdd(prefix, iri));
+ }
+ case PATCH -> {
+ // Add/update only specified prefixes
+ newPrefixes.forEach((prefix, iri) -> builder.prefixAdd(prefix, iri));
+ }
+ case DELETE -> {
+ // Remove specified prefixes
+ newPrefixes.keySet().forEach(builder::prefixDelete);
+ }
+ }
+
+ builder.txnCommit();
+ return builder.build();
+ }
+}
+```
+
+**Key insight:** Reuses existing `CreateCommitCommandHandler` - no new event types needed!
+
+### REST Controller
+
+**`PrefixManagementController`** (to be implemented):
+
+```java
+@RestController
+@RequestMapping("/version/datasets/{dataset}")
+public class PrefixManagementController {
+
+ @GetMapping("/branches/{branch}/prefixes")
+ public ResponseEntity getCurrentPrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch) {
+
+ DatasetGraph dsg = materializedBranchRepository
+ .getMaterializedBranch(dataset, branch);
+
+ Branch branchObj = branchRepository
+ .findByDatasetAndName(dataset, branch)
+ .orElseThrow(() -> new BranchNotFoundException(dataset, branch));
+
+ Map prefixes = dsg.getDefaultGraph()
+ .getPrefixMapping()
+ .getNsPrefixMap();
+
+ return ResponseEntity.ok(new PrefixResponse(
+ dataset,
+ branch,
+ branchObj.headCommitId(),
+ prefixes
+ ));
+ }
+
+ @GetMapping("/commits/{commitId}/prefixes")
+ public ResponseEntity getPrefixesAtCommit(
+ @PathVariable String dataset,
+ @PathVariable String commitId) {
+
+ DatasetGraph dsg = materializedViewRebuildService
+ .rebuildAtCommit(dataset, commitId);
+
+ Map prefixes = dsg.getDefaultGraph()
+ .getPrefixMapping()
+ .getNsPrefixMap();
+
+ return ResponseEntity.ok(new PrefixResponse(
+ dataset,
+ null, // No branch (time-travel query)
+ commitId,
+ prefixes
+ ));
+ }
+
+ @PutMapping("/branches/{branch}/prefixes")
+ public ResponseEntity replacePrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch,
+ @RequestHeader("SPARQL-VC-Author") String author,
+ @RequestBody UpdatePrefixesRequest request) {
+
+ UpdatePrefixesCommand cmd = new UpdatePrefixesCommand(
+ dataset,
+ branch,
+ author,
+ request.prefixes(),
+ Operation.PUT,
+ Optional.ofNullable(request.message())
+ );
+
+ CommitMetadata commit = updatePrefixesCommandHandler.handle(cmd);
+
+ URI location = URI.create(
+ "/version/datasets/" + dataset + "/commits/" + commit.id()
+ );
+
+ return ResponseEntity
+ .created(location)
+ .eTag(commit.id())
+ .body(new CommitResponse(dataset, branch, commit));
+ }
+
+ @PatchMapping("/branches/{branch}/prefixes")
+ public ResponseEntity updatePrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch,
+ @RequestHeader("SPARQL-VC-Author") String author,
+ @RequestBody UpdatePrefixesRequest request) {
+
+ UpdatePrefixesCommand cmd = new UpdatePrefixesCommand(
+ dataset,
+ branch,
+ author,
+ request.prefixes(),
+ Operation.PATCH,
+ Optional.ofNullable(request.message())
+ );
+
+ CommitMetadata commit = updatePrefixesCommandHandler.handle(cmd);
+
+ return ResponseEntity
+ .created(URI.create("/version/datasets/" + dataset + "/commits/" + commit.id()))
+ .eTag(commit.id())
+ .body(new CommitResponse(dataset, branch, commit));
+ }
+
+ @DeleteMapping("/branches/{branch}/prefixes")
+ public ResponseEntity deletePrefixes(
+ @PathVariable String dataset,
+ @PathVariable String branch,
+ @RequestParam List prefix,
+ @RequestParam(required = false) String message,
+ @RequestHeader("SPARQL-VC-Author") String author) {
+
+ // Convert prefix list to map (value doesn't matter for DELETE)
+ Map prefixesToDelete = prefix.stream()
+ .collect(Collectors.toMap(p -> p, p -> ""));
+
+ UpdatePrefixesCommand cmd = new UpdatePrefixesCommand(
+ dataset,
+ branch,
+ author,
+ prefixesToDelete,
+ Operation.DELETE,
+ Optional.ofNullable(message)
+ );
+
+ CommitMetadata commit = updatePrefixesCommandHandler.handle(cmd);
+
+ return ResponseEntity
+ .created(URI.create("/version/datasets/" + dataset + "/commits/" + commit.id()))
+ .eTag(commit.id())
+ .body(new CommitResponse(dataset, branch, commit));
+ }
+}
+```
+
+### Event Flow (Reuses Existing Architecture)
+
+```
+HTTP Request (PUT /prefixes)
+ ↓
+PrefixManagementController
+ ↓
+UpdatePrefixesCommandHandler
+ ↓ (generates RDFPatch with PA/PD)
+CreateCommitCommandHandler
+ ↓
+CommitCreatedEvent published to Kafka
+ ↓
+ReadModelProjector consumes event
+ ↓
+Updates materialized branch (applies PA/PD)
+ ↓
+Prefix map updated in memory
+```
+
+**No new events, projectors, or repositories needed!**
+
+---
+
+## Examples
+
+### Example 1: Complete Workflow
+
+**Step 1: Create dataset and check default prefixes**
+```http
+POST /version/datasets/mydata
+SPARQL-VC-Author: Alice
+
+→ 202 Accepted
+
+GET /version/datasets/mydata/branches/main/prefixes
+
+→ 200 OK
+{
+ "prefixes": {}
+}
+```
+
+**Step 2: Add common prefixes**
+```http
+PATCH /version/datasets/mydata/branches/main/prefixes
+SPARQL-VC-Author: Alice
+
+{
+ "message": "Add common prefixes",
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+
+→ 201 Created
+{
+ "commitId": "01JCDN7GHI...",
+ "message": "Add common prefixes"
+}
+```
+
+**Step 3: Import RDF data**
+```http
+PUT /version/datasets/mydata/branches/main/data?default
+Content-Type: application/rdf+xml
+SPARQL-VC-Author: Alice
+
+
+
+ Alice
+
+
+
+→ 201 Created
+```
+
+**Step 4: Query with prefixes**
+```sparql
+PREFIX foaf:
+
+SELECT ?name WHERE {
+ ?person foaf:name ?name .
+}
+```
+
+**Step 5: View commit history (includes prefix changes)**
+```http
+GET /version/datasets/mydata/branches/main/commits
+
+→ 200 OK
+[
+ {
+ "commitId": "01JCDN8JKL...",
+ "message": "Add RDF data",
+ "author": "Alice "
+ },
+ {
+ "commitId": "01JCDN7GHI...",
+ "message": "Add common prefixes",
+ "author": "Alice "
+ },
+ {
+ "commitId": "01JCDN6ABC...",
+ "message": "Initial commit",
+ "author": "System"
+ }
+]
+```
+
+---
+
+### Example 2: Branch Workflow
+
+**Create feature branch and add ontology-specific prefixes**
+```http
+POST /version/datasets/mydata/branches
+{
+ "branchName": "add-geospatial",
+ "sourceBranch": "main"
+}
+
+→ 201 Created
+
+PATCH /version/datasets/mydata/branches/add-geospatial/prefixes
+SPARQL-VC-Author: Bob
+
+{
+ "message": "Add geospatial prefixes",
+ "prefixes": {
+ "geo": "http://www.opengis.net/ont/geosparql#",
+ "sf": "http://www.opengis.net/ont/sf#",
+ "geof": "http://www.opengis.net/def/function/geosparql/"
+ }
+}
+
+→ 201 Created
+
+# Merge back to main
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "add-geospatial",
+ "message": "Merge geospatial prefixes"
+}
+
+→ 201 Created (prefixes automatically merged)
+```
+
+---
+
+### Example 3: Conflict Resolution
+
+**Create conflicting prefix definitions**
+```http
+# On main: Define foaf
+PATCH /version/datasets/mydata/branches/main/prefixes
+{
+ "prefixes": { "foaf": "http://xmlns.com/foaf/0.1/" }
+}
+
+# On dev: Define foaf differently
+POST /version/datasets/mydata/branches
+{ "branchName": "dev", "sourceBranch": "main" }
+
+PATCH /version/datasets/mydata/branches/dev/prefixes
+{
+ "prefixes": { "foaf": "http://example.org/my-foaf#" }
+}
+
+# Try to merge
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "dev"
+}
+
+→ 409 Conflict
+{
+ "conflicts": [
+ {
+ "type": "prefix",
+ "prefix": "foaf",
+ "ours": "http://xmlns.com/foaf/0.1/",
+ "theirs": "http://example.org/my-foaf#"
+ }
+ ]
+}
+
+# Resolve: Keep main's version
+POST /version/datasets/mydata/branches/main/merge
+{
+ "sourceBranch": "dev",
+ "resolutionStrategy": "ours"
+}
+
+→ 201 Created
+```
+
+---
+
+## Performance Considerations
+
+### Caching
+
+Prefix maps are **cached as part of materialized branches**:
+- LRU eviction (default: 100 branches)
+- Rebuilt on-demand if evicted (~1s typical)
+- No separate prefix cache needed
+
+### Optimization: Prefix-Only Queries
+
+For extremely large datasets, reading prefixes is **very fast** (no triple scanning):
+
+```java
+// O(1) - Just read prefix mapping
+PrefixMapping pm = dsg.getDefaultGraph().getPrefixMapping();
+Map prefixes = pm.getNsPrefixMap();
+```
+
+**Performance:** <1ms (in-memory hash map lookup)
+
+---
+
+## Security Considerations
+
+### Authorization
+
+Prefix modifications require same permissions as graph modifications:
+- Read permissions: Can GET prefixes
+- Write permissions: Can PUT/PATCH/DELETE prefixes
+
+### Audit Trail
+
+All prefix changes are **auditable**:
+- Stored in Kafka (permanent log)
+- Commit metadata includes author and timestamp
+- Can query: "Who changed the foaf prefix and when?"
+
+---
+
+## Future Enhancements
+
+Planned features (see `.tasks/pmp/` for details):
+
+1. **Prefix suggestions** - Analyze dataset and suggest conventional prefixes
+2. **Bulk operations** - Import/export entire prefix catalogs
+3. **Prefix diff** - Compare prefix maps between commits
+4. **Prefix templates** - Reusable prefix sets across datasets
+5. **Conflict prevention** - Warn before creating conflicting prefix
+
+---
+
+## Related Documentation
+
+- [Prefix Management Protocol (PMP) v1.0](../../protocol/Prefix_Management_Protocol.md) - Generic protocol specification
+- [CQRS + Event Sourcing Architecture](../architecture/cqrs-event-sourcing.md) - Why prefixes create commits
+- [RDFPatch Specification](https://afs.github.io/rdf-patch/) - PA/PD directive details
+- [Version Control API](./version-control.md) - Branch, commit, merge operations
+
+---
+
+## Support
+
+For implementation questions, see `.tasks/pmp/README.md` or contact the development team.
+
+---
+
+**End of CHUCC Prefix Management Implementation Guide**
diff --git a/protocol/Prefix_Management_Protocol.md b/protocol/Prefix_Management_Protocol.md
new file mode 100644
index 0000000..2656452
--- /dev/null
+++ b/protocol/Prefix_Management_Protocol.md
@@ -0,0 +1,479 @@
+# Prefix Management Protocol (PMP) for SPARQL Services
+
+**Version:** 1.0
+**Status:** Draft
+**Date:** 2025-11-06
+
+---
+
+## 1. Introduction
+
+This document defines an HTTP-based protocol for managing prefix declarations associated with a SPARQL dataset.
+
+SPARQL treats `PREFIX` declarations as query-local syntax and does not define a means to retrieve or modify server-side prefix mappings. Many SPARQL servers (e.g., Apache Jena Fuseki, Virtuoso) maintain prefix maps that are used when serializing RDF data (e.g., Turtle, JSON-LD). This protocol exposes those mappings in a simple, REST-style way so that SPARQL clients, UI editors, and administrative tools can synchronize their prefix lists with the server.
+
+**Design Goals:**
+- Simple REST semantics (GET, PUT, PATCH, DELETE)
+- Implementation-agnostic (works for any SPARQL server)
+- Integrates naturally with RDFPatch for transactional consistency
+- Supports both versioned and non-versioned implementations
+
+**Scope:** This protocol manages **dataset-level prefix maps only**. Graph-level prefix maps are not supported due to RDFPatch constraints.
+
+---
+
+## 2. Terminology
+
+- **Prefix map**: An ordered mapping from a short prefix name (e.g., `rdf`) to an absolute IRI (e.g., `http://www.w3.org/1999/02/22-rdf-syntax-ns#`)
+- **Dataset-level prefix map**: The prefix map associated with a SPARQL dataset as a whole
+- **Service endpoint**: Base URL where this protocol is available (deployment-specific)
+
+---
+
+## 3. Service Endpoint
+
+The Prefix Management Protocol is exposed at:
+
+```
+{dataset-base}/prefixes
+```
+
+**Example:**
+```
+https://example.org/sparql/myDataset/prefixes
+```
+
+The exact URL structure is deployment-specific.
+
+---
+
+## 4. Representation Format
+
+### 4.1 Media Type
+
+Servers **MUST** support `application/json`.
+
+### 4.2 JSON Structure
+
+**Request body (PUT/PATCH):**
+```json
+{
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+**Optional: Include commit message (for version-controlled servers):**
+```json
+{
+ "message": "Add FOAF ontology prefixes",
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+**Response body (GET):**
+```json
+{
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#"
+ }
+}
+```
+
+---
+
+## 5. Operations
+
+### 5.1 GET - Retrieve Prefix Map
+
+**Request:**
+```http
+GET /prefixes HTTP/1.1
+Accept: application/json
+```
+
+**Response:**
+- `200 OK` - Returns current prefix map as JSON
+- `404 Not Found` - Dataset does not exist
+
+**Example:**
+```http
+GET /myDataset/prefixes
+
+200 OK
+Content-Type: application/json
+
+{
+ "prefixes": {
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+---
+
+### 5.2 PUT - Replace Entire Prefix Map
+
+**Semantics:** Replace the entire prefix map with the provided mappings. Any existing prefix not included in the request body is removed.
+
+**Request:**
+```http
+PUT /prefixes HTTP/1.1
+Content-Type: application/json
+
+{
+ "prefixes": {
+ "ex": "http://example.org/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#"
+ }
+}
+```
+
+**Response:**
+- `204 No Content` - Prefix map updated in-place (non-versioned server)
+- `201 Created` - Commit created (version-controlled server)
+- `400 Bad Request` - Invalid prefix name or IRI
+- `404 Not Found` - Dataset does not exist
+
+**Version-controlled server response:**
+```http
+201 Created
+Location: /version/datasets/myDataset/commits/01JCDN...
+ETag: "01JCDN..."
+Content-Type: application/json
+
+{
+ "commitId": "01JCDN...",
+ "message": "Replace prefix map"
+}
+```
+
+---
+
+### 5.3 PATCH - Add or Update Selected Prefixes
+
+**Semantics:** Add new prefixes or update existing ones. Prefixes not mentioned in the request remain unchanged.
+
+**Request:**
+```http
+PATCH /prefixes HTTP/1.1
+Content-Type: application/json
+
+{
+ "prefixes": {
+ "geo": "http://www.opengis.net/ont/geosparql#",
+ "dct": "http://purl.org/dc/terms/"
+ }
+}
+```
+
+**Response:**
+- `204 No Content` - Prefixes updated in-place (non-versioned server)
+- `201 Created` - Commit created (version-controlled server)
+- `400 Bad Request` - Invalid prefix name or IRI
+- `404 Not Found` - Dataset does not exist
+
+---
+
+### 5.4 DELETE - Remove Prefixes
+
+**Semantics:** Remove one or more prefixes from the prefix map.
+
+**Request:**
+```http
+DELETE /prefixes?prefix=ex&prefix=temp HTTP/1.1
+```
+
+**Query parameters:**
+- `prefix` - Name of prefix to remove (can be repeated for multiple prefixes)
+
+**Response:**
+- `204 No Content` - Prefixes removed in-place (non-versioned server)
+- `201 Created` - Commit created (version-controlled server)
+- `404 Not Found` - Dataset does not exist
+
+**Note:** If a specified prefix does not exist, servers SHOULD ignore it silently (idempotent operation).
+
+---
+
+## 6. Prefix Name and IRI Constraints
+
+### 6.1 Prefix Name Validation
+
+Prefix names **MUST** conform to SPARQL `PNAME_NS` rules (the part before the colon in `PREFIX rdf:`).
+
+**Valid examples:** `rdf`, `foaf`, `ex`, `my-ontology`
+**Invalid examples:** `1foo`, `rdf:`, `http://example.org/`
+
+### 6.2 IRI Validation
+
+IRIs **MUST** be absolute IRIs (not relative).
+
+**Valid:** `http://example.org/`, `https://xmlns.com/foaf/0.1/`
+**Invalid:** `../relative`, `example.org` (missing scheme)
+
+### 6.3 Error Response
+
+If validation fails, return:
+
+```http
+400 Bad Request
+Content-Type: application/json
+
+{
+ "error": "InvalidPrefixName",
+ "message": "Prefix name '1foo' does not match SPARQL PNAME_NS rules"
+}
+```
+
+---
+
+## 7. RDFPatch Integration
+
+### 7.1 Background
+
+[RDFPatch](https://afs.github.io/rdf-patch/) is a standardized format for representing changes to RDF datasets. It includes directives for prefix management:
+- `PA prefix: ` - Add prefix
+- `PD prefix:` - Delete prefix
+
+### 7.2 Implementation Guidance
+
+Servers that use RDFPatch **SHOULD** translate prefix operations to PA/PD directives within transactions.
+
+**Example: PUT operation**
+
+Request:
+```json
+{
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+Translates to RDFPatch:
+```
+TX .
+PD rdf: .
+PD rdfs: .
+PA foaf: .
+TC .
+```
+
+**Benefits:**
+- Transactional consistency (all-or-nothing)
+- Atomic with RDF data changes
+- Automatic versioning (if server supports version control)
+- Merge/replay capabilities
+
+---
+
+## 8. Version-Controlled Implementations
+
+### 8.1 Behavior
+
+Servers with version control (branches, commits, etc.) **SHOULD**:
+- Treat prefix modifications as commit-creating operations
+- Return `201 Created` (instead of `204 No Content`)
+- Include `Location` header pointing to created commit
+- Include commit metadata in response body
+
+### 8.2 Response Format
+
+```http
+201 Created
+Location: /version/datasets/myDataset/commits/01JCDN...
+ETag: "01JCDN..."
+Content-Type: application/json
+
+{
+ "commitId": "01JCDN...",
+ "message": "Add geospatial prefixes",
+ "author": "Alice ",
+ "timestamp": "2025-11-06T10:30:00Z"
+}
+```
+
+### 8.3 Commit Messages
+
+Version-controlled servers **MAY** accept an optional `message` field:
+
+```json
+{
+ "message": "Add FOAF ontology prefixes",
+ "prefixes": {
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ }
+}
+```
+
+If not provided, servers should generate a default message (e.g., "Update prefixes").
+
+### 8.4 Time-Travel and History
+
+Version-controlled servers **MAY** expose additional endpoints for:
+- Retrieving prefixes at specific commits
+- Viewing prefix change history
+- Comparing prefix maps between branches
+
+These capabilities are **implementation-specific** and not defined in this protocol.
+
+---
+
+## 9. Error Conditions
+
+| Status Code | Meaning |
+|-------------|---------|
+| `200 OK` | GET successful |
+| `201 Created` | Modification created commit (versioned) |
+| `204 No Content` | Modification applied in-place (non-versioned) |
+| `400 Bad Request` | Invalid prefix name, IRI, or malformed JSON |
+| `404 Not Found` | Dataset or graph does not exist |
+| `415 Unsupported Media Type` | Client sent non-JSON content |
+| `500 Internal Server Error` | Server-side storage failure |
+
+---
+
+## 10. Security Considerations
+
+### 10.1 Authentication
+
+Servers **SHOULD** require authentication for write operations (PUT, PATCH, DELETE).
+
+### 10.2 Authorization
+
+Servers **SHOULD** enforce authorization policies:
+- Who can read prefix maps?
+- Who can modify prefix maps?
+- Are certain prefixes protected from deletion?
+
+### 10.3 Injection Attacks
+
+Servers **MUST** validate:
+- Prefix names conform to PNAME_NS rules
+- IRIs are well-formed absolute IRIs
+- JSON structure is valid
+
+Reject malicious input before processing.
+
+---
+
+## 11. Interaction with SPARQL and GSP
+
+### 11.1 Query Behavior
+
+This protocol **does NOT change** how SPARQL queries are parsed or executed. A query without `PREFIX` declarations still requires full IRIs.
+
+**Server behavior is implementation-specific:**
+- Some servers may inject prefixes into queries automatically
+- Some servers only use prefixes for serialization
+- Clients should not assume automatic prefix injection
+
+### 11.2 Serialization
+
+Servers **MAY** use the prefix map when serializing RDF data (e.g., Turtle responses from Graph Store Protocol). This improves readability but is not required.
+
+### 11.3 Coexistence
+
+This protocol can coexist with SPARQL Protocol and Graph Store Protocol on the same dataset:
+- SPARQL Protocol: `/sparql`
+- Graph Store Protocol: `/data`
+- Prefix Management: `/prefixes`
+
+---
+
+## 12. Conformance Levels
+
+### Level 1: Read-Only (Minimal)
+
+- **MUST** support `GET /prefixes`
+- **MUST** return JSON with `prefixes` object
+- **MUST** validate prefix names and IRIs
+
+### Level 2: Read/Write (Full)
+
+- All of Level 1
+- **MUST** support `PUT /prefixes`
+- **MUST** support `PATCH /prefixes`
+- **SHOULD** support `DELETE /prefixes?prefix=...`
+- **SHOULD** support RDFPatch integration (PA/PD directives)
+
+---
+
+## 13. Examples
+
+### Example 1: Non-Versioned Server (Fuseki-like)
+
+```http
+GET /myDataset/prefixes
+→ 200 OK
+ { "prefixes": { "rdf": "..." } }
+
+PUT /myDataset/prefixes
+ { "prefixes": { "foaf": "..." } }
+→ 204 No Content
+```
+
+### Example 2: Version-Controlled Server (CHUCC-like)
+
+```http
+GET /version/datasets/mydata/branches/main/prefixes
+→ 200 OK
+ { "prefixes": { "rdf": "..." } }
+
+PUT /version/datasets/mydata/branches/main/prefixes
+ { "message": "Add FOAF", "prefixes": { "foaf": "..." } }
+→ 201 Created
+ Location: /version/datasets/mydata/commits/01JCDN...
+ { "commitId": "01JCDN...", "message": "Add FOAF" }
+```
+
+---
+
+## 14. References
+
+- [SPARQL 1.1 Query Language](https://www.w3.org/TR/sparql11-query/)
+- [SPARQL 1.1 Graph Store HTTP Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/)
+- [RDFPatch](https://afs.github.io/rdf-patch/)
+- [RFC 7231: HTTP/1.1 Semantics and Content](https://tools.ietf.org/html/rfc7231)
+- [RFC 3987: Internationalized Resource Identifiers (IRIs)](https://tools.ietf.org/html/rfc3987)
+
+---
+
+## Appendix A: Comparison with Original Proposal
+
+This protocol simplifies the original [non-standard draft](./Prefix_Management_Protocol_for_SPARQL_Services.md) by:
+
+1. **Removed graph-level support** - Dataset-level only (RDFPatch constraint)
+2. **Removed capabilities endpoint** - Clients can probe with OPTIONS or try operations
+3. **Removed custom media type** - Standard `application/json` sufficient
+4. **Removed scope field** - Always dataset-level, no ambiguity
+5. **Removed ETag concurrency** - Optional for implementations, not core protocol
+6. **Simplified error format** - Implementation-specific (use RFC 7807 recommended)
+7. **Added RDFPatch integration** - Natural fit for transactional servers
+8. **Added version control notes** - Guidance for versioned implementations
+
+**Result:** 70% shorter protocol with broader applicability.
+
+---
+
+## Appendix B: Future Extensions
+
+Potential future extensions (not part of v1.0):
+
+- **Prefix suggestions** - Analyze dataset and suggest common prefixes
+- **Bulk operations** - Upload/download entire prefix catalogs
+- **Prefix validation** - Check for conflicts or duplicate namespaces
+- **Prefix templates** - Share prefix sets across datasets
+- **Federation** - Discover prefixes from federated endpoints
+
+---
+
+**End of Prefix Management Protocol v1.0**