@mmerterden/multi-agent-pipeline 20.12.0 → 20.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/README.tr.md +1 -1
  4. package/docs/facts.json +1 -1
  5. package/install/_common.mjs +1 -1
  6. package/install/_plugin-skills.mjs +13 -9
  7. package/install/catalog-history.json +1 -1
  8. package/install/codex.mjs +12 -12
  9. package/manifest.json +59 -53
  10. package/package.json +1 -1
  11. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
  13. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
  14. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  15. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
  16. package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
  17. package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
  18. package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
  19. package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
  20. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
  21. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
  25. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
  26. package/pipeline/lib/analysis-jira-write.sh +81 -27
  27. package/pipeline/lib/analysis-quality.mjs +261 -0
  28. package/pipeline/lib/analysis-sections.mjs +105 -0
  29. package/pipeline/lib/jira-epic-link.sh +54 -0
  30. package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
  31. package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
  32. package/pipeline/multi-agent-refs/analysis/render.md +20 -21
  33. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
  34. package/pipeline/multi-agent-refs/analysis/review.md +16 -3
  35. package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
  36. package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
  37. package/pipeline/multi-agent-refs/analysis-template.md +21 -14
  38. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
  39. package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
  40. package/pipeline/schemas/analysis-spec.schema.json +22 -0
  41. package/pipeline/schemas/prefs.schema.json +22 -1
  42. package/pipeline/scripts/analysis-story-body.mjs +210 -0
  43. package/pipeline/scripts/analysis-story-tree.mjs +73 -12
  44. package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
  45. package/pipeline/scripts/build-references.mjs +14 -1
  46. package/pipeline/scripts/confluence-readback.mjs +148 -0
  47. package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
  48. package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
  49. package/pipeline/skills/.skill-manifest.json +8 -8
  50. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
  51. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
  52. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
  53. package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
  54. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
  55. package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
  56. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -0
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: "Zero-base prompt review: measure the always-on instruction footprint, classify every rule block, propose keep/trial-removal/delete; applies only approved rows. Use when a new model ships or instructions have accumulated."
3
+ disable-model-invocation: true
3
4
  description-tr: "Sıfır-tabanlı prompt incelemesi: sürekli yüklü talimat yükünü ölçer, her kural bloğunu sınıflar, tut/dene/sil önerir; yalnızca onaylananı uygular."
4
5
  argument-hint: "[--report-only]"
5
6
  parameters: [{"name":"--report-only","kind":"flag"}]
@@ -26,6 +26,11 @@ Skip Phase 0-3 and review a diff only. Five input shapes, two providers:
26
26
 
27
27
  Branch-mode runs print to chat only. PR-mode runs print to chat AND post the verdict to the PR per `$HOME/.claude/multi-agent-refs/channels/pr-review-actions.md`: per-finding inline comments + an explicit approve / needs-work state. **No monolithic advisory comment** is ever posted.
28
28
 
29
+ ## Gotchas
30
+
31
+ - The counterpart-repo comparison needs exactly one resolved candidate. With several, an unattended run skips it silently, so an autopilot review can have no cross-platform section.
32
+ - The counterpart's stack comes from marker files, never from the repo name.
33
+
29
34
  ## Steps
30
35
 
31
36
  ### 0. Session bootstrap - TASK_ID + tmp cleanup
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: "Review a written analysis document instead of a diff: resolve it from a path, a Confluence page or a Jira issue, run the deterministic gates first, then a parallel model review. Findings cite the Locked rule they break. Never edits the document."
3
- description-tr: "Diff yerine yazılmış analiz dokümanını review eder: yol, Confluence sayfası veya Jira issue'sundan getirir, önce deterministik geçitleri koşar, sonra paralel model review. Bulgular ihlal edilen Locked kuralını gösterir. Dokümanı düzenlemez."
2
+ description: "Review a written analysis document instead of a diff: resolve it from a path, a Confluence page or a Jira issue, run the deterministic gates first, then a parallel model review. Findings cite the Locked rule they break. Never edits the document. Use when an analysis needs judging before development starts."
3
+ description-tr: "Diff yerine yazılmış analiz dokümanını review eder: yol, Confluence sayfası veya Jira issue'sundan getirir, önce deterministik geçitleri koşar, sonra paralel model review. Bulgular ihlal edilen Locked kuralını gösterir. Dokümanı düzenlemez. Bir analizin geliştirme başlamadan önce değerlendirilmesi gerektiğinde kullanılır."
4
4
  argument-hint: "[path | Confluence URL | pageId | JIRA-KEY] [--state <state.json>] - optional; with no argument, pick from recent analyses"
5
5
  parameters: [{"name":"target","kind":"text"},{"name":"--state","kind":"path"}]
6
6
  gui: form
@@ -14,6 +14,11 @@ confirm: none
14
14
 
