@mmerterden/multi-agent-pipeline 20.23.0 → 20.23.1

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 CHANGED
@@ -16,6 +16,15 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [20.23.1] - 2026-10-13
20
+
21
+ ### Fixed
22
+
23
+ - `/multi-agent:analysis-jira` finds a site's sub-task types on Jira Data Center 9, which answers only the per-project `createmeta/{project}/issuetypes`; sub-tasks were created under the generic `Sub-task` name there. `issueTree.subtaskIssueType` may now map each role to the site's type (`{"develop": "Development Task", "test": "Test Task"}`, `"*"` for any other role), a site with several sub-task types and no setting is named in the run's output, and the preview shows the type each role gets.
24
+ - The code graph reads Swift imports behind an attribute (`@testable import`, `@_exported import`, `@preconcurrency import`, `@_spi(...) import`).
25
+ - Python type parameters (`class Box[T](Base):`) keep their bases, and a TypeScript base built by a call (`extends mixin(Base)`) adds no edge to the function.
26
+ - A diff hunk that only inserts or only deletes lines is given to the declaration spanning both lines around it, at any context size; at `-U0` it was given to the line before it.
27
+
19
28
  ## [20.23.0] - 2026-10-13
20
29
 
21
30
  ### Added
package/docs/facts.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "Generated by pipeline/scripts/gen-facts.mjs. Do not hand-edit: the site reads this, and a number edited here instead of at its source is the drift this file removes.",
3
3
  "generatedAt": "2026-10-13",
4
- "version": "20.23.0",
4
+ "version": "20.23.1",
5
5
  "phaseSchema": 2,
