Skip to content

Decision Record: Tmp-First Analyze & Close Artifacts, with GitHub Fallback for Distill #176

Description

@sameera

Decision Record: Tmp-First Analyze & Close Artifacts, with GitHub Fallback for Distill

Summary

This epic re-homes a local close's analyze receipt and close record to the gitignored ephemeral area
and teaches /nxs.distill to drain from there. The load-bearing move sits upstream of the file
placement: the epic issue's close comment becomes the durable close record, in every mode, so the
ephemeral files are pure hand-off and nothing durable depends on them surviving. Consumption of an
ephemeral entry is then not a new state file but a derived fact — an entry is consumed when the
concept store on the trunk carries its provenance, which is true only after the distillation-PR
merges.

Chosen Approach

Move the durability boundary, not just the files. The ephemeral area holds derived, re-derivable,
disposable hand-off content: the materialized epic (already re-derivable from its issue at any time
by the resolver), the analyze receipt, and the close record. Everything a later drain genuinely needs
— the rationale, the record reference and its approved-body hash, the conformance verdict, and the
landed range — is stamped onto the epic issue's close comment at /nxs.close Phase 8.2, which
already runs in every mode and already carries Key Decisions and Deviation Rationale verbatim.
/nxs.distill gains three capabilities on top of that: it discovers ephemeral entries alongside
committed ones, it derives "already consumed" from the store at the trunk instead of from a deletion
it cannot perform, and it can rebuild an entry entirely from the epic issue when the local copy is
gone. --pr mode and old-contract committed entries are untouched.

Key Decisions

The epic issue's close comment is the durable close record

  • Decision: The close comment posted at /nxs.close Phase 8.2 is defined as the durable copy of
    a close's rationale; the close-record file is a hand-off copy with no durability obligation. This
    holds in every mode, local and --pr alike.
  • Why: Phase 8.2 already runs in every mode and already inlines Key Decisions and Deviation
    Rationale together with the record reference and its full hash. Promoting that from an incidental
    side-effect to the definition resolves the unrecoverable-local-entry question without inventing a
    new surface, gives the "nothing in the comment-writing step may be thinned" requirement a reason to
    exist, and makes the ephemeral/durable split statable as an invariant rather than a placement
    convention.
  • Refuted alternative: Keep the close-record file as the durable artifact and commit it in local
    mode too, mirroring the born-at-close mechanism --pr mode uses. A competent engineer would pick
    this — it reuses a mechanism that already exists and needs no fallback path at all. It lost because
    the local flow has no PR of its own to carry that commit to the trunk, so it either forces a manual
    commit-and-push of a throwaway file (the exact friction this epic exists to remove) or invents a
    second distill-branch flow for the non-PR path.
  • Story scope edit: The fourth story's third acceptance criterion is rewritten. Its premise —
    that a local close leaves no GitHub surface to recover from — is false, so it becomes: recovery
    reads the epic issue's close comment in local and --pr mode alike. Its fourth criterion's
    either/or (hard block vs. degraded drain) resolves to neither; see the recovery decision below.

The close comment carries a marker-anchored machine block alongside its prose

  • Decision: Phase 8.2's comment gains a small marker-anchored fenced block carrying the record
    reference and full approved-body hash, the conformance verdict, and the full-SHA landed range,
    in every mode. The prose sections are unchanged and stay in full.
  • Why: The prose recovers the why; it does not recover the stamped landed range, which a drain
    needs for the what, for the merge precondition, and for repo attribution — and which is by
    contract the exact range the close diffed, not something to recompute later. The block makes the
    epic issue a complete substitute for the close-record file. There is direct precedent:
    /nxs.analyze --pr already publishes exactly this shape on a PR review and /nxs.close already
    parses it back out. This is a deliberate, ratified deviation from the convention that keeps machine
    blocks off human surfaces: that convention's target is the committed queue, and the block stamps
    facts the human has just approved at the Phase 7 checkpoint.
  • Refuted alternative: Recover the range from the epic's merged PR at fallback time. It needs no
    format change and works for the --pr case. It lost on two counts — it fails outright for a local
    close whose work never went through a PR (the solo-developer persona this epic names), and it
    substitutes a recomputation for a fact the system has already declared authoritative.
  • Story scope edit: The block lands with the second story, which already carries the
    durability guarantee for the close comment; the fourth story is consumer-only. The two move
    together — if the block is deferred, the fourth story cannot meet its "no loss of content"
    criterion and that criterion must be weakened in the same breath.