15
15
  `/multi-agent:review` judges a diff. This judges the document that diff was supposed to come from, before anyone writes the code it describes. No worktree, no branch, no commit, and the reviewed document is never edited in place.
16
16
 
17
+ ## Gotchas
18
+
19
+ - Validator ERRORs are Blockers and WARNs are Important, unless the document states why the warned-about item is acceptable.
20
+ - Every rubric class is printed, with `none` when empty. A class missing from the verdict did not run; it is not a clean result.
21
+
17
22
  ## Flow
18
23
 
19
24
  Read `$HOME/.claude/multi-agent-refs/analysis/review.md` and execute it:
@@ -10,8 +10,8 @@
10
10
  # decides WHAT to write can be tested without a network.
11
11
  #
12
12
  # `jira-publish.sh` cannot do this: it writes a comment or a description on an
13
- # issue that already exists. The only creation path in this repo was a curl
14
- # hand-written inside a markdown instruction, which is the thing this replaces.
13
+ # issue that already exists. This file is the one place that creates issues, so
14
+ # the gate, the ledger and the label search apply to every creation.
15
15
  #
16
16
  # THE WRITE IS LEDGERED, AND THE LEDGER IS THE POINT
17
17
  #
@@ -32,11 +32,10 @@
32
32
  # AN EXISTING NODE IS SKIPPED, NEVER UPDATED
33
33
  #
34
34
  # Jira has no backup path for fields other than description. Rewriting a body an
35
- # engineer has since edited would repeat, at tree scale, the defect that made
36
- # jira-publish.sh take backups in the first place.
35
+ # engineer has since edited would lose that edit with no way back.
37
36
  #
38
37
  # Usage:
39
- # analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE]
38
+ # analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE] [--epic KEY]
40
39
  #
41
40
  # Exit: 0 written (or previewed), 3 the plan does not parse, 4 auth, 5 a Jira
42
41
  # call failed, 64 usage.
@@ -47,14 +46,15 @@ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
46
  # shellcheck source=/dev/null
48
47
  . "$SELF_DIR/_jira-auth.sh"
49
48
 
50
- PLAN=""; PROJECT=""; DRY=0; LEDGER=""
49
+ PLAN=""; PROJECT=""; DRY=0; LEDGER=""; EPIC=""
51
50
  while [ $# -gt 0 ]; do
52
51
  case "$1" in
53
52
  --plan) PLAN="${2:-}"; shift 2 ;;
54
53
  --project) PROJECT="${2:-}"; shift 2 ;;
55
54
  --ledger) LEDGER="${2:-}"; shift 2 ;;
56
55
  --dry-run) DRY=1; shift ;;
57
- *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE]" >&2; exit 64 ;;
56
+ --epic) EPIC="${2:-}"; shift 2 ;;
57
+ *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE] [--epic KEY]" >&2; exit 64 ;;
58
58
  esac
59
59
  done
60
60
  [ -n "$PLAN" ] && [ -f "$PLAN" ] || { echo "ERR: --plan FILE is required and must exist" >&2; exit 64; }
@@ -90,7 +90,7 @@ fi
90
90
  ORPHANS=0
91
91
  if [ -f "$LEDGER" ]; then
92
92
  ORPHANS=$(jq -rs '[.[] | select(.event=="intent")] as $i
93
- | [.[] | select(.event=="created") | .label] as $c
93
+ | [.[] | select(.event=="created" or .event=="exists") | .label] as $c
94
94
  | [$i[] | select(.label as $l | ($c | index($l)) | not)] | length' \
95
95
  "$LEDGER" 2>/dev/null || echo 0)
96
96
  if [ "${ORPHANS:-0}" -gt 0 ]; then
@@ -109,7 +109,7 @@ existing_for() { # existing_for <label> -> prints the key, or empty
109
109
  printf ''
110
110
  }
111
111
 
112
- LABELS="$(jq -r '[.nodes[] | .label, (.subtasks[]?.label)] | join(",")' "$PLAN")"
112
+ LABELS="$(jq -r '[.nodes[] | .label, (.clones[]?.label), (.subtasks[]?.label)] | join(",")' "$PLAN")"
113
113
  if [ "$DRY" -eq 0 ] && [ -n "$LABELS" ]; then
114
114
  JQL="project=${PROJECT} AND labels in (${LABELS})"
115
115
  ENC="$(printf '%s' "$JQL" | jq -sRr @uri)"
@@ -122,13 +122,11 @@ if [ "$DRY" -eq 0 ] && [ -n "$LABELS" ]; then
122
122
  fi
123
123
 
124
124
  # Two channels on purpose. The human line goes to stdout; the created key comes