6
6
  "phases": [
7
7
  {
package/manifest.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.23.0",
4
- "commit": "a64fcccc1e81e70293ec18d96f00b1bfc861dbe0",
5
- "generatedAt": "2026-10-13T11:40:10.540Z",
3
+ "version": "20.23.1",
4
+ "commit": "202735c51f3f1ba48c52d2faff53b4ebca2ce0e9",
5
+ "generatedAt": "2026-10-13T12:42:14.108Z",
6
6
  "algorithm": "sha256",
7
7
  "fileCount": 1367,
8
8
  "files": {
9
- "CHANGELOG.md": "49fd9096d6707758216930437150dcf42cdd5d38ff33019a2bdfef97311782af",
9
+ "CHANGELOG.md": "4989a15706727f4056d2f6008d9fb214369b3d9d9437dfcad867e79bedfcd798",
10
10
  "LICENSE": "8d8c51aaf1e1538b180c69965fa038db71be86798cc3e26a143bebfefe16a43e",
11
11
  "README.md": "4cb9fee0aed62bd1467bdc09954e011612b0de6e7895d663f297bc8778e00b2e",
12
12
  "README.tr.md": "f30edbc26e51e311077fbb5b64234e70285db92340e6dadc259cfd673f104326",
@@ -34,7 +34,7 @@
34
34
  "docs/best-practices.md": "f8b75b0d38b5896a6a852bb30a1edddeb24dab8b5b7b1d9313290e1241c72024",
35
35
  "docs/ecosystem.md": "328a56a9f48514bca380d311a8e62246d85ccb18668f873047a883596153395f",
36
36
  "docs/engineering.md": "550528cd47c7d2c921729c68d0782a00ac8e77a4bdfec479f6749f9c3235d188",
37
- "docs/facts.json": "64736478061d3d3af245acd7bc6695e3d96a31910da64622f597bc5831b1ba51",
37
+ "docs/facts.json": "0f668242196f9e4ad1fe68871118a8e28da6a9d2598228b2074f1b6760facdcc",
38
38
  "docs/features.md": "79a4b5a9722375ea16d7b337148d47bbea2951c8eb7bc8a6e616c9b3ed2c8703",
39
39
  "docs/performance.md": "bbf2f3922c25dcba0c1a9e69ef420e3871a729af5b35009a49b48a5d3c5cd151",
40
40
  "docs/recovery-guide.md": "59a8f1b66b34cb9ff5507b3d54dac3b9915e184aa044d9c758d18cac818d510e",
@@ -65,7 +65,7 @@
65
65
  "install/templates/multi-agent-autopilot-awake.plist.template": "ddd25c76c56fcbb38a89de8e35637372d70ace993d6bc487a88ca9cfb702fe1f",
66
66
  "install/templates/multi-agent-autopilot.plist.template": "122c8a61cd927edd99375d6e3da40f1d42bcceed5b6641785c8b28bb930bc77e",
67
67
  "install/unattended-profile-legacy.json": "f52e86b998956f09ad306b1735a6c10b58580d7082d8a55665a4c6bb15c3aa1f",
68
- "package.json": "bc6e1a7d54eb993ef1db3ded8880bc52748f5a4622145c26d5a0311ffd4ff7e1",
68
+ "package.json": "9378b09fd1cc14fce2d55dfc9d0e607728bcbcf09aee276058e760702b7d3b82",
69
69
  "pipeline/agents/android-architect.md": "11fb74543cb0ebcf9525ca6654fe95d53da7229e0bf4a1149b31fd9005abc453",
70
70
  "pipeline/agents/backend-architect.md": "b02a606c301e4714131aed70ae6d16eeb77fbc819a001e2752cff2488d4246de",
71
71
  "pipeline/agents/bulk-reader.md": "6597052cd7d205bac2b1c7633f5232fe1a40730664a913b8eb77efd5a786dc25",
@@ -169,7 +169,7 @@
169
169
  "pipeline/lib/_jira-auth.sh": "fc468736057f8841d824eb1a4db527442562f8979fea333ebaef5527f4369e9f",
170
170
  "pipeline/lib/account-resolver.sh": "f7bef9beba3f839268509361651be0b1e1f319f2f23821f90ba466fdf185a8f6",
171
171
  "pipeline/lib/agent-model-sync.mjs": "2e0264ca8d8fef761563db219d588fed9067847043f960f68f055c186c20b668",
172
- "pipeline/lib/analysis-jira-write.sh": "d9ad4065d8675db42f3f6248df992842e6f73fa6e5d571dfc5326ba9d37283c6",
172
+ "pipeline/lib/analysis-jira-write.sh": "d38a7665c91a4b65b219381184357a723a43c3daff8fae0bace6f55605f8e5bf",
173
173
  "pipeline/lib/analysis-quality.mjs": "58dec3447178ff05dc0687c256fa5134c909cd485dd22a0932eafc05ba5c9ad8",
174
174
  "pipeline/lib/analysis-sections.mjs": "e31ddf128ef7a6d1b9061896fc2d7cb4b5165202d8e03a46a1e0f9ec906ed01d",
175
175
  "pipeline/lib/ask-choice.sh": "b77282645cf64d016cdebf43d1ee299874a1411f5c51b6ae3b6c580d1bbfe78a",
@@ -282,7 +282,7 @@
282
282
  "pipeline/multi-agent-refs/design-check/figma-mapping.md": "731b7f19ddee2e67ea5d318e95e3a0d6840fa62c4ad682a25315509ac9ddcbc3",
283
283
  "pipeline/multi-agent-refs/design-check/init-inventory.md": "62defc5ea804f6c8f059a8d512b023791daac59274c0def28cefd7709a7fe862",
284
284
  "pipeline/multi-agent-refs/design-check/mcp-currency-gate.md": "70a96d3135af905989cd1819e6fb37fc44d25df9817b1efcd20121ec6db76d74",
285
- "pipeline/multi-agent-refs/features/analysis-jira.md": "5f6932d2d675c4f7812c6d4c44a8de1bd4c6b07d2ff92a05d8747e811d0f22ad",
285
+ "pipeline/multi-agent-refs/features/analysis-jira.md": "fe95a8a2435c0118f804f6ad17ab312d04ffa2ea7e0eab9e00d84c684039c93b",
286
286
  "pipeline/multi-agent-refs/features/analysis-outline.md": "36a917343a7c07ae115aa1e89f825304e5878db931915aefef614d251b167e92",
287
287
  "pipeline/multi-agent-refs/features/analysis-sources.md": "12c8035e6aabf8e8a973b7264edc9a943e6758e79dbc53ae3e9b43c7f998000c",
288
288
  "pipeline/multi-agent-refs/features/assertion-judge.md": "ee9d1f32a5edfc35bf7f97126fa285eaddad6c1ba99724149bcf414c6e318e00",
@@ -458,7 +458,7 @@
458
458
  "pipeline/schemas/plan-todos.schema.json": "f1dcf8df338b29bbc1d874addd3408c0841a5f869c0b6075efd9242e3e8552d6",
459
459
  "pipeline/schemas/planning-output.schema.json": "bc6eda32ecd435cf84d846078f9d38347f8f741f8a427cff15913dd6c380d4b8",
460
460
  "pipeline/schemas/pr-request.schema.json": "ae75c4856349d62834802b1189af516bb6fb3d36646831b7ff2fa61d938afcd0",
461
- "pipeline/schemas/prefs.schema.json": "009d3f1c9b5dfbbcdc5fd38b2edbea12ec8dead9ab116557288db12a8e44cd46",
461
+ "pipeline/schemas/prefs.schema.json": "57701b646d651f6ec6bb3a688da1ad2c3c11bd63a28cf0174b78b717ad2cae62",
462
462
  "pipeline/schemas/repo-profile.schema.json": "1a74ef8330da9a26a5a70955f6f9cbb20eda54a8615efaf86c7993d420a7f04e",
463
463
  "pipeline/schemas/repos.schema.json": "a9bf56f25c66201c170912e3389bd73e4a3f61922410f83e31a6c6ba47aee861",
464
464
  "pipeline/schemas/research-output.schema.json": "54e3ba1b04fdc20fbaa84333b943a8ab95d1395c7b0e0aedc67929184cb49523",
@@ -496,7 +496,7 @@
496
496
  "pipeline/scripts/_code-graph.mjs": "6b1848d78cef2000c11fcaaa6f1faaae89365a86a37642aa458db9ad422a4cc1",
497
497
  "pipeline/scripts/_command-contract.mjs": "e97a5c624e0768769da54d4175e9f7655f1de1dcd0832468bc66bf140b0f5a6f",
498
498
  "pipeline/scripts/_cost.mjs": "bdf03835af4719b72e4574f2d5ca827e94f9d80e49527a8e9e200a0aa4ecd7a9",
499
- "pipeline/scripts/_diff-impact.mjs": "fc61b34d6495ae8576ecccf1cf1e4effac2d025644259f30bfc46fafe5de264a",
499
+ "pipeline/scripts/_diff-impact.mjs": "cda016ca6071c741a680a3dbc35bb21926465b296defa4fa3677ce550704198a",
500
500
  "pipeline/scripts/_fingerprint.mjs": "d8c63b062a3be0e138b6675aa031824fcd96f5517cf0f7b477378913c1db7d73",
501
501
  "pipeline/scripts/_notices.mjs": "1f78037b0849ab3cf8a85ca353ed1610c2895250ac21a1d50ac1fb9732be4300",
502
502
  "pipeline/scripts/_path-match.mjs": "880b27d90351441b4f3c05e1a87ace62cd1caab3ec7a286663efb2176cb69a69",
@@ -508,14 +508,14 @@
508
508
  "pipeline/scripts/_smoke-root.sh": "dad129959a4e37a0cd0f4f6c04f6d66c5a43752b2bf33989db9c002eb106b832",
509
509
  "pipeline/scripts/_stack-adapter.mjs": "a754c042d237cd03abf11c6909099a7a3ea798cdd13d49b03787797fd3496cd7",
510
510
  "pipeline/scripts/_stack-routing.mjs": "d1f804d457295e5cc8ad7039e8c29431f0e9f52f4396cc54c1305eb82b9b61e9",
511
- "pipeline/scripts/_supertypes.mjs": "d31af077ee670dc68c25dca49b332ebefe44f1ccba3d6d5bd1f534c72609084f",
511
+ "pipeline/scripts/_supertypes.mjs": "7bf4bf8fca087ad93a19316fd24bfaae19d05f8a0907edda4c17946fcaf375d8",
512
512
  "pipeline/scripts/agent-guard.py": "ba8f0c08a80d953d339ea69492efb1fbe8db5d364c2bcb2094a9839cf6dcc081",
513
513
  "pipeline/scripts/agent-guard.sh": "5b57cea687754533c673b66a9dd2281ca06d12ef86bdc0d03fa285fc9e7debf8",
514
514
  "pipeline/scripts/aggregate-metrics.mjs": "c324e349993fb154a885263cf6665af0ff656e9c987b455a263dad8b96cca667",
515
515
  "pipeline/scripts/analysis-conform-coverage.mjs": "2b7de99855d1901b1b7803f47d4bb5f75ed5df1b5f4840d55bfc3537e3f65afe",
516
516
  "pipeline/scripts/analysis-sources.mjs": "53d843b0504a6cadd7d4a047331ebc34a2868304fcf791617ad0a215396e35a6",
517
517
  "pipeline/scripts/analysis-story-body.mjs": "309cf9f1a9859228d073aff6e7b4a9322bc960d4fe1a21a10ce58521a0c1b641",
518
- "pipeline/scripts/analysis-story-tree.mjs": "6c14d66b9718e0b68a7020058cbaeaf2f38f0ea3b1ee72ffa11a0878d8e38f44",
518
+ "pipeline/scripts/analysis-story-tree.mjs": "5fb7b5978391b1a090db26dae982bb9763177736c2457180a941946d40eb4224",
519
519
  "pipeline/scripts/analysis-tickets-writeback.mjs": "8c1f117ac5c6516385e3458462de6bde28c81768853c2d9524118d0155befe8b",
520
520
  "pipeline/scripts/anonymize-findings.mjs": "5d7e042793790c1b924d4d952b4f79b7e5f1dd0a3fe936f1ee16a4fa2ba820dd",
521
521
  "pipeline/scripts/answer-question.mjs": "b948df9d063f623a7ccd9cc65903c4388018b6b968228f77eb2e229223a61b1c",
@@ -545,7 +545,7 @@
545
545
  "pipeline/scripts/classify-plan-safety.mjs": "d25f21da21dd4ee1ea507fb0df89301715071121b1bd96aca9ef140a4eee896b",
546
546
  "pipeline/scripts/code-graph-rules/android.json": "6ba2cc0d496f5c6cc40cca9931e0903da3a64596d939fe2e86367615ec6241c2",
547
547
  "pipeline/scripts/code-graph-rules/go.json": "7d794157675c0d4c71d7ba3d6e538de0e20de6711c652d55844eac11b10e0bdf",
548
- "pipeline/scripts/code-graph-rules/ios.json": "097cb95842ddbe4ed66c2fee3f87ce3be319eaee79533ee46ba917978ddcd8cd",
548
+ "pipeline/scripts/code-graph-rules/ios.json": "b2c8acc464158b29c669050e7f13cf1f3445fcc2e8b8fb5c5da80feea78f9d2d",
549
549
  "pipeline/scripts/code-graph-rules/node.json": "062d7337519f5a743da6953d2748e95560729715502a665d19025108ceba4845",
550
550
  "pipeline/scripts/code-graph-rules/python.json": "91394dc31a9392e8d02cf46797bf39551fbe483aa2e6b34145cd33aa2460eca9",
551
551
  "pipeline/scripts/commands.mjs": "5b20a7d01aa0f205f2605fcfcbdb197bdfeafbdfc1e5164b5ee083ac6ff39cc2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.23.0",
3
+ "version": "20.23.1",
4
4
  "description": "6-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -239,6 +239,25 @@ if [ -n "${EPIC:-}" ]; then
239
239
  fi
240
240
  fi
241
241
 
242
+ # Discover rather than assume: the types this site marks subtask:true, under
243
+ # whatever names it gave them. Jira Data Center 9 answers only the per-project
244
+ # createmeta; older sites answer only the classic one. Read once per run.
245
+ SITE_SUBTASK_TYPES=""
246
+ SITE_SUBTASK_READ=0
247
+ site_subtask_types() {
248
+ SITE_SUBTASK_READ=1
249
+ SITE_SUBTASK_TYPES="$(jira_api GET "/rest/api/2/issue/createmeta/${PROJECT}/issuetypes?maxResults=100" 2>/dev/null \
250
+ | jq -r '.values[]? | select(.subtask == true) | .name' 2>/dev/null || echo "")"
251
+ [ -n "$SITE_SUBTASK_TYPES" ] || SITE_SUBTASK_TYPES="$(jira_api GET \
252
+ "/rest/api/2/issue/createmeta?projectKeys=${PROJECT}&expand=projects.issuetypes" 2>/dev/null \
253
+ | jq -r '.projects[]?.issuetypes[]? | select(.subtask == true) | .name' 2>/dev/null || echo "")"
254
+ local n
255
+ n="$(printf '%s' "$SITE_SUBTASK_TYPES" | grep -c . || true)"
256
+ if [ "${n:-0}" -gt 1 ]; then
257
+ echo " note ${PROJECT} has ${n} sub-task types ($(printf '%s' "$SITE_SUBTASK_TYPES" | paste -sd, - | sed 's/,/, /g')); using the first. Set issueTree.subtaskIssueType to a name, or to a {role: type} map, to choose." >&2
258
+ fi
259
+ }
260
+
242
261
  RC=0
243
262
  NODES="$(jq -c '.nodes[]' "$PLAN")"
244
263
  while IFS= read -r node; do
@@ -272,12 +291,13 @@ while IFS= read -r node; do
272
291
  [ -n "$sub" ] || continue
273
292
  t_label="$(printf '%s' "$sub" | jq -r '.label')"
274
293
  t_role="$(printf '%s' "$sub" | jq -r '.role')"
275
- t_type="$(jq -r '.prefsUsed.subtaskIssueType // empty' "$PLAN")"
294
+ # A name, or a map from role to name ("*" for any other role).
295
+ t_type="$(jq -r --arg r "$t_role" '.prefsUsed.subtaskIssueType
296
+ | if type == "object" then (.[$r] // .["*"] // empty)
297
+ elif type == "string" then . else empty end' "$PLAN")"
276
298
  if [ -z "$t_type" ] && [ "$DRY" -eq 0 ]; then
277
- # Discover rather than assume: whichever type this site marks subtask:true,
278
- # under whatever name the site gave it.
279
- t_type="$(jira_api GET "/rest/api/2/issue/createmeta?projectKeys=${PROJECT}&expand=projects.issuetypes" \
280
- | jq -r '[.projects[]?.issuetypes[]? | select(.subtask==true) | .name] | first // empty' 2>/dev/null || echo "")"
299
+ [ "$SITE_SUBTASK_READ" -eq 1 ] || site_subtask_types
300
+ t_type="$(printf '%s\n' "$SITE_SUBTASK_TYPES" | head -n 1)"
281
301
  fi
282
302
  [ -n "$t_type" ] || t_type="Sub-task"
283
303
  t_desc="$(printf '%s' "$sub" | jq -r '.description // empty')"
@@ -173,9 +173,15 @@ are values the site fills in:
173
173
  - `epicLinkFieldId`, when set, is the site's own field id; otherwise it is
174
174
  discovered
175
175
  - `subtaskIssueType: null` means discover it - `createmeta` returns whichever
176
- type carries `subtask: true`, under whatever name the site gave it. Same rule
177
- as `features/jira-context.md`: the type travels as it comes from Jira and is
176
+ type carries `subtask: true`, under whatever name the site gave it (the
177
+ per-project `createmeta/{project}/issuetypes` first, which is all Jira Data
178
+ Center 9 answers, then the classic form). Same rule as
179
+ `features/jira-context.md`: the type travels as it comes from Jira and is
178
180
  never a matching criterion in code
181
+ - a site with several sub-task types (Analysis Task, Development Task, Test
182
+ Task) names them per role: `subtaskIssueType` as a map from a `subtaskRoles`
183
+ entry to the type, `"*"` for any other role. Without one the first discovered
184
+ type is used and the run prints the types it found
179
185
 
180
186
  The preview prints every field beside the pref key it came from, so a wrong
181
187
  setting is visible before the writes rather than in Jira afterwards.
@@ -2202,9 +2202,10 @@
2202
2202
  "default": "Story"
2203
2203
  },
