Skip to content

intent: an amended source rewrites the posting it already made (#7071) - #7086

Merged
delchev merged 1 commit into
masterfrom
issue-7071-posting-reissue
Sep 6, 2026
Merged

intent: an amended source rewrites the posting it already made (#7071)#7086
delchev merged 1 commit into
masterfrom
issue-7071-posting-reissue

Conversation

@delchev

@delchev delchev commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The defect

The amend path is ordinary and documented — issue, reject, add a line, issue again — and it raises a postings: moment a second time. The generated handler answered that with its idempotency test (an existing post whose item count reached the derived one) and did nothing:

SalesInvoice SI00000003  issued 1 200.00  ->  JournalEntry 8: Dt 1 200 / Ct 1 000 / Ct 200
rejected, a line added, issued again at 1 260.00
JournalEntry 8 still says 1 200 / 1 000 / 200

No second entry (right) and a ledger 60.00 short (wrong), with nothing in the application saying so. Idempotence was answering "have I handled this source?" when the question is "does the post still say what the source says?".

The fix

The handler derives the WHOLE content first — the header assignments and every guarded item row — and compares it with the post the back-reference finds:

  • no post → create it, as before;
  • a post carrying exactly the derived content → no-op (a redelivery);
  • anything else → rewrite that post: header assignments re-applied through update, items replaced. One source still has at most one post, and the half-post resume (an item write that failed after the document was saved) is now the same case rather than a separate rule.

The comparison is order-insensitive (row order is not a query guarantee) over the union of every cell the item rows assign, and numbers compare by value so a rescaled amount is not read as a change.

The rewrite stops at the created document's own lifecycle, which the posting itself established: it is rewritable while its function: EntityStatus relation still holds the init: value its own create wrote (a target with no status lifecycle has nothing to act on, so it is always rewritable). Past that — the entry is posted, approved, closed — the divergence is logged naming both documents and the entry is left alone. Unwinding a document somebody has acted on is a correcting entry's job (reverses:), not a silent overwrite; that boundary is deliberate and stated, not an oversight.

No new DSL key. amendableGuard and itemComparedProps are pre-rendered into the glue like everything else (the expansions convention), and GlueGenerator.bindPosting defaults both so a .glue written before this change renders exactly what it used to.

Verification

  • GluePostingsAmendTest (new) — the guard for a target with a status + init:, for one without, and the compared-property union.
  • IntentEngineIT.postings_generates_the_idempotent_resumable_handler — updated to the new emitted shape; a_post_is_not_rewritten_once_the_created_document_has_left_the_status_it_was_created_in (new) asserts the guard and that the refusal is reported.
  • IntentEmissionCoverageIT — green, and it publishes and compiles the generated handlers, so both the plain and the reverses: shape are proven to compile; two assertions added for the comparison and the in-place rewrite.
  • mvn formatter:validate and the release javadoc profile on the touched modules.

Docs: dirigible.io help/intent/dsl-reference.md, the in-repo engine-intent/CLAUDE.md + intent-assistant-guide.md, and an IntentFile spec proposal + site page.

Fixes #7071

The amend path is ordinary and documented - issue, reject, add a line, issue
again - and it raises a `postings:` moment a SECOND time. The generated handler
answered that with its idempotency test (an existing post whose item count
reached the derived one) and did nothing, so the journal entry kept the amounts
of the first issue while the invoice it references said something else: no
second entry (right) and a ledger short by the difference (wrong), with nothing
anywhere saying so. Idempotence was answering "have I handled this source?"
when the question is "does the post still say what the source says?".

The handler now derives the WHOLE content first - the header assignments and
every guarded item row - and compares it with the post the back-reference
finds: identical is a redelivery (no-op), different is either a half-post to
complete or an amendment to rewrite the existing post from. One source still
has at most one post. The comparison is order-insensitive (row order is not a
query guarantee) over the union of every cell the rows assign, and numbers
compare by value so a rescaled amount is not a change.

The rewrite stops where the created document's own lifecycle says somebody has
taken it over, which the posting itself established: it is rewritable while its
`function: EntityStatus` relation still holds the `init:` its own create wrote
(no status lifecycle at all - nothing to act on - is always rewritable). Past
that the divergence is logged naming both documents and left to a correcting
entry (`reverses:`) - overwriting a document an accountant has acted on is
worse than the divergence it repairs.

`amendableGuard` and `itemComparedProps` are pre-rendered into the glue like
everything else, and both fall back to the previous behaviour for a `.glue`
written before them.

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

Labels

None yet

Projects

None yet

1 participant