125
- # back in CREATED_KEY. Returning the key on stdout too meant a caller using
126
- # command substitution swallowed the report - the first dry run printed a header
127
- # and nothing else, and the tree looked empty.
125
+ # back in CREATED_KEY. Keeping the key off stdout lets a caller capture the
126
+ # report with command substitution without losing any of it.
128
127
  # Same gate, same five candidate paths, same refusal as jira-publish.sh and
129
- # post-pr-review.sh. This file is the only ISSUE-CREATION path in the tree, and
130
- # a summary plus a description is outbound text like any other - it was the one
131
- # writer the gate did not cover.
128
+ # post-pr-review.sh. A summary plus a description is outbound text like any
129
+ # other, so every issue this file creates passes the gate first.
132
130
  ma_outbound_gate_text() {
133
131
  local text="$1" og="" tmp rc
134
132
  for c in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/outbound-gate.mjs" \
@@ -149,25 +147,52 @@ ma_outbound_gate_text() {
149
147
  return $rc
150
148
  }
151
149
 
150
+ link_issues() { # link_issues <type> <fromKey> <toKey> <cloneLabel>
151
+ local ltype="$1" from="$2" to="$3" label="$4" linked
152
+ if [ "$DRY" -eq 1 ]; then
153
+ echo " link $ltype ${from:-<story>} <-> ${to:-<clone>}"
154
+ return 0
155
+ fi
156
+ # A link already there is left alone, so a re-run never doubles it.
157
+ linked="$(jira_api GET "/rest/api/2/issue/${from}?fields=issuelinks" 2>/dev/null \
158
+ | jq -r --arg t "$to" '[.fields.issuelinks[]? | (.outwardIssue.key // .inwardIssue.key)] | index($t) != null' 2>/dev/null || echo false)"
159
+ if [ "$linked" = "true" ]; then
160
+ echo " linked $from <-> $to"
161
+ return 0
162
+ fi
163
+ if ! printf '%s' "$(jq -cn --arg t "$ltype" --arg a "$from" --arg b "$to" \
164
+ '{type: {name: $t}, inwardIssue: {key: $a}, outwardIssue: {key: $b}}')" \
165
+ | jira_api POST "/rest/api/2/issueLink" --data @- >/dev/null; then
166
+ echo "ERR: could not link $from to $to" >&2
167
+ return 5
168
+ fi
169
+ ledger link "$label" "$to"
170
+ echo " linked $from <-> $to"
171
+ }
172
+
152
173
  CREATED_KEY=""
153
- create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description>
154
- local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" body key existing
174
+ create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description> [extraFieldsJson]
175
+ local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" extra="${6:-}" body key existing
176
+ [ -n "$extra" ] || extra="{}"
155
177
  CREATED_KEY=""
156
178
  existing="$(existing_for "$label")"
157
179
  if [ -n "$existing" ]; then
158
180
  # Skipped, never updated: Jira has no backup path for fields other than
159
181
  # description, and an engineer may have edited this body since.
160
182
  echo " exists $existing $summary"
183
+ ledger exists "$label" "$existing"
161
184
  CREATED_KEY="$existing"
162
185
  return 0
163
186
  fi
164
187
  body="$(jq -n --arg p "$PROJECT" --arg s "$summary" --arg t "$itype" \
165
- --arg l "$label" --arg d "$desc" --arg par "$parent" '
188
+ --arg l "$label" --arg d "$desc" --arg par "$parent" --argjson x "$extra" '
166
189
  {fields: ({project: {key: $p}, summary: $s, issuetype: {name: $t},
167
190
  labels: [$l], description: $d}
168
- + (if $par == "" then {} else {parent: {key: $par}} end))}')"
191
+ + (if $par == "" then {} else {parent: {key: $par}} end)
192
+ + $x)}')"
169
193
  if [ "$DRY" -eq 1 ]; then
170
194
  echo " create $itype $summary [$label]"
195
+ [ "$parent" = "" ] && printf '%s\n' "$desc" | sed 's/^/ /'
171
196
  return 0
172
197
  fi
173
198
  # After the dry-run branch, because a dry run publishes nothing, and before
@@ -196,6 +221,24 @@ echo "analysis-jira-write: ${PROJECT}, coverage ${VERDICT}$([ "$DRY" -eq 1 ] &&
196
221
  [ "$VERDICT" = "unverifiable" ] && \
197
222
  echo " NOTE: coverage could not be checked for this document; the tree is unverified."
198
223
 