2204
2204
  "subtaskIssueType": {
2205
- "type": ["string", "null"],
2205
+ "type": ["string", "null", "object"],
2206
+ "additionalProperties": { "type": "string" },
2206
2207
  "default": null,
2207
- "description": "Null means discover from createmeta rather than assume a name."
2208
+ "description": "The sub-task type name, or a map from a subtaskRoles entry to this site's type name (\"*\" for any other role), e.g. {\"develop\": \"Development Task\", \"test\": \"Test Task\"}. Null means discover from createmeta (the per-project endpoint, else the classic one) rather than assume a name; when the site has several sub-task types the first is used and the run says so."
2208
2209
  },
2209
2210
  "subtaskRoles": {
2210
2211
  "type": "array",
@@ -38,7 +38,7 @@ function addLine(set, line) {
38
38
  set.add(Math.max(1, line));
39
39
  }
40
40
 
41
- function toRanges(lines) {
41
+ function toRanges(lines, gaps = new Set()) {
42
42
  const sorted = [...lines].sort((a, b) => a - b);
43
43
  const out = [];
44
44
  for (const l of sorted) {
@@ -46,21 +46,28 @@ function toRanges(lines) {
46
46
  if (last && l === last.end + 1) last.end = l;
47
47
  else if (!last || l > last.end) out.push({ start: l, end: l });
48
48
  }
49
- return out;
49
+ // A gap is a point between two lines where lines went in (or, on the new
50
+ // side, came out): `{start: before, end: before + 1, gap: true}`.
51
+ for (const before of gaps)
52
+ if (!sorted.includes(before) && !sorted.includes(before + 1))
53
+ out.push({ start: before, end: before + 1, gap: true });
54
+ return out.sort((a, b) => a.start - b.start);
50
55
  }
51
56
 
52
57
  /**
53
58
  * Every file of a unified diff with the lines it touched on each side.
54
59
  *
55
- * New side: an added line counts as itself, and a deletion as the line now
56
- * standing where the removed lines were. Old side: a removed line counts as
57
- * itself, and an insertion as the old line standing where it went in. Either way a hunk that
58
- * only adds or only removes still lands inside the declaration it changed.
60
+ * New side: an added line counts as itself, and a pure deletion as the gap
61
+ * between the two new lines it fell between. Old side: a removed line counts as
62
+ * itself, and a pure insertion as the gap between the two old lines it went in
63
+ * between. A gap belongs to the innermost declaration spanning both of its
64
+ * lines, so a hunk that only adds or only removes lands inside the declaration
65
+ * it changed, whatever the context size.
59
66
  * Hunks are counted from their headers, so concatenated `diff -u` output with
60
67
  * no `diff --git` lines parses too, CRLF included, at any context size.
61
68
  *
62
69
  * @param {string} diffText - unified diff
63
- * @returns {Array<{oldPath: string|null, newPath: string|null, oldRanges: Array<{start: number, end: number}>, newRanges: Array<{start: number, end: number}>}>}
70
+ * @returns {Array<{oldPath: string|null, newPath: string|null, oldRanges: Array<{start: number, end: number, gap?: boolean}>, newRanges: Array<{start: number, end: number, gap?: boolean}>}>}
64
71
  */
65
72
  export function parseDiffFiles(diffText) {
66
73
  const files = [];
@@ -70,23 +77,36 @@ export function parseDiffFiles(diffText) {
70
77
  let newLine = 0;
71
78
  let oldLeft = 0;
72
79
  let newLeft = 0;
80
+ // One run of -/+ lines. Its removed lines stand for it on the old side and
81
+ // its added lines on the new side; a run with nothing on one side is a gap
82
+ // there, between the lines either side of it.
83
+ let run = null;
84
+ const endRun = () => {
85
+ if (run && !run.removed) cur.oldGaps.add(run.oldBefore);
86
+ if (run && !run.added) cur.newGaps.add(run.newBefore);
87
+ run = null;
88
+ };
73
89
  for (const line of String(diffText).split("\n")) {
74
90
  const raw = line.endsWith("\r") ? line.slice(0, -1) : line;
75
91
  if (oldLeft > 0 || newLeft > 0) {
76
- if (raw.startsWith("+")) {
77
- addLine(cur.newLines, newLine);
78
- addLine(cur.oldLines, oldLine);
79
- newLine++;
80
- newLeft--;
81
- continue;
82
- }
83
- if (raw.startsWith("-")) {
84
- addLine(cur.oldLines, oldLine);
85
- addLine(cur.newLines, newLine);
86
- oldLine++;
87
- oldLeft--;
92
+ if (raw.startsWith("+") || raw.startsWith("-")) {
93
+ run = run || { oldBefore: oldLine - 1, newBefore: newLine - 1, removed: 0, added: 0 };
94
+ if (raw.startsWith("+")) {
95
+ addLine(cur.newLines, newLine);
96
+ run.added++;
97
+ newLine++;
98
+ newLeft--;
99
+ } else {
100
+ addLine(cur.oldLines, oldLine);
101
+ run.removed++;
102
+ oldLine++;
103
+ oldLeft--;
104
+ }
105
+ if (oldLeft <= 0 && newLeft <= 0) endRun();
88
106
  continue;
89
107
  }
108
+ if (raw.startsWith("\\")) continue;
109
+ endRun();
90
110
  if (raw.startsWith(" ") || raw === "") {
91
111
  oldLine++;
92
112
  newLine++;
@@ -94,7 +114,6 @@ export function parseDiffFiles(diffText) {
94
114
  newLeft--;
95
115
  continue;
96
116
  }
97
- if (raw.startsWith("\\")) continue;
98
117
  oldLeft = 0;
99
118
  newLeft = 0;
100
119
  }
@@ -112,6 +131,8 @@ export function parseDiffFiles(diffText) {
112
131
  newPath: sidePath(raw.slice(4), "b/"),
113
132
  oldLines: new Set(),
114
133
  newLines: new Set(),
134
+ oldGaps: new Set(),
135
+ newGaps: new Set(),
115
136
  };
116
137
  files.push(cur);
117
138
  pendingOld = undefined;
@@ -121,12 +142,14 @@ export function parseDiffFiles(diffText) {
121
142
  if (hunk && cur) {
122
143
  const oldCount = hunk[2] === undefined ? 1 : Number(hunk[2]);
123
144
  const newCount = hunk[4] === undefined ? 1 : Number(hunk[4]);
124
- oldLine = Number(hunk[1]);
125
- newLine = Number(hunk[3]);
145
+ // An empty side names the line BEFORE the change; any other side names
146
+ // the change's first line.
147
+ oldLine = Number(hunk[1]) + (oldCount === 0 ? 1 : 0);
148
+ newLine = Number(hunk[3]) + (newCount === 0 ? 1 : 0);
126
149
  oldLeft = oldCount;
127
150
  newLeft = newCount;
128
- if (newCount === 0) addLine(cur.newLines, newLine);
129
- if (oldCount === 0) addLine(cur.oldLines, oldLine);
151
+ if (oldCount === 0) cur.oldGaps.add(oldLine - 1);
152
+ if (newCount === 0) cur.newGaps.add(newLine - 1);
130
153
  }
131
154
  }
132
155
  return files
@@ -134,8 +157,8 @@ export function parseDiffFiles(diffText) {
134
157
  .map((f) => ({
135
158
  oldPath: f.oldPath,
136
159
  newPath: f.newPath,
137
- oldRanges: toRanges(f.oldLines),
138
- newRanges: toRanges(f.newLines),
160
+ oldRanges: toRanges(f.oldLines, f.oldGaps),
161
+ newRanges: toRanges(f.newLines, f.newGaps),
139
162
  }));
140
163
  }
141
164
 
@@ -202,8 +225,13 @@ export function changedSymbols(graph, ranges) {
202
225
  continue;
203
226
  }
204
227
  for (const r of list) {
228
+ // A gap belongs to a declaration only when the lines on both sides do.
205
229
  const containing = spanned
206
- .filter((s) => (s.startLine ?? s.line) <= r.end && s.endLine >= r.start)
230
+ .filter((s) =>
231
+ r.gap
232
+ ? (s.startLine ?? s.line) <= r.start && s.endLine >= r.end
233
+ : (s.startLine ?? s.line) <= r.end && s.endLine >= r.start,
234
+ )
207
235
  .sort((a, b) => b.line - a.line);
208
236
  if (!containing.length) {
209
237
  seeds.set(fileNode.id, fileNode);
@@ -6,7 +6,7 @@
6
6
  * protocols or interfaces. Three syntaxes cover the stacks the graph indexes:
7
7
  *
8
8
  * colon Swift and Kotlin: `Name<T>(ctor): A, B<X>() where ... {`
9
- * parens Python: `Name(A, B, metaclass=M):`
9
+ * parens Python: `Name(A, B, metaclass=M):`, `Name[T](A):`
10
10
  * extends TypeScript / JavaScript: `Name<T> extends A implements B, C {`
11
11
  *
12
12
  * Only names that start with a letter or underscore are kept, qualified names
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const HEADER_MAX = 4000;
23
+ const CALL_RE = /^\s*[A-Za-z_$][\w$.]*\s*(?:<[^()]*>)?\s*\(/;
23
24
  const OPEN = { "(": ")", "<": ">", "[": "]" };
24
25
  const CLOSE = new Set([")", ">", "]"]);
25
26
 
@@ -99,10 +100,14 @@ function names(list) {
99
100
  export function supertypesAt(body, start, syntax) {
100
101
  const header = headerText(body, start, syntax);
101
102
  if (syntax === "parens") {
102
- const m = /^\s*\(/.exec(header);
103
+ let k = 0;
104
+ while (k < header.length && /\s/.test(header[k])) k++;
105
+ // PEP 695 type parameters come before the bases: `class Box[T](Base):`.
106
+ const rest = header.slice(skipGroup(header, k, "["));
107
+ const m = /^\s*\(/.exec(rest);
103
108
  if (!m) return [];
104
- const end = skipGroup(header, m[0].length - 1, "(");
105
- return names(header.slice(m[0].length, end - 1));
109
+ const end = skipGroup(rest, m[0].length - 1, "(");
110
+ return names(rest.slice(m[0].length, end - 1));
106
111
  }
107
112
  if (syntax === "extends") {
108
113
  let k = 0;
@@ -111,8 +116,16 @@ export function supertypesAt(body, start, syntax) {
111
116
  const out = [];
112
117
  const ext = /\bextends\s+([\s\S]*?)(?=\bimplements\b|$)/.exec(rest);
113
118
  const imp = /\bimplements\s+([\s\S]*)$/.exec(rest);
119
+ // A base built by a call (`extends mixin(Base)`) is the call's result,
120
+ // which names no type the graph can point at.
114
121
  for (const part of [ext?.[1], imp?.[1]])
115
- if (part) for (const n of names(part)) if (!out.includes(n)) out.push(n);
122
+ if (part)
123
+ for (const n of names(
124
+ splitTop(part)
125
+ .filter((p) => !CALL_RE.test(p))
126
+ .join(","),
127
+ ))
128
+ if (!out.includes(n)) out.push(n);
116
129
  return out;
117
130
  }
118
131
  let i = 0;
@@ -153,7 +153,7 @@ export function identityLabel(prefix, docId, sourceIds) {
153
153
  * issueTree.projectKey where the tree is created
154
154
  * issueTree.epicIssueType the type name for an epic on this site
155
155
  * issueTree.storyIssueType the type name for a story
156
- * issueTree.subtaskIssueType null = discover from createmeta
156
+ * issueTree.subtaskIssueType a name, a {role: name} map, or null = discover
157
157
  * issueTree.subtaskRoles how a story is split; ships empty on purpose
158
158
  * issueTree.channelComponents channel -> this site's component name
159
159
  * issueTree.channelTeams channel -> this site's team name
@@ -195,6 +195,17 @@ function readPrefs() {
195
195
  }
196
196
  }
197
197
 
198
+ /**
199
+ * The sub-task type a role is created as, from issueTree.subtaskIssueType: a
200
+ * name for every role, or a {role: name} map with "*" for any other role. Null
201
+ * means the writer discovers it from the site.
202
+ */
203
+ export function subtaskTypeFor(setting, role) {
204
+ if (typeof setting === "string" && setting) return setting;
205
+ if (setting && typeof setting === "object") return setting[role] ?? setting["*"] ?? null;
206
+ return null;
207
+ }
208
+
198
209
  /** Every open placeholder that makes a document unfit to become work. */
199
210
  export function openItems(text) {
200
211
  const found = [];
@@ -557,7 +568,10 @@ async function main() {
557
568
  for (const line of s.description.split("\n")) process.stdout.write(` ${line}\n`);
558
569
  }
559
570
  for (const t of s.subtasks) {
560
- process.stdout.write(` subtask ${t.role} [${t.label}]\n`);
571
+ const type = subtaskTypeFor(result.prefsUsed.subtaskIssueType, t.role);
572
+ process.stdout.write(
573
+ ` subtask ${t.role} type ${type ?? "discovered from createmeta"} [${t.label}]\n`,
574
+ );
561
575
  }
562
576
  for (const c of s.clones || []) {
563
577
  process.stdout.write(
@@ -47,7 +47,7 @@
47
47
  "importPatterns": [
48
48
  {
49
49
  "id": "import",
50
- "regex": "^\\s*import\\s+(?:class|struct|enum|protocol|func|var|let|typealias)\\s+([A-Za-z_][A-Za-z0-9_]*)|^\\s*import\\s+([A-Za-z_][A-Za-z0-9_]*)"
50
+ "regex": "^\\s*(?:@[A-Za-z_]\\w*(?:\\([^)\\n]*\\))?\\s+)*import\\s+(?:class|struct|enum|protocol|func|var|let|typealias)\\s+([A-Za-z_][A-Za-z0-9_]*)|^\\s*(?:@[A-Za-z_]\\w*(?:\\([^)\\n]*\\))?\\s+)*import\\s+([A-Za-z_][A-Za-z0-9_]*)"
51
51
  }
52
52
  ],
53
53
  "referenceKinds": ["class", "struct", "enum", "protocol", "actor", "typealias"],