@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.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/docs/facts.json +1 -1
- package/install/_common.mjs +1 -1
- package/install/_plugin-skills.mjs +13 -9
- package/install/catalog-history.json +1 -1
- package/install/codex.mjs +12 -12
- package/manifest.json +59 -53
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
- package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
- package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
- package/pipeline/lib/analysis-jira-write.sh +81 -27
- package/pipeline/lib/analysis-quality.mjs +261 -0
- package/pipeline/lib/analysis-sections.mjs +105 -0
- package/pipeline/lib/jira-epic-link.sh +54 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
- package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
- package/pipeline/multi-agent-refs/analysis/render.md +20 -21
- package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
- package/pipeline/multi-agent-refs/analysis/review.md +16 -3
- package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
- package/pipeline/multi-agent-refs/analysis-template.md +21 -14
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
- package/pipeline/schemas/analysis-spec.schema.json +22 -0
- package/pipeline/schemas/prefs.schema.json +22 -1
- package/pipeline/scripts/analysis-story-body.mjs +210 -0
- package/pipeline/scripts/analysis-story-tree.mjs +73 -12
- package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
- package/pipeline/scripts/build-references.mjs +14 -1
- package/pipeline/scripts/confluence-readback.mjs +148 -0
- package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
- package/pipeline/skills/.skill-manifest.json +8 -8
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
- package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
- package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
- 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.
|
|
14
|
-
#
|
|
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
|
|
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
|
-
|
|
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.
|
|
126
|
-
#
|
|
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.
|
|
130
|
-
#
|
|
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="
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# parentless issues
|
|
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
|
-
|
|
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
|
+
}
|