224
+ # The epic the stories join, if one was named. The link field depends on the
225
+ # deployment, so it is resolved once, against the live site, before any write.
226
+ STORY_EXTRA="{}"
227
+ if [ -n "${EPIC:-}" ]; then
228
+ echo " epic $EPIC"
229
+ if [ "$DRY" -eq 0 ]; then
230
+ . "$(dirname "${BASH_SOURCE[0]}")/jira-epic-link.sh"
231
+ E_MODE="$(jq -r '.prefsUsed.epicLinkMode // "auto"' "$PLAN")"
232
+ E_FIELD="$(jq -r '.prefsUsed.epicLinkFieldId // empty' "$PLAN")"
233
+ E_SERVER="" E_FIELDS=""
234
+ if [ "$E_MODE" = "auto" ]; then
235
+ E_SERVER="$(jira_api GET "/rest/api/2/serverInfo" 2>/dev/null || echo "")"
236
+ [ -n "$E_FIELD" ] || E_FIELDS="$(jira_api GET "/rest/api/2/field" 2>/dev/null || echo "")"
237
+ fi
238
+ STORY_EXTRA="$(epic_link_fields "$E_MODE" "$EPIC" "$E_FIELD" "$E_SERVER" "$E_FIELDS")"
239
+ fi
240
+ fi
241
+
199
242
  RC=0
200
243
  NODES="$(jq -c '.nodes[]' "$PLAN")"
201
244
  while IFS= read -r node; do
@@ -204,18 +247,27 @@ while IFS= read -r node; do
204
247
  s_title="$(printf '%s' "$node" | jq -r '.title')"
205
248
  s_src="$(printf '%s' "$node" | jq -r '.sourceIds | join(", ")')"
206
249
  s_type="$(jq -r '.prefsUsed.storyIssueType // "Story"' "$PLAN")"
207
- s_desc="Sources: ${s_src:-none (derived from the section heading)}"
208
- if ! create_issue "$s_label" "$s_title" "$s_type" "" "$s_desc"; then
209
- # A failed story does not get its sub-tasks written anyway. `create_issue`
210
- # omits the parent field when the parent key is empty, so without this the
211
- # sub-tasks of a story Jira refused were still POSTed - and landed as live,
212
- # parentless issues nobody asked for, from a run that had already reported
213
- # an error.
250
+ s_desc="$(printf '%s' "$node" | jq -r '.description // empty')"
251
+ [ -n "$s_desc" ] || s_desc="Sources: ${s_src:-none (derived from the section heading)}"
252
+ if ! create_issue "$s_label" "$s_title" "$s_type" "" "$s_desc" "$STORY_EXTRA"; then
253
+ # A failed story does not get its sub-tasks or clones written. `create_issue`
254
+ # omits the parent field when the parent key is empty, so they would land as
255
+ # live, parentless issues from a run that has already reported an error.
214
256
  RC=5
215
257
  echo " skipped this story's sub-tasks: the story itself was not created" >&2
216
258
  continue
217
259
  fi
218
260
  skey="$CREATED_KEY"
261
+ LINK_TYPE="$(jq -r '.prefsUsed.cloneLinkType // "Relates"' "$PLAN")"
262
+ while IFS= read -r clone; do
263
+ [ -n "$clone" ] || continue
264
+ c_label="$(printf '%s' "$clone" | jq -r '.label')"
265
+ c_title="$(printf '%s' "$clone" | jq -r '.title')"
266
+ c_extra="$(printf '%s' "$clone" | jq -c --argjson e "$STORY_EXTRA" \
267
+ '$e + {components: [.components[] | {name: .}]}')"
268
+ create_issue "$c_label" "$c_title" "$s_type" "" "$s_desc" "$c_extra" || { RC=5; continue; }
269
+ link_issues "$LINK_TYPE" "$skey" "$CREATED_KEY" "$c_label" || RC=5
270
+ done < <(printf '%s' "$node" | jq -c '.clones[]?')
219
271
  while IFS= read -r sub; do
220
272
  [ -n "$sub" ] || continue
221
273
  t_label="$(printf '%s' "$sub" | jq -r '.label')"
@@ -228,7 +280,9 @@ while IFS= read -r node; do
228
280
  | jq -r '[.projects[]?.issuetypes[]? | select(.subtask==true) | .name] | first // empty' 2>/dev/null || echo "")"
229
281
  fi
230
282
  [ -n "$t_type" ] || t_type="Sub-task"
231
- create_issue "$t_label" "${s_title} - ${t_role}" "$t_type" "$skey" "$s_desc" || RC=5
283
+ t_desc="$(printf '%s' "$sub" | jq -r '.description // empty')"
284
+ [ -n "$t_desc" ] || t_desc="Sources: ${s_src:-none (derived from the section heading)}"
285
+ create_issue "$t_label" "${s_title} - ${t_role}" "$t_type" "$skey" "$t_desc" || RC=5
232
286
  done < <(printf '%s' "$node" | jq -c '.subtasks[]?')
233
287
  done <<< "$NODES"
234
288
 
