Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6d90825
feat(workspace): add explicit catalog practice adoption
FelixTJDietrich Aug 19, 2026
a4bdac2
test(server): register catalog adoption controller
FelixTJDietrich Aug 19, 2026
f4c23af
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 19, 2026
38f8b42
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 19, 2026
e850b78
fix(server): stage practice runner timing policy
FelixTJDietrich Aug 19, 2026
25ba521
fix(workspace): harden catalog adoption invariants
FelixTJDietrich Aug 19, 2026
253cb03
docs(workspace): clarify catalog adoption lifecycle
FelixTJDietrich Aug 19, 2026
089bf6d
test(webapp): strengthen catalog adoption stories
FelixTJDietrich Aug 19, 2026
5697364
refactor(webapp): align catalog adoption with practice UI
FelixTJDietrich Aug 20, 2026
0a41a98
feat(workspace): unify practice setup and library adoption
FelixTJDietrich Aug 20, 2026
b147b85
Merge branch 'main' into 1362-production-readiness-research
FelixTJDietrich Aug 20, 2026
11bf52b
feat(webapp): open practice adoption in stacked detail drawers
FelixTJDietrich Aug 21, 2026
f309cc8
refactor(webapp): compose practice adoption from shared design-system…
FelixTJDietrich Aug 21, 2026
8164917
feat(webapp): read a practice without opening its edit form
FelixTJDietrich Aug 21, 2026
2243f02
feat(webapp): dismiss a detail drawer by sliding it out
FelixTJDietrich Aug 21, 2026
2070ffd
refactor(webapp): compose practice admin from repo primitives
FelixTJDietrich Aug 21, 2026
83f112b
refactor(webapp): nest drawer levels so the stack needs no reveal delay
FelixTJDietrich Aug 22, 2026
2f5e2e9
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 22, 2026
1799e25
docs(release): say what changes for a workspace, not only what was built
FelixTJDietrich Aug 22, 2026
ff08c92
docs: describe catalog installation as the code actually behaves
FelixTJDietrich Aug 22, 2026
098022a
feat(webapp): give a workspace area its icon, and render criteria as …
FelixTJDietrich Aug 22, 2026
8c7102c
feat(workspace): lead adoption with the habit, not the review rule
FelixTJDietrich Aug 22, 2026
6e6c0ad
docs: give practice adoption a page for the people who do it
FelixTJDietrich Aug 22, 2026
50ba35c
test(webapp): make the panel stories assert the panel
FelixTJDietrich Aug 22, 2026
beb72de
fix(webapp): recover an area's stale plan where the practice's recovers
FelixTJDietrich Aug 22, 2026
ee91730
feat(webapp): make the practice surfaces answer for themselves
FelixTJDietrich Aug 22, 2026
17ddce6
feat(webapp): say what the catalog actually does to a workspace copy
FelixTJDietrich Aug 22, 2026
e000828
fix(webapp): stop a setting and a provenance state looking identical
FelixTJDietrich Aug 22, 2026
93847eb
feat(webapp): let the catalog section arrive rather than appear
FelixTJDietrich Aug 22, 2026
e90003a
docs: name the practice page for the word the product uses
FelixTJDietrich Aug 22, 2026
e2911f3
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 22, 2026
7c6017b
refactor(webapp): one word per concept, and atoms for the facts repea…
FelixTJDietrich Aug 22, 2026
c2411ff
refactor(webapp): let the compiler catch a mis-ordered drawer stack
FelixTJDietrich Aug 22, 2026
1b582cf
refactor(webapp): one control, one meta line, and the rules written down
FelixTJDietrich Aug 22, 2026
8ad4782
fix(webapp): keep your place when editing, and let wide fields stack
FelixTJDietrich Aug 22, 2026
b85ef83
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 23, 2026
a4da43f
refactor(webapp): delete two dead modules and the prose that claimed …
FelixTJDietrich Aug 23, 2026
d155a70
refactor(webapp): make the practice editors drawer levels, not pages
FelixTJDietrich Aug 23, 2026
9b40364
fix(webapp): make the detail drawer slide, peek and step back correctly
FelixTJDietrich Aug 23, 2026
dc03d47
fix(webapp): give the practice editors the panel's own edges
FelixTJDietrich Aug 23, 2026
5f3a931
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 23, 2026
9f5f4e5
refactor(webapp): use Base UI's own refusal, and cut the prose that r…
FelixTJDietrich Aug 23, 2026
8882ee1
refactor(webapp): say each thing once
FelixTJDietrich Aug 23, 2026
1f7218b
fix(webapp): let every panel close the same four ways
FelixTJDietrich Aug 23, 2026
e198658
Merge remote-tracking branch 'origin/main' into 1362-production-readi…
FelixTJDietrich Aug 23, 2026
dafb4cb
fix(webapp): put the version banner inside the panel it belongs to
FelixTJDietrich Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-practices-adopt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hephaestus": minor
---

