• Closes the core PBC row of the v1.0 target. Ships pbc-quality as a
    lean v1 recording-only aggregate: any caller that performs a quality
    inspection (inbound goods, in-process work order output, outbound
    shipment) appends an immutable InspectionRecord with a decision
    (APPROVED/REJECTED), inspected/rejected quantities, a free-form
    source reference, and the inspector's principal id.
    
    ## Deliberately narrow v1 scope
    
    pbc-quality does NOT ship:
      - cross-PBC writes (no "rejected stock gets auto-quarantined" rule)
      - event publishing (no InspectionRecordedEvent in api.v1 yet)
      - inspection plans or templates (no "item X requires checks Y, Z")
      - multi-check records (one decision per row; multi-step
        inspections become multiple records)
    
    The rationale is the "follow the consumer" discipline: every seam
    the framework adds has to be driven by a real consumer. With no PBC
    yet subscribing to inspection events or calling into pbc-quality,
    speculatively building those capabilities would be guessing the
    shape. Future chunks that actually need them (e.g. pbc-warehousing
    auto-quarantine on rejection, pbc-production WorkOrder scrap from
    rejected QC) will grow the seam into the shape they need.
    
    Even at this narrow scope pbc-quality delivers real value: a
    queryable, append-only, permission-gated record of every QC
    decision in the system, filterable by source reference or item
    code, and linked to the catalog via CatalogApi.
    
    ## Module contents
    
    - `build.gradle.kts` — new Gradle subproject following the existing
      recipe. api-v1 + platform/persistence + platform/security only;
      no cross-pbc deps (guardrail #9 stays honest).
    - `InspectionRecord` entity — code, item_code, source_reference,
      decision (enum), inspected_quantity, rejected_quantity, inspector
      (principal id as String, same convention as created_by), reason,
      inspected_at. Owns table `quality__inspection_record`. No `ext`
      column in v1 — the aggregate is simple enough that adding Tier 1
      customization now would be speculation; it can be added in one
      edit when a customer asks for it.
    - `InspectionDecision` enum — APPROVED, REJECTED. Deliberately
      two-valued; see the entity KDoc for why "conditional accept" is
      rejected as a shape.
    - `InspectionRecordJpaRepository` — existsByCode, findByCode,
      findBySourceReference, findByItemCode.
    - `InspectionRecordService` — ONE write verb `record`. Inspections
      are immutable; revising means recording a new one with a new code.
      Validates:
        * code is unique
        * source reference non-blank
        * inspected quantity > 0
        * rejected quantity >= 0
        * rejected <= inspected
        * APPROVED ↔ rejected = 0, REJECTED ↔ rejected > 0
        * itemCode resolves via CatalogApi
      Inspector is read from `PrincipalContext.currentOrSystem()` at
      call time so a real HTTP user records their own inspections and
      a background job recording a batch uses a named system principal.
    - `InspectionRecordController` — `/api/v1/quality/inspections`
      with GET list (supports `?sourceReference=` and `?itemCode=`
      query params), GET by id, GET by-code, POST record. Every
      endpoint @RequirePermission-gated.
    - `META-INF/vibe-erp/metadata/quality.yml` — 1 entity, 2
      permissions (`quality.inspection.read`, `quality.inspection.record`),
      1 menu.
    - `distribution/.../db/changelog/pbc-quality/001-quality-init.xml`
      — single table with the full audit column set plus:
        * CHECK decision IN ('APPROVED', 'REJECTED')
        * CHECK inspected_quantity > 0
        * CHECK rejected_quantity >= 0
        * CHECK rejected_quantity <= inspected_quantity
      The application enforces the biconditional (APPROVED ↔ rejected=0)
      because CHECK constraints in Postgres can't express the same
      thing ergonomically; the DB enforces the weaker "rejected is
      within bounds" so a direct INSERT can't fabricate nonsense.
    - `settings.gradle.kts`, `distribution/build.gradle.kts`,
      `master.xml` all wired.
    
    ## Smoke test (fresh DB + running app, as admin)
    
    ```
    POST /api/v1/catalog/items {code: WIDGET-1, baseUomCode: ea}
      → 201
    
    POST /api/v1/quality/inspections
         {code: QC-2026-001, itemCode: WIDGET-1, sourceReference: "WO:WO-001",
          decision: APPROVED, inspectedQuantity: 100, rejectedQuantity: 0}
      → 201 {inspector: <admin principal uuid>, inspectedAt: "..."}
    
    POST /api/v1/quality/inspections
         {code: QC-2026-002, itemCode: WIDGET-1, sourceReference: "WO:WO-002",
          decision: REJECTED, inspectedQuantity: 50, rejectedQuantity: 7,
          reason: "surface scratches detected on 7 units"}
      → 201
    
    GET  /api/v1/quality/inspections?sourceReference=WO:WO-001
      → [{code: QC-2026-001, ...}]
    GET  /api/v1/quality/inspections?itemCode=WIDGET-1
      → [APPROVED, REJECTED]   ← filter works, 2 records
    
    # Negative: APPROVED with positive rejected
    POST /api/v1/quality/inspections
         {decision: APPROVED, rejectedQuantity: 3, ...}
      → 400 "APPROVED inspection must have rejected quantity = 0 (got 3);
             record a REJECTED inspection instead"
    
    # Negative: rejected > inspected
    POST /api/v1/quality/inspections
         {decision: REJECTED, inspectedQuantity: 5, rejectedQuantity: 10, ...}
      → 400 "rejected quantity (10) cannot exceed inspected (5)"
    
    GET  /api/v1/_meta/metadata
      → permissions include ["quality.inspection.read",
                              "quality.inspection.record"]
    ```
    
    The `inspector` field on the created records contains the admin
    user's principal UUID exactly as written by the
    `PrincipalContextFilter` — proving the audit trail end-to-end.
    
    ## Tests
    
    - 9 new unit tests in `InspectionRecordServiceTest`:
      * `record persists an APPROVED inspection with rejected=0`
      * `record persists a REJECTED inspection with positive rejected`
      * `inspector defaults to system when no principal is bound` —
        validates the `PrincipalContext.currentOrSystem()` fallback
      * `record rejects duplicate code`
      * `record rejects non-positive inspected quantity`
      * `record rejects rejected greater than inspected`
      * `APPROVED with positive rejected is rejected`
      * `REJECTED with zero rejected is rejected`
      * `record rejects unknown items via CatalogApi`
    - Total framework unit tests: 297 (was 288), all green.
    
    ## Framework state after this commit
    
    - **20 → 21 Gradle subprojects**
    - **10 of 10 core PBCs live** (pbc-identity, pbc-catalog, pbc-partners,
      pbc-inventory, pbc-warehousing, pbc-orders-sales, pbc-orders-purchase,
      pbc-finance, pbc-production, pbc-quality). The P5.x row of the
      implementation plan is complete at minimal v1 scope.
    - The v1.0 acceptance bar's "core PBC coverage" line is met. Remaining
      v1.0 work is cross-cutting (reports, forms, scheduler, web SPA)
      plus the richer per-PBC v2/v3 scopes.
    
    ## What this unblocks
    
    - **Cross-PBC quality integration** — any PBC that needs to react
      to a quality decision can subscribe when pbc-quality grows its
      event. pbc-warehousing quarantine on rejection is the obvious
      first consumer.
    - **The full buy-make-sell BPMN scenario** — now every step has a
      home: sales → procurement → warehousing → production → quality →
      finance are all live. The big reference-plug-in end-to-end
      flow is unblocked at the PBC level.
    - **Completes the P5.x row** of the implementation plan. Remaining
      v1.0 work is cross-cutting platform units (P1.8 reports, P1.9
      files, P1.10 jobs, P2.2/P2.3 designer/forms) plus the web SPA.
    zichun authored
     
    Browse Code »