An ephemeral entry is consumed when the concept store at the trunk carries its provenance

  • Decision: Consumption is derived, not marked. An ephemeral entry is consumed exactly when the
    concept store at the fetched trunk carries that epic's provenance in a structured provenance
    position; no state file, no marker file, and no timing logic is introduced.
  • Why: This generalizes the existing "presence means unconsumed" rule rather than abandoning it —
    for a committed entry, the removal riding the distillation-PR is the local expression of the same
    fact, that the store merged. Reading the store directly has three properties nothing else offers:
    the mark cannot exist before the PR carrying the pages merges, because it is the merge; a PR
    closed unmerged leaves the entry rediscoverable, so the never-auto-delete-an-undrained-entry rule
    holds with no extra machinery; and it needs no state file, which the drain stage forbids outright.
    The match must be on structured provenance positions and whole tokens, never free prose, or a stray
    mention silently consumes an undrained entry. Provenance entries are append-only and never edited,
    so once written the mark is permanent.
  • Refuted alternative: Write a marker file into the ephemeral entry, or delete it, when the
    distillation-PR opens. This is genuinely tempting — local, offline, trivially simple, no network
    fetch. It lost because the mark would land before the merge that actually consumes the entry, so a
    PR closed unmerged would silently lose a closed epic's rationale forever; the epic's own acceptance
    criteria forbid exactly this, and no cheap fix exists because nothing runs after the merge.
  • Accepted consequence: A drain that produces zero concept deltas leaves no provenance and is
    re-offered on the next run. That is the safe direction. It is reported plainly, and the ephemeral
    directory is named as safe to delete by hand — never auto-deleted.

For an entry not present at the trunk, the merge precondition is range-head reachability with a merged-PR second test

  • Decision: The merge precondition for such an entry is the recorded range head being reachable
    from the trunk, or that head resolving to a merged pull request. Only when both fail does the
    existing not-merged gate fire, unchanged.
  • Why: The epic's premise is right — the epic-file-presence proxy is meaningless for a file that
    never left the ephemeral area, and the range-head-reachability check the drain already uses in
    continuation mode is the correct replacement. But reachability alone is not sufficient for a local
    close: such a close stamps the pre-merge feature-branch tip as its range head, and a squash or
    rebase merge means that commit never becomes an ancestor of the trunk. Reachability alone would
    therefore report every squash-merged local epic as not-merged, firing the waiver gate on the normal
    path and training the operator to waive it — which destroys the gate's meaning. Resolving the
    recorded head to its associated merged pull request is the same squash-and-rebase-safe resolution
    the PR-worktree helper already performs at close.
  • Story scope edit: The third story's fourth acceptance criterion is rewritten to the two-test
    form, and gains a criterion covering a squash-merged local epic specifically.

The consumed-entry removal is re-aimed, not skipped

  • Decision: At the drain's removal step, the committed deletion targets the epic's committed
    per-user scratch directory when one exists; the ephemeral directory is deleted with no commit.
  • Why: The story says to skip the committed deletion because nothing is committed, but something
    is: the per-user decision scratch, which implementation writes into the committed queue under the
    epic issue number and which is deliberately a different directory from the ephemeral entry. Today
    it is deleted along with the entry when the distillation-PR merges. Under a tmp-only local close, a
    literal skip would leave every closed epic's scratch directory on the trunk with nothing to ever
    delete it. Re-aiming the removal preserves scratch's existing lifecycle exactly and keeps the
    deletion atomic with the page writes.
  • Story scope edit: The third story's second acceptance criterion becomes a re-aim rather than a
    skip. The lead ratified landing this here rather than splitting it out, on the grounds that the
    change is small and deferring it makes a pre-existing leak permanent by design.

GitHub recovery is an explicit per-entry path, not a discovery source

  • Decision: Recovery is invoked explicitly for a named epic issue. It re-derives the epic through
    the resolver, and takes the rationale, record reference, hash, and range from the close comment;
    where a PR exists it takes the conformance verdict from the published analyze review. The
    genuinely unrecoverable case narrows to an epic issue with no close comment, or one whose recorded
    range cannot be resolved locally — that is a hard per-entry block with a named diagnostic.
  • Why: When the ephemeral copy is gone and nothing is committed, there is no local object to
    discover; discovery would require scanning closed epic issues on every run. The lead in this
    scenario knows which epic they are recovering, so an explicit invocation is both sufficient and
    bounded. Narrowing the hard block to "no durable rationale exists at all" is what lets the drain
    refuse precisely instead of either fabricating rationale or mistaking a closed epic for an open
    one.
  • Refuted alternative: Scan closed epic issues for undistilled close comments on every run. This
    would make the drain-SLO report genuinely complete — it would see epics closed but never drained,
    which nothing sees today. It lost on cost and blast radius for an epic assessed with no slack: an
    unbounded issue query on every drain, a new failure mode when the network is unavailable, and a
    report that would surface every historically-closed epic on its first run.

In member mode the migration's unit is the epic — ephemeral artifacts plus committed scratch

  • Decision: A member close migrates the union of the epic's ephemeral artifacts and its committed
    per-user scratch into exactly one hub entry, and the migration helper derives its source-relative
    and destination-relative paths separately.
  • Why: Two concrete defects sit under the member story. First, the helper derives one relative
    path from the entry's directory name and uses it both for the hub destination (correct) and for the
    code-repo tracked-file check (wrong once the source is the ephemeral area): where a committed
    scratch directory of the same name exists, the helper reads it as tracked, then tries to commit a
    removal of a path it never deleted — failing after the irreversible hub commit has landed.
    Second, migrating only the ephemeral artifacts strands the committed scratch in the member repo
    where nothing will ever delete it, and drops it from the hub entry the drain would have cleaned up.
    Migrating the union restores today's end state exactly: one entry, in one place, deleted whole when
    the distillation-PR merges. The rest of the helper is already safe against a gitignored source — it
    walks the filesystem and hashes content directly, so ignore status never affects what it copies or
    verifies.

