Skip to content

ORB-213: Create goals with habit links atomically - #454

Open
thomasluizon wants to merge 4 commits into
mainfrom
feature/orb-213-goal-habit-ids
Open

ORB-213: Create goals with habit links atomically#454
thomasluizon wants to merge 4 commits into
mainfrom
feature/orb-213-goal-habit-ids

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Aug 7, 2026

Copy link
Copy Markdown
Owner

DEGRADED: same-vendor review

Links ORB-213.

Summary

  • Adds optional habitIds to the goal creation command and HTTP request.
  • Resolves only caller owned habits, requires every requested identifier to resolve, attaches them to the new goal, and persists the goal plus links with the existing single save.
  • Applies the existing maximum habits per goal rule in both the validator and handler.
  • Adds optional habit_ids to create_goal so Astra can create and link in one call.
  • Refreshes the checked in OpenAPI contract with the optional field.

Frozen fixer follow-up

  • Makes the three-dependency CreateGoalTool constructor the explicit DI constructor, so the registered tool always receives the habit repository advertised by its schema.
  • Converts the legacy two-dependency path from a null exception into a controlled tool failure when habit linking is requested.
  • Rejects non-array habit_ids, non-string items, and invalid GUID strings at the tool boundary while preserving absent and empty behavior.
  • Replaces the trivial streak assertion with a scheduled daily habit containing three concrete consecutive logs and asserts that creation leaves progress at the concrete value zero for passive sync.

Design notes

The create handler calls the existing ownership and cap helpers directly. It does not dispatch LinkHabitsToGoalCommand because that command replaces links, repeats the pay gate, and saves independently.

Streak goal creation does not add a speculative sync call. New linked streak goals are already eligible for the hourly passive sync, and goal read paths compute the live value before that sweep. This also preserves the existing completion and gamification path owned by the passive sync service.

Cache invalidation remains the single existing InvalidateUserAiCaches call after creation. The link command does not invalidate any additional key.

Contract and rollout

The request field is append only, optional, and defaults to absent. Existing clients keep the unchanged creation path and response shape. Companion UI ticket ORB-251 carries the shared client contract change.

Validation

  • dotnet build Orbit.slnx --no-restore: passed with zero errors.
  • Focused goal creation, Astra tool, and metadata tests: 34 passed.
  • dotnet test --no-build --no-restore: analyzer 32, domain 516, application 2,989, infrastructure 2,071; all passed.
  • Diff scope: 9 files and 367 insertions, within the authorized caps.
  • Architecture artifacts regenerated with zero orphan endpoints and zero untested feature folders.
  • No new suppression, warning disable, narration comment, migration, or external response field read.

Copy link
Copy Markdown
Owner Author

Implementation approach:

  1. Extend CreateGoalCommand and CreateGoalRequest with optional habit identifiers, preserving the existing absent and empty list behavior.
  2. In CreateGoalCommandHandler, enforce the existing cap, resolve only habits owned by the caller, require OwnershipValidation.AllResolved, attach them through Goal.AddHabit, then use the existing single SaveChangesAsync call.
  3. Extend CreateGoalCommandValidator with the cap rule and expose habit_ids through CreateGoalTool so Astra uses the same atomic command path.
  4. Add focused command and tool tests for absent, empty, valid, foreign, cap, and streak goal cases, asserting persisted relationships and no goal creation on failure.
  5. Confirm cache invalidation and streak sync behavior from existing code before deciding whether either path needs any additional call.

The implementation will land in the named goal command, validator, controller, chat tool, and existing goal test files. I am using the aggregate method and existing ownership helpers directly because dispatching LinkHabitsToGoalCommand would replace links, repeat the pay gate, and introduce a second save. I will not extract a shared service because this remains the second use of the validation pattern.

thomasluizon and others added 2 commits August 6, 2026 23:17
The drift check failed because architecture.html and architecture.json were
not regenerated after CreateGoalCommand gained HabitIds. Habit-linking
assertions move from CreateGoalToolTests to CreateGoalCommandHandlerTests,
where the linking actually happens.

Orbit.Application.Tests: 2984 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thomasluizon

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 50dc37d513

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomasluizon

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50dc37d513

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomasluizon

Copy link
Copy Markdown
Owner Author

DEGRADED: same-vendor review

Independent xhigh review found four blocking items:

  1. The mandatory append-only habitIds shared create-goal contract is absent from orbit-ui-mobile/packages/shared/src/types/goal.ts; the API request changed, but the shared client contract cannot represent it.
  2. CreateGoalTool advertises habit_ids while its habit repository remains optional, so existing two-argument construction can throw on a valid linking request.
  3. habit_ids does not fail closed on malformed JSON: non-arrays are treated as absent and non-string array members can throw.
  4. The streak regression computes its expectation with the production calculator over a trivial no-log case, so it does not independently prove creation-time streak progress.

Two non-blocking test-coverage gaps (controller forwarding and Astra cap enforcement) will be tracked separately per the review contract.

The first blocking item crosses repository boundaries exactly as the ticket’s own Scope requires, while this PR is already 9 files versus the 8-file cap. No fixer is being launched until Thomas chooses the contract/cap handoff. No merge was performed.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Frozen fixer approach for F2 through F4:

  1. Make the habit repository a required CreateGoalTool dependency so every DI-created instance that advertises habit_ids can process it, then update every explicit construction in existing tests.
  2. Parse habit_ids as a strict optional array at the tool boundary. Absent and empty stay unchanged; non-arrays, non-string items, and invalid GUID strings return a ToolResult failure without dispatching the command.
  3. Replace the streak creation assertion with an independently derived scheduled habit and log history whose expected current streak is a fixed concrete value.
  4. Run the focused CreateGoalTool and CreateGoalCommand handler tests, regenerate architecture artifacts only if the constructor shape changes them, then run the solution build and touched suites.
  5. Keep the existing nine-file PR scope, stage named files only, commit, and push this branch.

The production changes remain in CreateGoalTool and existing registrations or constructors only. The regression change remains in the current goal command handler test file. I am requiring the dependency instead of adding a null fallback because the published schema promises the capability for every registered tool instance.

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@thomasluizon

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: e3b9db03cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomasluizon

Copy link
Copy Markdown
Owner Author

DEGRADED: same-vendor review

Same-vendor bias direction is known, but its magnitude here is unmeasured.

Reviewed head: e3b9db03ccee0125bff5ebf344af09d0ca37e5a8

APPROVED BY PR-REVIEW: zero blocking findings

Non-blocking findings and existing follow-ups:

  • F1, High, test quality: src/Orbit.Api/Controllers/GoalsController.cs:60 adds HabitIds: request.HabitIds, but no test exercises the request-to-command mapping. Dropping that mapping would break POST goal linking while the suite remains green. Existing follow-up: ORB-252.
  • F2, High, test quality: src/Orbit.Application/Chat/Tools/Implementations/CreateGoalTool.cs:83 enforces AppConstants.MaxHabitsPerGoal, but no test exercises this Astra-specific branch. Removing it would let create_goal exceed the cap while the suite remains green. Existing follow-up: ORB-253.

No duplicate Linear tickets were filed. ORB-251 already carries the paired optional shared-contract field required by the deploy-API-first rollout.

N/A dimensions:

    1. Cross-platform parity: backend-only diff, with no web or mobile mirror surface.
    1. i18n: no locale files or directly rendered user-facing copy are changed.
    1. Design: no apps/* UI files are changed.
    1. FEATURES.md gating: no new screen, route, tab, Astra or MCP tool, plan gate, or platform or locale availability change is introduced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant