feat(intent): personal surfaces phase A — identity/personal/sensitive + the generated my-controller#6260
Conversation
| * the target's property - {@code name} resolves to the target's own display label (its authored | ||
| * name field or its synthesized {@code Name}). | ||
| */ | ||
| private static List<Map<String, Object>> buildLabelParts(EntityIntent entity, Map<String, EntityIntent> byName) { |
… DSL + the scoped my-controller One model, two generated REST surfaces. 'identity: <field>' on the entity representing the person declares how the logged-in username maps to a record; 'personal: true' on a record-owning to-one relation (one per entity, target must declare identity - parse-checked same-model, generation-checked cross-model via TargetInfo.identityProperty) makes the entity get an ADDITIONAL generated <Entity>MyController: reads filtered to the mapped identity record, the owner FK forced server-side on writes, foreign and missing records the same 404, and 'sensitive: true' fields stripped from responses AND ignored on writes (server-side allow-list - UI hiding alone would be cosmetic security). Composition children inherit the scope through their direct parent as an ancestor-ownership guard. The power controller is untouched. Emission: entity identityProperty, FK relationshipPersonal + relationshipIdentityProperty, field sensitiveProperty; parameterUtils derives personalProperty/personalParent/sensitiveProperties; generateUtils gains the personalModels collection; rest-java registers EntityMyController.java.template on it. Design + phasing (personal UI, My Shell, per-user task assignee, collection-driven generation): PERSONALIZATION_PLAN.md. Unit: parser (5) + EDM emission; IntentEmissionCoverageIT extends the fixture and asserts both layers - generated tokens and the published app's behavior over REST (scoped list, 404 on foreign rows, forced owner, kept-on-update sensitive values, child ancestor guard). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d}|format tokens)
An entity 'label:' expression synthesizes a stored, read-only Name property
recomputed by the generated repository on every write path (save/update and
updateWithoutEvent - workflow writes stamp label inputs like the document
number). Tokens are own fields or ONE-hop to-one relation properties with an
optional date format; deeper paths are rejected - labels compose by
referencing the related entity's own generated Name ({ProjectTimesheet.Name}
-> '2026 July - My Project - Ivan Georgiev'). Dropdown label resolution
prefers Name (same-model labelFieldName + cross-model labelField), so every
lookup to a labeled entity renders meaningfully instead of raw ids. Parser
rejects a label next to an authored name field and tokens referencing
sensitive fields.
Unit: parser + EDM emission (Name synthesis, labelParts, dropdown value).
IntentEmissionCoverageIT: computeName token + live computed Name on the
personal create ('spoofed (Admin)') and after update ('edited (Admin)').
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…honors readOnly
Phase D: a user task with 'assignee: personal' emits
flowable:assignee="${__personalUser}" instead of a candidate group; the
trigger listener resolves the record's owner through the personal relation +
identity mapping and carries the username IN the start payload - assignee
expressions evaluate at task creation, inside Process.start, so a variable
set afterwards arrives too late (caught live by the IT: the task stayed
unassigned). Parser requires the trigger entity to have a personal relation.
Phase F: the document item dialog renders read-only columns (calculated /
intent readOnly) as plain values instead of controls - the save path already
ignored them, so the editable steppers were fake editability with silently
lost input.
IntentEmissionCoverageIT: ClaimConfirm process fixture; asserts the BPMN
assignee token, the trigger's __personalUser seeding, and - live - that the
confirm task lands ASSIGNED in the owner's (admin's) inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase B: every personal entity gets generated my pages talking to the scoped MyController only - a list per personal root and a create/edit form per personal entity. The owner FK and sensitive fields are never rendered (the form model simply does not carry them); children inheriting the personal scope render as embedded panels on the parent's form: an x-h-calendar for detailCalendar children (empty-day click books with the parent FK + clicked date preset, event-click edits) or a table. The SPA gains /my/<Entity> routes and a 'My' sidebar section; each personal root contributes a perspective to the new application-personal-perspectives extension point. Phase C: resources-my - the My Shell at /services/web/my/, the personal counterpart of the shared application shell: aggregates every module's personal perspectives under one 'my' group, task-first (the Inbox IS the home page), no power surfaces (projections/documents/reports). Wired into components/pom + group-ui. Platform gotcha found live: the perspectives extension service classifies a group by the PRESENCE of items[] and SILENTLY DROPS any perspective whose groupId matches no group - the group definer must carry items: []. IntentEmissionCoverageIT extended: my page emission (scoped controller, sensitive/owner absent from the form, child my form, SPA routes, extension point registration) + runtime (the My Shell serves; the aggregation lists the published personal perspective). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ee22582 to
c8a3e30
Compare
…ource collection
Phase E: schedules[].generate gains children[] - one child row of the just-
generated parent per element of a source collection: forEach {entity, match}
(rows of a LOCAL entity whose field equals a source-row field, e.g. one
timesheet line per assigned employee) or forEach {days: workingDays} +
dayField (one row per working day - Mon-Fri - of the month, e.g. one 8h day
allocation per working day). parent: names the child's to-one back to the
generated record; children nest one more level (depth two, parser-capped).
Everything pre-renders in the glue (the expansions convention) - the Job
template stays shape-only; numeric literal defaults render as BigDecimal
(a bare int literal broke the registry-wide javac batch, caught live by the
emission IT's implicit compile verification).
IntentEmissionCoverageIT: the monthly-claims schedule fixture (Person ->
Claim + per-working-day ClaimLine) asserts the generated job saves the
parent, iterates working days, and writes children through the child's
repository - and the published registry batch compiling IS the deepest
assert (a broken job fails every downstream check).
PERSONALIZATION_PLAN.md: all six phases done.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All six phases are now on this branch (one commit each), plus the agreed
Everything is asserted in Two platform gotchas found live and encoded in the docs: the perspectives extension service silently drops a perspective whose Formatting validated across all touched modules; the release-profile javadoc gate passes on engine-intent (rebased over #6261). 🤖 Generated with Claude Code |
…idebar was floating over content) The My Shell index.html (resources-my, from the personalization shell #6260) never closed its <div x-h-sidebar-content>. With that one </div> missing, the whole tag nesting shifted by one: the footer's closes were consumed by sidebar-content and sidebar, so the sidebar's <div x-h-split-panel> was never closed and the MAIN panel nested inside it. The x-h-split then had a single child, collapsing the two-column layout - the sidebar rendered as a floating overlay at the left, partially hiding the main content. Add the missing </div> after the domain-apps </template> (matching the shared application shell). The document is now div-balanced (48/48). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6265) * fix(my-shell): close the sidebar-content div so the split lays out (sidebar was floating over content) The My Shell index.html (resources-my, from the personalization shell #6260) never closed its <div x-h-sidebar-content>. With that one </div> missing, the whole tag nesting shifted by one: the footer's closes were consumed by sidebar-content and sidebar, so the sidebar's <div x-h-split-panel> was never closed and the MAIN panel nested inside it. The x-h-split then had a single child, collapsing the two-column layout - the sidebar rendered as a floating overlay at the left, partially hiding the main content. Add the missing </div> after the domain-apps </template> (matching the shared application shell). The document is now div-balanced (48/48). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(my-shell): toolbar title 'My Applications' (not 'Applications') The main-panel toolbar reused the application shell's 'Applications' title; in the personal My Shell it should read 'My Applications'. New i18n key application-core:shell.nav.myApplications with that fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
One model, two generated surfaces
For entities that are a person's own records (vacation requests, timesheet day allocations), one generated UI is wrong for everybody: the back-office user needs everything; the person needs a surface bound to themselves with confidential fields absent. Per-role field rules sprinkled through one UI rot and leak; this PR generates a second, scoped surface instead — safe by construction.
The DSL (three words)
What gets generated (phase A: the REST contract)
An additional
<Entity>MyControllerper personalized entity:Criteria.eq(identityProperty, User.getName())); no mapping row → empty, never an error;sensitivefields stripped from responses and ignored on writes (kept values on update) — enforcement is in the serialization, not the UI;requireMyParentancestor guard);immutable/immutableWhenguards preserved; the power controller is untouched.Parser validations: identity must be an own string field; one personal relation per entity; target must declare identity (same-model at parse, cross-model at generation, loud); sensitive never on the PK or the identity field.
Tests
IntentParserTest(5 new) +EdmIntentGeneratorTest(emission ofidentityProperty/relationshipPersonal/relationshipIdentityProperty/sensitiveProperty).IntentEmissionCoverageIT— fixture gains the Person/Claim/ClaimLine slice (the IT user maps via anemail: adminseed) and asserts both layers: the generated tokens, then the published app over REST — scoped list with the sensitive field null, 404 for the foreign row, spoofed owner/rate on create forced/ignored, update keeps the stored sensitive value, child lines guarded through the parent. Green in 46s.Design & phasing
PERSONALIZATION_PLAN.md(repo root, HARMONIA_RUNTIME_PLAN.md-style) records the agreed decisions and the follow-up phases: B personal UI ("My …" pages), C My Shell (/services/web/my/aggregating all personal perspectives), D per-user task assignment for personal processes, E collection-driven generation, F item-dialog readOnly.🤖 Generated with Claude Code