Constraints & Invariants

  1. In local (non---pr) mode against an issue-sourced epic, the materialized epic, the analyze
    receipt, and the close record are ephemeral hand-off content in the gitignored temporary area —
    never committed, never linked from an issue, and never described as committed on any surface.
  2. An old-contract entry whose epic is already committed keeps every artifact in its committed
    directory, unchanged, in every mode.
  3. --pr mode's artifact placement is unchanged: the analyze verdict is a published PR review, and
    the born-at-close epic plus close record are committed and pushed on the distill branch.
  4. The single durable copy of a local close's rationale is the epic issue's close comment; a local
    close whose close comment fails to post must end with an explicit action-required instruction and
    a preserved body, and must never report success as if the rationale were safe.
  5. The close comment carries the record reference and full approved-body hash, the conformance
    verdict, and the full-SHA landed range in a marker-anchored machine block, in every mode, and
    continues to inline Key Decisions and Deviation Rationale in full.
  6. A directory in the temporary area is a drainable entry only when it carries a close record; an
    epic-only materialization is resolver scratch and is never listed, warned about, or aged.
  7. Ephemeral entries never enter drain-SLO accounting — drain-SLO is a property of durable queues,
    and a local temporary directory says nothing about any other machine.
  8. An ephemeral entry is consumed exactly when the concept store at the fetched trunk carries that
    epic's provenance in a structured provenance position; nothing else marks it, and no state file is
    introduced.
  9. An entry whose provenance is absent from the store is unconsumed and is never auto-deleted,
    whatever its age.
  10. For any entry not present at the trunk, the merge precondition is the recorded range head being
    reachable from the trunk, or that head resolving to a merged pull request; failing both, the
    existing not-merged gate fires unchanged and never proceeds silently.
  11. An ephemeral entry whose recorded range cannot be resolved locally is a named per-entry hard
    error — never a silent empty diff, never a partial one, and never an invented range.
  12. The consumed-entry removal targets the epic's committed per-user scratch directory when one
    exists and never a path in the temporary area; the ephemeral directory is deleted without a
    commit.
  13. The checkpoint digest names each drained entry's source — committed queue, ephemeral area, or
    recovered from the epic issue — and states which deletions actually land with the merge.
  14. GitHub recovery is invoked explicitly for a named epic issue; it re-derives the epic through the
    resolver and takes rationale, record reference, hash, and range from the close comment.
  15. An epic issue carrying no close comment, or one whose range cannot be resolved, is a hard
    per-entry block with a named diagnostic — never drained with empty or fabricated rationale, and
    never reported as "not yet closed".
  16. A member close migrates the epic's ephemeral artifacts and its committed scratch into exactly one
    hub entry, byte-for-byte, and leaves no copy behind in the member checkout that a later drain
    could discover.
  17. The migration helper derives its source-relative path from the source and its
    destination-relative path from the destination, and never applies one to the other.
  18. The migrated hub entry's drain-SLO attribution comes from the first recorded range entry's repo,
    and its age from the migration commit — unchanged.

Risks (BLOCKER / ADDRESS only)

  • ADDRESS — the merge precondition as originally specified fires on the normal local path. A
    local close stamps the pre-merge branch tip, which a squash or rebase merge never lands on the
    trunk, so reachability alone would report the ordinary case as not-merged. Mitigation: the two-test
    form is an invariant above, and the third story gains an acceptance criterion covering a
    squash-merged local epic specifically. If that criterion is dropped, the waiver gate becomes noise
    and stops gating anything.
  • ADDRESS — the second story grows by the machine block. The stamped block was not in the epic's
    scope and this epic is assessed with no slack. Mitigation: it is small, and it belongs to the story
    that already owns the close comment's durability guarantee. Land the two together; deferring the
    block without simultaneously weakening the fourth story's "no loss of content" criterion would ship
    a story that cannot pass.
  • ADDRESS — a failed close comment now loses the rationale outright. Today a failure to post is
    reported and tolerated because the close-record file is committed; under the tmp-only contract
    there is no other copy. Mitigation: the second story must specify the failure behaviour as an
    acceptance criterion — the run ends with an action-required retry naming the preserved body — not
    as prose.
  • ADDRESS — provenance matching is the only thing standing between a stray mention and a silently
    consumed entry.
    Matching on free prose rather than structured provenance positions would let an
    unrelated page mentioning an epic number consume an undrained entry. Mitigation: the whole-token,
    structured-position rule is an invariant above and needs a test that a prose mention does not
    consume.

Open Clarifications

None. Three clarifications were raised by the analysis and resolved at this record's decision gate:
the treatment of a local close with no local copy (full recovery from the close comment; hard block
only where no comment exists), the machine block on the close comment (ratified as an explicit
deviation), and the committed-scratch cleanup (landed in the third story rather than split out).

Metadata

Metadata

Assignees

No one assigned

    Labels

    decision-recordEpic decision record (why: key decisions, invariants, risks)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions