# analysis-jira - an analysis document, read as work

<!-- toc -->
- [The marker gate runs first, before any network call](#the-marker-gate-runs-first-before-any-network-call)
- [Coverage is two-way, and the second direction is the useful one](#coverage-is-two-way-and-the-second-direction-is-the-useful-one)
- [An unverifiable run is allowed; looking verified is not](#an-unverifiable-run-is-allowed-looking-verified-is-not)
- [Identity is a label, not a title](#identity-is-a-label-not-a-title)
- [The write is ledgered](#the-write-is-ledgered)
- [An existing node is skipped, never updated](#an-existing-node-is-skipped-never-updated)
- [The story body is document text, escaped](#the-story-body-is-document-text-escaped)
- [An existing epic, linked the way the site links](#an-existing-epic-linked-the-way-the-site-links)
- [Channel clones are opt-in](#channel-clones-are-opt-in)
- [The document lists its tickets](#the-document-lists-its-tickets)
- [Every site-specific name is a VALUE, never a schema key](#every-site-specific-name-is-a-value-never-a-schema-key)
- [Auth](#auth)
<!-- /toc -->

`/multi-agent:analysis-jira` turns a rendered analysis document into a Jira tree.
Two files do it, and the split is the design:

| File | Does | Network |
|---|---|---|
| `scripts/analysis-story-tree.mjs` | decides WHAT to create, checks coverage, assigns identity | none |
| `scripts/analysis-story-body.mjs` | builds each story's description from the document | none |
| `lib/analysis-jira-write.sh` | creates it | yes |
| `lib/jira-epic-link.sh` | decides which field links a story to its epic | none (the writer passes the site's answers in) |
| `scripts/analysis-tickets-writeback.mjs` | writes the created keys back into the document | none |

Planning is deterministic and testable offline; creating issues is neither. Kept
apart, the file that can write to a tracker is small enough to read in one
sitting.

`jira-publish.sh` writes a comment or a description on an issue that already
exists; creating issues is a different contract, so it is a different file, and
the only one that creates issues.

## The marker gate runs first, before any network call

Zero `EKLENECEK`, zero `TBD`, no open Section 20 row. A document with an open
placeholder is not a plan, and turning it into a tree publishes the gap as work
somebody is now assigned. Refusal is exit 4 and it names
`/multi-agent:analysis-resolve` as the step.

This is the same closure contract `status: final` enforces in
`validate-analysis-doc.mjs`, applied at the point the document leaves the
analysis world.

## Coverage is two-way, and the second direction is the useful one

| Direction | Catches |
|---|---|
| every defined id appears in some story | a dropped requirement |
| every cited id is defined in the document | an **invented story** - a node with no requirement behind it |

No forward check can see the second one, and it is the failure mode of building
a tree from a model's reading rather than from the document's own ids.

The atom is `BR-<slug>-NN` in the global profile and `FG-NN` in the corporate
one; the group is the `BR-<slug>` prefix, or `UC-NN`. Locked 30 guarantees both
exist, which is why the tree can be derived rather than invented.

`coverageOf()` is exported and tested directly. The planner cannot emit an
invented id - it derives every `sourceIds` from the defined set - so a test that
fabricates one and re-checks it with its own logic proves nothing about the
shipped code. The backward direction guards the boundary where a plan arrives
from somewhere else: hand-edited, resumed from an older format, or produced by
something that read the prose instead of the ids.

## An unverifiable run is allowed; looking verified is not

A lite document may carry no ids at all. Coverage then cannot run, and the
verdict is `unverifiable`, never `ok`. It appears:

- on its own line in the preview, where a reader looks for `ok`
- as a separate fourth approval option, not folded into `Approve`
- in the writer's own output, and beside every key in the ledger

Such a document still plans work, from its user-story sub-sections, so a
document without atoms gets stories and an honest `unverifiable` verdict rather
than "nothing to plan".

## Identity is a label, not a title

Each node carries `<labelPrefix>-<10 hex>`, hashed from the document id
(`evidence_digest`) plus the node's own source ids. A second run finds its tree
back with one JQL search on those labels.

Titles were the obvious key and are the wrong one: they get edited, and matching
on them breaks exactly when someone has improved the wording. The label lives
server-side, so it survives a new machine, a deleted `~/.claude`, and a **second
analyst** - who is precisely the person positioned to open a duplicate tree.

## The write is ledgered

`~/.claude/logs/multi-agent/_analysis-jira/<docId>.jsonl`. An `intent` line
before each POST, the key after the response. A crash between them leaves an
intent with no key; the next run sees that and searches by label before sending
anything. Without the ledger the failure mode is not "the run stopped" but "the
run stopped and the retry made a second tree", which is the expensive one.

Every line also carries the coverage verdict, so "was this tree checked?" stays
answerable from the tree's own trail.

## An existing node is skipped, never updated

Jira has no backup path for fields other than description. Rewriting a body an
engineer has since edited would lose that edit with no way back. A node found by
its label is recorded in the ledger as `exists`, beside its key.

## The story body is document text, escaped

`buildStoryBody()` assembles the description from the document alone:

| Part | Global profile | Corporate profile |
|---|---|---|
| User story | the "As a ..." line of the story's own section; the user-story section only when it holds exactly one | the use case's actor and short description |
| Acceptance criteria | the G/W/T column of each rule, else the rule | each functional requirement, plus the use case's alternative and exception flows |
| Current vs target | redesign rows that point at the story's section | the same, from the corporate redesign sub-sections |
| Links | the analysis page, Figma URLs in the story's sections, Swagger/OpenAPI URLs in References | same |

At most 8 criteria. What is missing stays missing and becomes a body warning
(no user story, fewer than 2 criteria, no negative criterion); nothing is filled
in. Every piece of document text passes `escapeJiraWiki()` before it is placed
in wiki markup, and links pass `wikiLink()`, which accepts http and https only,
so document text cannot become a macro, a mention, an image or a link.

## An existing epic, linked the way the site links

`--epic KEY` links every story to an epic that already exists; the tree never
creates one. Cloud links through the `parent` field. Server and Data Center use
an Epic Link custom field whose id differs per site, so `issueTree.epicLinkMode:
auto` reads `serverInfo` and discovers the field by its schema type
(`com.pyxis.greenhopper.jira:gh-epic-link`). No field found means the stories
are created unlinked, with a warning; a field id is never guessed. `parent`,
`epicLinkField` (with `epicLinkFieldId`) and `none` force one path. A story that
already exists is not relinked.

## Channel clones are opt-in

With `issueTree.cloneByChannel: true` and `channelComponents` filled, each story
gets one clone per channel: its own label (the story's identity plus
`ch:<channel>`), the channel's component, and an issue link of
`cloneLinkType` (default `Relates`) back to the story. Clone labels are part of
the label search, and a link is checked before it is sent, so a re-run doubles
neither.

## The document lists its tickets

After the write, `analysis-tickets-writeback.mjs` puts a `## Tickets` table at
the end of the local document, between `<!-- tickets:start -->` and
`<!-- tickets:end -->`: story, key, source ids and channel, keys taken from the
ledger. It carries no section number, so the canonical numbering is untouched,
and a re-run replaces the block. An unverifiable tree is titled
`## Tickets (coverage unverified)`. Publishing the updated document to its
Confluence page is a separate approval.

## Every site-specific name is a VALUE, never a schema key

`prefs.global.issueTree` holds the vocabulary. A key is a published literal and
this schema ships to everyone, so a site's component, team and issue-type names
are values the site fills in:

- `channelComponents` / `channelTeams` are free-form maps: the key is the
  channel, the value is the site's own name for it
- `subtaskRoles` ships **empty**. An empty list is an instruction to look at how
  this board actually splits work; a ready-made list would be the guess most
  worth avoiding
- `epicLinkFieldId`, when set, is the site's own field id; otherwise it is
  discovered
- `subtaskIssueType: null` means discover it - `createmeta` returns whichever
  type carries `subtask: true`, under whatever name the site gave it. Same rule
  as `features/jira-context.md`: the type travels as it comes from Jira and is
  never a matching criterion in code

The preview prints every field beside the pref key it came from, so a wrong
setting is visible before the writes rather than in Jira afterwards.

## Auth

`lib/_jira-auth.sh` holds one host-and-token resolution and one curl idiom: the
token reaches curl through a `-K` config on process substitution and never
touches argv, a log, or `ps`. That idiom is the part most easily retyped badly,
so it lives in one file.

**Only this writer consumes it.** `jira-publish.sh` and `issue-fetcher.sh`
carry their own resolution; `issue-fetcher.sh` resolves per-account token keys
that this helper does not model. The file is the shared copy for new callers. The leak property itself is asserted on all three callers
independently in `smoke-analysis-jira.sh`, which is the part that must hold
whether or not they ever share code.