@@ -0,0 +1,261 @@
1
+ /**
2
+ * analysis-quality.mjs - document-quality checks on a rendered analysis
3
+ * document: every use case says how it fails, the document does not contradict
4
+ * itself, each layer carries only its own kind of content, and a design frame
5
+ * belongs to a use case.
6
+ *
7
+ * Each check takes the document's lines and returns `{errors, warns}` so the
8
+ * validator stays the only place that prints and decides the exit code.
9
+ *
10
+ * @module pipeline/lib/analysis-quality
11
+ */
12
+
13
+ import { sectionBody, tableDataRows } from "./analysis-sections.mjs";
14
+
15
+ const cellsOf = (row) =>
16
+ row
17
+ .trim()
18
+ .replace(/^\||\|$/g, "")
19
+ .split("|")
20
+ .map((c) => c.trim());
21
+ const unbold = (s) => s.replace(/\*\*/g, "").trim();
22
+ const isHeading = (l) => /^#{1,6}\s/.test(l);
23
+ const isNumberedHeading = (l) => /^#{2,4}\s+\d+(\.\d+)*\.?\s/.test(l);
24
+ const UC_HEADING = /\bUC-\d{2,3}\b/;
25
+
26
+ const MAIN_FLOW = ["Ana Akış", "Ana Akis", "Main Flow"];
27
+ const EXCEPTION_FLOW = ["İstisna Akışı", "Istisna Akisi", "Exception Flow"];
28
+ const EXPLICIT_NONE = /^(yok|none)\s*:\s*\S/i;
29
+
30
+ /** Use-case sections: from a heading naming `UC-NNN` up to the next numbered heading. */
31
+ function useCaseBlocks(lines) {
32
+ const blocks = [];
33
+ let current = null;
34
+ for (const l of lines) {
35
+ if (isHeading(l)) {
36
+ const uc = l.match(UC_HEADING);
37
+ if (uc && isNumberedHeading(l)) {
38
+ current = { id: uc[0], lines: [] };
39
+ blocks.push(current);
40
+ continue;
41
+ }
42
+ if (isNumberedHeading(l) || /^#\s/.test(l)) current = null;
43
+ }
44
+ if (current) current.lines.push(l);
45
+ }
46
+ return blocks;
47
+ }
48
+
49
+ function fieldRow(lines, names) {
50
+ for (const l of lines) {
51
+ if (!l.trim().startsWith("|")) continue;
52
+ const c = cellsOf(l);
53
+ if (c.length >= 2 && names.some((n) => unbold(c[0]).toLowerCase() === n.toLowerCase())) {
54
+ return c[1];
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * A use case with a main flow states its exception flow. Missing is an error on
62
+ * a final document and a warning on a draft; "-" or empty is a warning;
63
+ * "Yok: <reason>" / "None: <reason>" is an explicit answer.
64
+ */
65
+ export function exceptionFlow(lines, { final = false } = {}) {
66
+ const errors = [];
67
+ const warns = [];
68
+ for (const uc of useCaseBlocks(lines)) {
69
+ if (fieldRow(uc.lines, MAIN_FLOW) === undefined) continue;
70
+ const v = fieldRow(uc.lines, EXCEPTION_FLOW);
71
+ if (v === undefined) {
72
+ const msg = `${uc.id} has a main flow but no exception flow row (İstisna Akışı / Exception Flow)`;
73
+ (final ? errors : warns).push(msg);
74
+ } else if (!v || /^\p{Pd}$/u.test(v)) {
75
+ warns.push(
76
+ `${uc.id} exception flow is empty; write the failure path, or "Yok: <reason>" when there is none`,
77
+ );
78
+ } else if (/^(yok|none)\.?$/i.test(v) && !EXPLICIT_NONE.test(v)) {
79
+ warns.push(`${uc.id} exception flow says none without a reason ("Yok: <reason>")`);
80
+ }
81
+ }
82
+ return { errors, warns };
83
+ }
84
+
85
+ const normalize = (s) =>
86
+ s
87
+ .toLocaleLowerCase("tr")
88
+ .replace(/[.,;:!?"'`*_]/g, "")
89
+ .replace(/\s+/g, " ")
90
+ .trim();
91
+
92
+ const GOALS_KEYWORDS = [
93
+ "Hedefler ve Karşı Hedefler",
94
+ "Hedefler ve Karsi Hedefler",
95
+ "Goals and Non-Goals",
96
+ "Goals",
97
+ ];
98
+
99
+ /** The same item on both sides of the goals table. */
100
+ export function scopeOverlap(lines) {
101
+ const errors = [];
102
+ const body = sectionBody(lines, GOALS_KEYWORDS);
103
+ if (!body) return { errors, warns: [] };
104
+ const goals = new Map();
105
+ const nonGoals = new Set();
106
+ for (const row of tableDataRows(body)) {
107
+ const c = cellsOf(row);
108
+ if (c.length < 2) continue;
109
+ if (c[0] && c[0] !== "-") goals.set(normalize(c[0]), c[0]);
110
+ if (c[1] && c[1] !== "-") nonGoals.add(normalize(c[1]));
111
+ }
112
+ for (const [key] of goals) {
113
+ if (key && nonGoals.has(key)) {
114
+ errors.push(`"${key}" is both in scope and out of scope in the goals table (Locked 14)`);
115
+ }
116
+ }
117
+ return { errors, warns: [] };
118
+ }
119
+
120
+ const CITABLE = [
121
+ /\bBR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+\b/gi,
122
+ /\bCB-[a-z0-9]+(?:-[a-z0-9]+)*-\d+\b/gi,
123
+ /\b(?:IG|FG)-\d{2,3}\b/g,
124
+ /\bUC-\d{2,3}\b/g,
125
+ ];
126
+
127
+ /**
128
+ * Ids the text cites but never defines. An id is defined by the first cell of a
129
+ * table row or by a heading. Section 20 `AS-NN` ids have their own pairing check.
130
+ */
131
+ export function citedUndefined(lines) {
132
+ const defined = new Set();
133
+ const cited = new Set();
134
+ for (const l of lines) {
135
+ const t = l.trim();
136
+ const sources = [];
137
+ if (isHeading(t)) {
138
+ sources.push([t, defined]);
139
+ } else if (t.startsWith("|")) {
140
+ const c = cellsOf(t);
141
+ sources.push([unbold(c[0] || ""), defined], [c.slice(1).join(" | "), cited]);
142
+ } else {
143
+ sources.push([t, cited]);
144
+ }
145
+ for (const [chunk, set] of sources) {
146
+ for (const re of CITABLE) {
147
+ for (const m of chunk.matchAll(re)) set.add(m[0].toUpperCase());
148
+ }
149
+ }
150
+ }
151
+ const errors = [...cited]
152
+ .filter((id) => !defined.has(id))
153
+ .sort()
154
+ .map((id) => `${id} is cited but never defined (no table row or heading names it)`);
155
+ return { errors, warns: [] };
156
+ }
157
+
158
+ const FIGMA_FRAME = /https:\/\/(?:www\.)?figma\.com\/[^\s)|\]]*node-id=([0-9]+[-:][0-9]+)/g;
159
+ const REFERENCES = /Referans|References/i;
160
+
161
+ /** A Figma frame linked outside every use case and outside the references (corporate). */
162
+ export function designFrameUnmapped(lines) {
163
+ const warns = [];
164
+ let inUc = false;
165
+ let inRefs = false;
166
+ for (const l of lines) {
167
+ if (isHeading(l)) {
168
+ if (isNumberedHeading(l) || /^#\s/.test(l)) {
169
+ inUc = UC_HEADING.test(l) && isNumberedHeading(l);
170
+ inRefs = REFERENCES.test(l) && /^##\s/.test(l);
171
+ } else if (/^##\s/.test(l)) {
172
+ inUc = false;
173
+ inRefs = false;
174
+ }
175
+ continue;
176
+ }
177
+ if (inUc || inRefs) continue;
178
+ for (const m of l.matchAll(FIGMA_FRAME)) {
179
+ warns.push(
180
+ `design frame ${m[1]} is linked outside every use case; list it under the use case it serves`,
181
+ );
182
+ }
183
+ }
184
+ return { errors: [], warns };
185
+ }
186
+
187
+ const PART_A = new Set(["1", "2", "3", "4", "18", "19", "20"]);
188
+ const REDESIGN_EVIDENCE = [
189
+ "Mevcut Davranış",
190
+ "Mevcut Davranis",
191
+ "Current Behaviour",
192
+ "Current Behavior",
193
+ "Endpoint Eşlemesi",
194
+ "Endpoint Eslemesi",
195
+ "Endpoint Mapping",
196
+ ];
197
+ const CODE_PATH =
198
+ /[\w.-]+(?:\/[\w.-]+)+\.(?:swift|kt|kts|java|tsx?|jsx?|mjs|py|mm?|go|rb|cs|dart)\b/g;
199
+ const ENDPOINT = /\b(?:GET|POST|PUT|PATCH|DELETE)\s+\/[\w{}/.-]*/g;
200
+
201
+ /**
202
+ * File paths and endpoints in the business layer (Part A: sections 1-4, 18-20) of
203
+ * the global profile. Collapsible `<details>` blocks and the redesign evidence
204
+ * tables are where such evidence belongs, so they are not read.
205
+ */
206
+ export function businessLayerTechnical(lines) {
207
+ const warns = [];
208
+ let section = null;
209
+ let skip = false;
210
+ let details = 0;
211
+ for (const l of lines) {
212
+ const top = l.match(/^##\s+(\d+)\.?\s/);
213
+ if (top) {
214
+ section = top[1];
215
+ skip = false;
216
+ continue;
217
+ }
218
+ if (/^#\s/.test(l)) {
219
+ section = null;
220
+ continue;
221
+ }
222
+ if (/^#{3,4}\s/.test(l)) {
223
+ skip = REDESIGN_EVIDENCE.some((k) => l.includes(k));
224
+ continue;
225
+ }
226
+ details += (l.match(/<details\b/gi) || []).length;
227
+ const closes = (l.match(/<\/details>/gi) || []).length;
228
+ const inDetails = details > 0;
229
+ details = Math.max(0, details - closes);
230
+ if (!section || !PART_A.has(section) || skip || inDetails) continue;
231
+ for (const re of [CODE_PATH, ENDPOINT]) {
232
+ for (const m of l.matchAll(re)) {
233
+ warns.push(
234
+ `Section ${section} (business layer) names "${m[0].trim()}"; move it to Part B/C or into a <details> evidence block`,
235
+ );
236
+ }
237
+ }
238
+ }
239
+ return { errors: [], warns };
240
+ }
241
+
242
+ const PANEL = /^>\s*\*\*(Missing inputs|Eksik girdiler)\s*:?\s*\*\*/i;
243
+
244
+ /** A final document that still lists missing inputs. */
245
+ export function missingInputsFinal(lines, { final = false } = {}) {
246
+ if (!final) return { errors: [], warns: [] };
247
+ const start = lines.findIndex((l) => PANEL.test(l.trim()));
248
+ if (start < 0) return { errors: [], warns: [] };
249
+ let items = 0;
250
+ for (const l of lines.slice(start + 1)) {
251
+ const t = l.trim();
252
+ if (!t.startsWith(">")) break;
253
+ if (/^>\s*[-*]\s+\S/.test(t)) items++;
254
+ }
255
+ return {
256
+ errors: [],
257
+ warns: items
258
+ ? [`the document is final but its missing inputs panel lists ${items} input(s)`]
259
+ : [],
260
+ };
261
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * analysis-sections.mjs - locate sections and table rows in a rendered
3
+ * analysis document.
4
+ *
5
+ * Shared by the document validator and the story-tree planner, so both read a
6
+ * section the same way. Sections are found by heading keyword, not by number:
7
+ * the global and corporate profiles number the same content differently, and a
8
+ * rendered document keeps canonical numbers with gaps.
9
+ *
10
+ * @module pipeline/lib/analysis-sections
11
+ */
12
+
13
+ /**
14
+ * Body lines of the first numbered heading at one of `levels` whose text holds
15
+ * one of `keywords`. Closed by the next heading at the same level or shallower,
16
+ * so the last sub-section of a parent does not run into the following section.
17
+ *
18
+ * @param {string[]} lines
19
+ * @param {string[]} keywords
20
+ * @param {number|number[]} levels
21
+ * @returns {string[]|null}
22
+ */
23
+ export function sectionBody(lines, keywords, levels = 2) {
24
+ for (const level of [].concat(levels)) {
25
+ const open = new RegExp(`^#{${level}}\\s+\\d+(\\.\\d+)*\\.?\\s`);
26
+ const close = new RegExp(`^#{1,${level}}\\s`);
27
+ const start = lines.findIndex((l) => open.test(l) && keywords.some((k) => l.includes(k)));
28
+ if (start < 0) continue;
29
+ let end = lines.length;
30
+ for (let i = start + 1; i < lines.length; i++) {
31
+ if (close.test(lines[i])) {
32
+ end = i;
33
+ break;
34
+ }
35
+ }
36
+ return lines.slice(start + 1, end);
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Data rows of the tables in `bodyLines`: the pipe-rows after a separator row,
43
+ * which is the only shape that tells them from the header without reading cells.
44
+ *
45
+ * @param {string[]} bodyLines
46
+ * @returns {string[]}
47
+ */
48
+ export function tableDataRows(bodyLines) {
49
+ const rows = [];
50
+ let afterSeparator = false;
51
+ for (const line of bodyLines) {
52
+ const t = line.trim();
53
+ if (!t.startsWith("|")) {
54
+ afterSeparator = false;
55
+ continue;
56
+ }
57
+ if (/^\|[\s\-:|]+\|$/.test(t)) {
58
+ afterSeparator = true;
59
+ continue;
60
+ }
61
+ if (afterSeparator) rows.push(t);
62
+ }
63
+ return rows;
64
+ }
65
+
66
+ const CB_KEYWORDS = ["Mevcut Davranış", "Mevcut Davranis", "Current Behaviour", "Current Behavior"];
67
+ const EP_KEYWORDS = ["Endpoint Eşlemesi", "Endpoint Eslemesi", "Endpoint Mapping"];
68
+ const DL_KEYWORDS = ["Fark Listesi", "Difference List"];
69
+
70
+ /**
71
+ * Where each redesign artefact lives per profile. The global profile has them as
72
+ * sub-sections 4.5, 4.6 and 9.5; the corporate profile nests them under its own
73
+ * spine (2.1.1, 2.3.1 and the endpoint mapping under Section 5 service details).
74
+ */
75
+ export const REDESIGN_SECTIONS = {
76
+ global: {
77
+ cb: { keywords: CB_KEYWORDS, levels: [3], label: "4.5 Current Behaviour" },
78
+ ep: { keywords: EP_KEYWORDS, levels: [3], label: "4.6 Endpoint Mapping" },
79
+ dl: { keywords: DL_KEYWORDS, levels: [3], label: "9.5 Difference List" },
80
+ },
81
+ corporate: {
82
+ cb: { keywords: CB_KEYWORDS, levels: [4], label: "2.1.1 Mevcut Davranış / Current Behaviour" },
83
+ ep: {
84
+ keywords: EP_KEYWORDS,
85
+ levels: [4],
86
+ label: "5.(N+4).1 Endpoint Eşlemesi / Endpoint Mapping",
87
+ },
88
+ dl: { keywords: DL_KEYWORDS, levels: [4], label: "2.3.1 Fark Listesi / Difference List" },
89
+ },
90
+ };
91
+
92
+ /**
93
+ * The three redesign artefact bodies for a profile, plus their labels.
94
+ *
95
+ * @param {string[]} lines
96
+ * @param {string} profile
97
+ */
98
+ export function redesignSections(lines, profile) {
99
+ const spec = REDESIGN_SECTIONS[profile] || REDESIGN_SECTIONS.global;
100
+ const out = {};
101
+ for (const [key, s] of Object.entries(spec)) {
102
+ out[key] = { body: sectionBody(lines, s.keywords, s.levels), label: s.label };
103
+ }
104
+ return out;
105
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # jira-epic-link.sh - which fields link a new story to an existing epic.
3
+ #
4
+ # Jira links a story to its epic in two different ways. Cloud uses the standard
5
+ # `parent` field; Server and Data Center use an "Epic Link" custom field whose id
6
+ # differs per site. The id is discovered from the field list by its schema type,
7
+ # never assumed. When no way is found the story is created without a link and a
8
+ # warning says so, rather than writing a guessed field.
9
+ #
10
+ # Pure: the caller fetches /rest/api/2/serverInfo and /rest/api/2/field and
11
+ # passes the JSON in, so the decision is testable offline.
12
+ #
13
+ # epic_link_fields <mode> <epicKey> <fieldId> <serverInfoJson> <fieldsJson>
14
+ # mode: auto | parent | epicLinkField | none
15
+ # Prints a JSON object to merge into the create body's `fields`.
16
+
17
+ EPIC_LINK_SCHEMA="com.pyxis.greenhopper.jira:gh-epic-link"
18
+
19
+ epic_link_fields() {
20
+ local mode="${1:-auto}" epic="$2" field_id="$3" server="$4" fields="$5" deployment found
21
+ command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required to link an epic" >&2; return 3; }
22
+ if [ -z "$epic" ] || [ "$mode" = "none" ]; then
23
+ printf '{}\n'
24
+ return 0
25
+ fi
26
+ case "$mode" in
27
+ parent)
28
+ jq -cn --arg e "$epic" '{parent: {key: $e}}'
29
+ return 0
30
+ ;;
31
+ epicLinkField)
32
+ if [ -z "$field_id" ]; then
33
+ echo "WARN: epicLinkField mode needs issueTree.epicLinkFieldId; the epic is not linked" >&2
34
+ printf '{}\n'
35
+ return 0
36
+ fi
37
+ jq -cn --arg f "$field_id" --arg e "$epic" '{($f): $e}'
38
+ return 0
39
+ ;;
40
+ esac
41
+ deployment="$(printf '%s' "$server" | jq -r '.deploymentType // empty' 2>/dev/null || true)"
42
+ if [ "$deployment" = "Cloud" ]; then
43
+ jq -cn --arg e "$epic" '{parent: {key: $e}}'
44
+ return 0
45
+ fi
46
+ found="${field_id:-$(printf '%s' "$fields" | jq -r --arg s "$EPIC_LINK_SCHEMA" \
47
+ '[.[]? | select(.schema.custom == $s) | .id] | first // empty' 2>/dev/null || true)}"
48
+ if [ -z "$found" ]; then
49
+ echo "WARN: no epic-link field on this site; the stories are created without an epic" >&2
50
+ printf '{}\n'
51
+ return 0
52
+ fi
53
+ jq -cn --arg f "$found" --arg e "$epic" '{($f): $e}'
54
+ }