Workspace administrators can now review an instance-catalog practice and its validation state before adopting an independent, editable copy. New workspaces choose what to adopt, and adopted practices start with Review before sending so eligible feedback waits for an owner or administrator to approve or reject it.
32 changes: 28 additions & 4 deletions docs/contributor/practice-catalogue.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,23 @@ bundled definition and order apply. See
[ADR 0028](https://github.com/ls1intum/Hephaestus/blob/main/docs/decisions/0028-source-synced-practice-catalog.md)
for the architectural decision.

## Durable invariants
## Current workspace adoption lifecycle

- **Slug is identity.** It participates in provenance and observation recurrence. Change the display name
freely; rename a slug only with an explicit remapping strategy.
- **Slug is the current operational identity.** It participates in provenance and observation recurrence.
Change the display name freely; rename a slug only with an explicit remapping strategy. This is the
pre-1.0 workspace-copy model, not the final catalogue identity design.
- **Definitions and order are independent.** Reordering does not create a definition override or an
audit event. **Use Hephaestus order** removes custom positions.
- **Workspace installation happens once.** Later instance changes never rewrite workspace copies.
- **Legacy workspace installation happens once.** Startup repair preserves the snapshots created before
explicit adoption shipped; later instance changes never rewrite those workspace copies. New workspaces
start empty and choose what to adopt.
- **Adoption is deliberate.** Workspace administrators can browse the entries the instance currently
offers, inspect the complete effective definition and its validation fingerprint, and then adopt an
independent copy. The preview's entity tag must still match when the copy is created.
- **Adoption does not authorize automatic sending.** A newly adopted practice receives its own
`HUMAN_APPROVAL` autonomy (**Review before sending**). Reviews may run, but eligible feedback waits for
a workspace owner or administrator to approve or reject it. Moving to **Send automatically** is a
separate administrator decision; validation evidence does not make that authorization implicitly.
- **Provenance is descriptive, not referential.** Matched workspace copies retain the source slug and
comparison fingerprint without a foreign key. A bundled source may have no database row and may
disappear in a later release.
Expand All @@ -128,6 +138,20 @@ bundled entries continue to follow repository order and instance-created entries
deliberate reorder records the complete affected list. Moving a practice to another area is a
definition change and is audited.

## Catalogue identity and variants after the first rollout

Explicit review before adoption remains the product boundary, but workspace-local copies keyed by slug
are transitional. The 1.0 catalogue design is tracked in
[#1445](https://github.com/ls1intum/Hephaestus/issues/1445): repository-minted stable identities,
immutable revisions, adoption records, and repository-managed specializations for technology or course
context. A workspace should adopt the general practice once; review-time resolution can then select one
applicable specialization and pin its exact revision on the observation.

Do not add one-off technology fields or treat specializations as unrelated practices in this model.
Applicability, ambiguity checks, stack confirmation, history, and family-level rollups need one coherent
catalogue design. Until that lands, adoption stores the exact source slug and review-rule fingerprint and
never derives automatic-delivery authority from a detected technology.

## Release behavior

Each effective entry is resolved from the running bundled definition and any instance override:
Expand Down
202 changes: 202 additions & 0 deletions server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ tags:
name: Admin
- description: Current user's Slack privacy preferences
name: Slack User Preferences
- description: Adopt instance catalog practices into a workspace
name: Practice Catalog Adoption
- description: Per-workspace Outline collection mirroring control plane
name: Outline collections
- description: Team management within workspace
Expand Down Expand Up @@ -4228,6 +4230,131 @@ paths:
summary: Set how much autonomy the system has over one area
tags:
- Practice Areas
/workspaces/{workspaceSlug}/practice-catalog/adoption:
get:
operationId: listAdoptablePractices
parameters:
- description: Workspace slug
in: path
name: workspaceSlug
required: true
schema:
type: string
pattern: "^[a-z0-9][a-z0-9-]{2,50}$"
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/CatalogPracticeSummary"
description: Available practices returned
"403":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: Workspace administrator access is required
summary: List practices available for adoption
tags:
- Practice Catalog Adoption
/workspaces/{workspaceSlug}/practice-catalog/adoption/{slug}:
get:
operationId: previewPracticeAdoption
parameters:
- description: Workspace slug
in: path
name: workspaceSlug
required: true
schema:
type: string
pattern: "^[a-z0-9][a-z0-9-]{2,50}$"
- in: path
name: slug
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/CatalogPracticePreview"
description: Adoption preview returned
headers:
ETag:
description: Strong validator for If-Match
style: simple
"403":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: Workspace administrator access is required
"404":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: Practice is not offered
summary: Get a practice adoption preview
tags:
- Practice Catalog Adoption
post:
operationId: adoptPractice
parameters:
- description: Workspace slug
in: path
name: workspaceSlug
required: true
schema:
type: string
pattern: "^[a-z0-9][a-z0-9-]{2,50}$"
- in: path
name: slug
required: true
schema:
type: string
- in: header
name: If-Match
required: false
schema:
type: string
responses:
"201":
content:
application/json:
schema:
$ref: "#/components/schemas/Practice"
description: Practice adopted in Review before sending
"403":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: Workspace administrator access is required
"409":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: The practice slug already exists in the workspace
"412":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: Catalog or workspace state changed since preview
"428":
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
description: The If-Match preview validator is required
summary: Adopt a practice in Review before sending
tags:
- Practice Catalog Adoption
/workspaces/{workspaceSlug}/practice-catalog/practices/{slug}:
get:
description: The current instance-catalog definition for comparison with a workspace
Expand Down Expand Up @@ -7746,6 +7873,21 @@ components:
- "null"
description: Destination area slug; omit or set to null for Unassigned
example: review-ready-work
CatalogAdoptionArea:
type: object
properties:
definition:
$ref: "#/components/schemas/CuratedAreaRequest"
disposition:
type: string
enum:
- UNASSIGNED
- REUSE_EXISTING_AREA
- CREATE_CATALOG_AREA
slug:
type: string
required:
- disposition
CatalogEntryStatus:
type: object
properties:
Expand Down Expand Up @@ -7795,6 +7937,66 @@ components:
- link
- slug
- sourceOffered
CatalogPracticePreview:
type: object
properties:
area:
$ref: "#/components/schemas/CatalogAdoptionArea"
availability:
type: string
enum:
- AVAILABLE
- ADOPTED
- SLUG_CONFLICT
definition:
$ref: "#/components/schemas/CuratedPracticeDefinition"
etag:
type: string
initialAutonomy:
type: string
enum:
- "OFF"
- HUMAN_APPROVAL
- AUTOMATIC
slug:
type: string
sourceReviewRuleFingerprint:
type: string
required:
- area
- availability
- definition
- etag
- initialAutonomy
- slug
- sourceReviewRuleFingerprint
CatalogPracticeSummary:
type: object
properties:
areaName:
type: string
areaSlug:
type: string
artifactKind:
type: string
automatedReviewValidation:
$ref: "#/components/schemas/PracticeAutomatedReviewValidation"
availability:
type: string
enum:
- AVAILABLE
- ADOPTED
- SLUG_CONFLICT
name:
type: string
slug:
type: string
required:
- artifactKind
- automatedReviewValidation
- availability
- name
- slug
ChatMessage:
type: object
description: Mentor chat message in AI SDK UIMessage shape.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionOperations;

/** Installs the effective instance catalog once; workspace copies remain independent afterwards. */
/** Repairs legacy workspace catalog snapshots once; new workspaces adopt practices explicitly. */
@Component
@ConditionalOnServerRole
class DefaultPracticeCatalogSeeder {
Expand Down Expand Up @@ -97,10 +97,10 @@ public void onWorkspaceCreated(WorkspaceCreatedEvent event) {
try {
taskExecutor.execute(() -> {
try {
workspaceRepository.findById(event.workspaceId()).ifPresent(this::seedCatalogSafely);
transactionOperations.executeWithoutResult(ignored -> markCatalogReady(event.workspaceId()));
} catch (RuntimeException exception) {
log.error(
"Could not load workspace {} for default practice catalog seeding",
"Could not initialize explicit practice catalog adoption for workspace {}",
event.workspaceId(),
exception
);
Expand All @@ -115,6 +115,15 @@ public void onWorkspaceCreated(WorkspaceCreatedEvent event) {
}
}

private void markCatalogReady(Long workspaceId) {
Workspace workspace = workspaceRepository.findByIdForUpdate(workspaceId).orElse(null);
if (workspace == null || installationRepository.existsById(workspaceId)) {
return;
}
Instant now = clock.instant();
installationRepository.save(new PracticeCatalogInstallation(workspaceId, now, now));
}

private void seedCatalogSafely(Workspace workspace) {
try {
transactionOperations.executeWithoutResult(ignored -> seedCatalog(workspace));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ public PracticeArea createAreaFromCatalog(
return practiceAreaRepository.save(area);
}

@Transactional
public PracticeArea adoptAreaFromCatalog(
WorkspaceContext ctx,
String slug,
AreaDefinition definition,
int displayOrder
) {
PracticeArea area = createAreaFromCatalog(ctx, slug, definition, displayOrder);
configAudit.record(
ConfigAuditEntry.created(
ConfigAuditEntityType.PRACTICE_AREA,
area.getId(),
ctx.id(),
PracticeAreaSnapshot.of(area)
)
);
return area;
}

/**
* Sets — or clears — the autonomy this area imposes on the practices under it that hold no autonomy of their own.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ record PracticeDefinitionSnapshot(
String automatedReviewPolicySha256,
@Nullable String whyItMatters,
@Nullable String whatGoodLooksLike,
@Nullable String areaSlug
@Nullable String areaSlug,
@Nullable String sourceCuratedSlug,
@Nullable String sourceCuratedFingerprint
) implements ConfigAuditSnapshot {
static PracticeDefinitionSnapshot of(Practice practice, @Nullable Integer criteriaRevision) {
return new PracticeDefinitionSnapshot(
Expand All @@ -31,7 +33,9 @@ static PracticeDefinitionSnapshot of(Practice practice, @Nullable Integer criter
PracticeAutomatedReviewPolicyDigest.digest(practice.getAutomatedReviewPolicy()),
practice.getWhyItMatters(),
practice.getWhatGoodLooksLike(),
practice.getArea() == null ? null : practice.getArea().getSlug()
practice.getArea() == null ? null : practice.getArea().getSlug(),
practice.getSourceCuratedSlug(),
practice.getSourceCuratedFingerprint()
);
}
}
Loading
Loading