kd-cli 2.6.5__tar.gz → 2.7.0__tar.gz

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 (86) hide show
  1. {kd_cli-2.6.5/kd_cli.egg-info → kd_cli-2.7.0}/PKG-INFO +1 -1
  2. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/articles.py +16 -2
  3. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cache.py +91 -6
  4. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/article/__init__.py +59 -6
  5. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/cache/__init__.py +5 -2
  6. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/primer/__init__.py +397 -13
  7. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/output.py +34 -3
  8. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/client.py +41 -0
  9. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/primer.py +176 -2
  10. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/CHANGELOG.md +50 -0
  11. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/operating-guide.md +37 -7
  12. {kd_cli-2.6.5 → kd_cli-2.7.0/kd_cli.egg-info}/PKG-INFO +1 -1
  13. {kd_cli-2.6.5 → kd_cli-2.7.0}/pyproject.toml +1 -1
  14. {kd_cli-2.6.5 → kd_cli-2.7.0}/LICENSE +0 -0
  15. {kd_cli-2.6.5 → kd_cli-2.7.0}/MANIFEST.in +0 -0
  16. {kd_cli-2.6.5 → kd_cli-2.7.0}/README.md +0 -0
  17. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/__init__.py +0 -0
  18. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/__init__.py +0 -0
  19. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/http.py +0 -0
  20. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/hub.py +0 -0
  21. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/saved_queries.py +0 -0
  22. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/tags.py +0 -0
  23. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/youtrack.py +0 -0
  24. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/article_marker.py +0 -0
  25. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/auth.py +0 -0
  26. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/__init__.py +0 -0
  27. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/__init__.py +0 -0
  28. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/command_base.py +0 -0
  29. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/command_parser.py +0 -0
  30. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/confirm.py +0 -0
  31. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/namespaces.py +0 -0
  32. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/option_registry.py +0 -0
  33. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/input.py +0 -0
  34. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/main.py +0 -0
  35. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/__init__.py +0 -0
  36. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/alias/__init__.py +0 -0
  37. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/init.py +0 -0
  38. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/issue/__init__.py +0 -0
  39. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/project/__init__.py +0 -0
  40. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/query/__init__.py +0 -0
  41. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/release_notes.py +0 -0
  42. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/search/__init__.py +0 -0
  43. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/system/__init__.py +0 -0
  44. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/tag/__init__.py +0 -0
  45. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/time/__init__.py +0 -0
  46. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/user/__init__.py +0 -0
  47. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/tree.py +0 -0
  48. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/config.py +0 -0
  49. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/config_read.py +0 -0
  50. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/config_schema.py +0 -0
  51. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/connection.py +0 -0
  52. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/debug.py +0 -0
  53. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/exceptions.py +0 -0
  54. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/history.py +0 -0
  55. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/locking.py +0 -0
  56. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/__init__.py +0 -0
  57. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/article.py +0 -0
  58. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/assist.py +0 -0
  59. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/attachment.py +0 -0
  60. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/comment.py +0 -0
  61. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/common.py +0 -0
  62. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/issue.py +0 -0
  63. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/project.py +0 -0
  64. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/saved_query.py +0 -0
  65. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/search.py +0 -0
  66. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/tag.py +0 -0
  67. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/tag_entities.py +0 -0
  68. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/user.py +0 -0
  69. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/work_item.py +0 -0
  70. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/operating_guide.py +0 -0
  71. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/query_parser.py +0 -0
  72. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/release_notes.py +0 -0
  73. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/__init__.py +0 -0
  74. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/command-synonyms.yaml +0 -0
  75. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/config-template.yaml +0 -0
  76. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/error-messages.yaml +0 -0
  77. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/primers/__init__.py +0 -0
  78. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/primers/hitl.yaml +0 -0
  79. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/sdlc-doctrine.md +0 -0
  80. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/sdlc.py +0 -0
  81. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/SOURCES.txt +0 -0
  82. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/dependency_links.txt +0 -0
  83. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/entry_points.txt +0 -0
  84. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/requires.txt +0 -0
  85. {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/top_level.txt +0 -0
  86. {kd_cli-2.6.5 → kd_cli-2.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.6.5
3
+ Version: 2.7.0
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -1,12 +1,16 @@
1
1
  """Articles REST API wrapper.
2
2
 
3
3
  Covers ``/api/articles`` endpoints:
4
- - Articles: list (global + project-scoped), get, create, update, delete
4
+ - Articles: list (global + project-scoped), search, get, create, update, delete
5
5
  - Child articles: list, attach, detach parent
6
6
  - Comments: list, create
7
7
  - Attachments: list, get, create, delete
8
8
 
9
- Articles have no ``query`` parameter — filtering is project-scoped only.
9
+ The ``query`` parameter on ``/api/articles`` is undocumented in the
10
+ public REST reference but probed-and-supported on the YT tiers kd
11
+ targets (current cloud + on-prem 2023.2); ``search_articles`` is the
12
+ canonical full-text path. Servers that reject it surface as a 400 with
13
+ "unknown parameter" wording, classified at the SDK boundary.
10
14
  """
11
15
 
12
16
  from __future__ import annotations
@@ -103,6 +107,16 @@ class ArticlesAPI:
103
107
  )
104
108
  return [Article.model_validate(a) for a in data]
105
109
 
110
+ # --- Search ---
111
+
112
+ def search_articles(self, query: str, top: int = 50, skip: int = 0) -> list[Article]:
113
+ data = self._http.get(
114
+ "/articles",
115
+ fields=_ARTICLE_LIST_FIELDS,
116
+ params={"query": query, "$top": top, "$skip": skip},
117
+ )
118
+ return [Article.model_validate(a) for a in data]
119
+
106
120
  # --- Tag scan (articles with tags for client-side filtering) ---
107
121
 
108
122
  def list_articles_with_tags(self, top: int = 50, skip: int = 0) -> list[Article]:
@@ -30,7 +30,14 @@ from kd.connection import ConnectionContext
30
30
  from kd.exceptions import APIError, ConfigError, ValidationError
31
31
  from kd.history import atomic_write_text
32
32
  from kd.models.tag_entities import TagEntitiesResult, tag_entities_result_to_dict
33
- from kd.primer import PRIMER_FILENAME, PrimerArticle, PrimerTag, load_primer, require_workspace_dir
33
+ from kd.primer import (
34
+ PRIMER_FILENAME,
35
+ PrimerArticle,
36
+ PrimerIssue,
37
+ PrimerTag,
38
+ load_primer,
39
+ require_workspace_dir,
40
+ )
34
41
  from kd.query_parser import youtrack_query_literal
35
42
 
36
43
  # Public surface: names imported by external callers (CLI, client) and the
@@ -90,6 +97,7 @@ __all__ = [
90
97
  "HydratedQuery",
91
98
  "HydratedQueryRun",
92
99
  "IssueRefreshReport",
100
+ "IssueScopeRefreshReport",
93
101
  "MilestoneRefreshReport",
94
102
  "QueryNameRefreshReport",
95
103
  "QueryRefreshReport",
@@ -285,11 +293,26 @@ class QueryRefreshReport:
285
293
  failed: list[FailedQuery]
286
294
 
287
295
 
296
+ @dataclass
297
+ class IssueScopeRefreshReport:
298
+ """Per-pinned-issue half of :class:`RefreshReport`.
299
+
300
+ Parallels :class:`ArticleRefreshReport`. Reuses :class:`HydratedIssue`
301
+ and :class:`FailedIssue` from the single-issue refresh path, with the
302
+ same per-id isolation contract.
303
+ """
304
+
305
+ skipped_no_pinned: bool
306
+ hydrated: list[HydratedIssue]
307
+ failed: list[FailedIssue]
308
+
309
+
288
310
  @dataclass
289
311
  class RefreshReport:
290
312
  """Result of a single ``refresh_primer_scope`` invocation."""
291
313
 
292
314
  articles: ArticleRefreshReport
315
+ issues: IssueScopeRefreshReport
293
316
  queries: QueryRefreshReport
294
317
 
295
318
 
@@ -1881,6 +1904,57 @@ def _refresh_primer_articles(
1881
1904
  return ArticleRefreshReport(skipped_no_pinned=False, hydrated=hydrated, failed=failed)
1882
1905
 
1883
1906
 
1907
+ def _refresh_primer_issues(
1908
+ client: Any,
1909
+ pinned: list[PrimerIssue],
1910
+ fetched_at: datetime,
1911
+ cache_root: Path,
1912
+ ) -> IssueScopeRefreshReport:
1913
+ """Hydrate each pinned issue into ``cache_root/issues/``.
1914
+
1915
+ Mirrors :func:`_refresh_primer_articles`. Per-id ``APIError`` (and
1916
+ subclasses) are isolated into ``failed``; ``AuthError`` and
1917
+ ``ConfigError`` propagate as command-level failures and abort the
1918
+ run. Issues are fetched with ``detail="rich"`` so the cache payload
1919
+ matches the single-issue refresh shape (proposal-39 §Issue Cache
1920
+ Shape); ``include_comments`` stays off — the primer-scope contract
1921
+ is "what does the agent need to start work," not "full comment log."
1922
+ """
1923
+ if not pinned:
1924
+ return IssueScopeRefreshReport(skipped_no_pinned=True, hydrated=[], failed=[])
1925
+
1926
+ hydrated: list[HydratedIssue] = []
1927
+ failed: list[FailedIssue] = []
1928
+ for entry in pinned:
1929
+ try:
1930
+ issue = client.issues.get(entry.id, detail="rich")
1931
+ payload = _issue_to_cache_payload(
1932
+ issue,
1933
+ fetched_at=fetched_at,
1934
+ include_comments=False,
1935
+ command=PRIMER_SCOPE_REFRESH_COMMAND,
1936
+ reason="primer pinned issue",
1937
+ )
1938
+ write_cached_issue(
1939
+ issue.id_readable,
1940
+ payload,
1941
+ fetched_at=fetched_at,
1942
+ cache_root=cache_root,
1943
+ )
1944
+ hydrated.append(
1945
+ HydratedIssue(
1946
+ id=issue.id_readable,
1947
+ summary=issue.summary,
1948
+ upstream_updated=_ms_to_iso(issue.updated),
1949
+ path=_report_path(cache_root / ISSUES_DIRNAME / f"{issue.id_readable}.yaml"),
1950
+ comments_count=None,
1951
+ )
1952
+ )
1953
+ except APIError as e:
1954
+ failed.append(FailedIssue(id=entry.id, reason=f"{type(e).__name__}: {e}"))
1955
+ return IssueScopeRefreshReport(skipped_no_pinned=False, hydrated=hydrated, failed=failed)
1956
+
1957
+
1884
1958
  def _refresh_primer_query(
1885
1959
  client: Any,
1886
1960
  pinned_tags: list[PrimerTag],
@@ -1962,17 +2036,23 @@ def refresh_primer_scope(
1962
2036
  root = connection_cache_dir(connection, workspace_dir.parent)
1963
2037
  when = fetched_at or datetime.now(timezone.utc)
1964
2038
 
1965
- if not primer.articles and not primer.tags:
2039
+ if not primer.articles and not primer.tags and not primer.issues:
1966
2040
  raise ValidationError(
1967
- "--primer-scope set but .kd/primer.yaml has no pinned articles or tags. "
1968
- "Pin a tag with: kd /primer/tag add TAG_NAME, or pin an article with: "
2041
+ "--primer-scope set but .kd/primer.yaml has no pinned articles, tags, "
2042
+ "or issues. Pin a tag with: kd /primer/tag add TAG_NAME, an issue "
2043
+ "with: kd /primer/issue add ISSUE-ID, or an article with: "
1969
2044
  "kd /primer/article add ARTICLE-ID"
1970
2045
  )
1971
2046
 
1972
2047
  write_scope_manifest(connection, cache_root=root, fetched_at=when)
1973
2048
  article_report = _refresh_primer_articles(client, primer.articles, when, root)
2049
+ issue_report = _refresh_primer_issues(client, primer.issues, when, root)
1974
2050
  query_report = _refresh_primer_query(client, primer.tags, when, root)
1975
- return RefreshReport(articles=article_report, queries=query_report)
2051
+ return RefreshReport(
2052
+ articles=article_report,
2053
+ issues=issue_report,
2054
+ queries=query_report,
2055
+ )
1976
2056
 
1977
2057
 
1978
2058
  # ---------------------------------------------------------------------------
@@ -2027,6 +2107,7 @@ def _issue_to_cache_payload(
2027
2107
  fetched_at: datetime,
2028
2108
  include_comments: bool,
2029
2109
  command: str,
2110
+ reason: str = "single-entity refresh",
2030
2111
  ) -> dict[str, Any]:
2031
2112
  """Build the proposal-39 §Issue Cache Shape dict from an SDK ``Issue``.
2032
2113
 
@@ -2037,6 +2118,10 @@ def _issue_to_cache_payload(
2037
2118
  :func:`_project_key_from_id`. ``links`` and ``attachments`` are
2038
2119
  deliberately not projected (deferred to S7+).
2039
2120
 
2121
+ ``reason`` defaults to ``"single-entity refresh"`` for the existing
2122
+ single-issue refresh path; primer-scope refresh passes
2123
+ ``"primer pinned issue"`` to record why the entry was hydrated.
2124
+
2040
2125
  Not shared with ``kd/cli/namespaces/issue/__init__.py:_issue_to_detail``
2041
2126
  — that helper truncates comments to the last 5 (display logic),
2042
2127
  while cache projection must include all comments when
@@ -2053,7 +2138,7 @@ def _issue_to_cache_payload(
2053
2138
  "fetched_at": _format_iso(fetched_at),
2054
2139
  "source": {
2055
2140
  "command": command,
2056
- "reason": "single-entity refresh",
2141
+ "reason": reason,
2057
2142
  },
2058
2143
  }
2059
2144
  for cf in issue.custom_fields:
@@ -1,6 +1,6 @@
1
1
  """Article namespace: /article
2
2
 
3
- Commands: list, show, children, create, update, delete, tree, move
3
+ Commands: list, search, show, pull, push, children, create, update, delete, tree, move
4
4
  Sub-namespaces: /article/comment (list, add), /article/attachment (list, add, show, download, delete)
5
5
  API layer: YouTrack REST API (/api/articles)
6
6
  """
@@ -81,17 +81,19 @@ class ArticleListCommand(Command):
81
81
  requires_client=True,
82
82
  usage="kd /article list [--project P] [--parent ID] [--limit N]",
83
83
  long_description=(
84
- "Articles do not support search queries. Use --project to filter "
85
- "by project, or --parent ARTICLE_ID to list direct children of "
86
- "another article. When --parent is given, --project is ignored "
87
- "because child listing is already scoped to the parent's project."
84
+ "List articles structurally — by project (--project) or by "
85
+ "parent (--parent ARTICLE_ID). For full-text / body / "
86
+ "field-bearing queries use `/article search`; this command "
87
+ "does not interpret query syntax. When --parent is given, "
88
+ "--project is ignored because child listing is already "
89
+ "scoped to the parent's project."
88
90
  ),
89
91
  examples=[
90
92
  "kd /article list --project FB",
91
93
  "kd /article list --project FB --limit 10",
92
94
  "kd /article list --parent FB-A-1",
93
95
  ],
94
- see_also=["/article show", "/article children"],
96
+ see_also=["/article show", "/article children", "/article search"],
95
97
  )
96
98
 
97
99
  def _register_options(self) -> None:
@@ -118,6 +120,56 @@ class ArticleListCommand(Command):
118
120
  return [_article_to_row(a) for a in articles]
119
121
 
120
122
 
123
+ class ArticleSearchCommand(Command):
124
+ """Server-side article full-text search via the YT article query DSL."""
125
+
126
+ def __init__(self) -> None:
127
+ super().__init__(
128
+ "search",
129
+ "Search articles by full-text query",
130
+ requires_client=True,
131
+ usage="kd /article search QUERY [--limit N] [--skip N]",
132
+ long_description=(
133
+ "Run a YouTrack article query against /api/articles?query=. "
134
+ "Matches body, summary, tags, and other YT-indexed fields; "
135
+ "results are server-side relevance-ranked. The upstream "
136
+ "query parameter is undocumented; servers that reject it "
137
+ "surface a specific capability error rather than silently "
138
+ "falling back to local substring search. See the article "
139
+ "search how-to for query syntax and field filters."
140
+ ),
141
+ examples=[
142
+ 'kd /article search "project: {Code OnTime} {wix}"',
143
+ 'kd /article search "project: {Intellect Solutions} content: HKEY_LOCAL_MACHINE"',
144
+ 'kd /article search "summary: timeout" --limit 10',
145
+ ],
146
+ see_also=["/article list", "/search", "/tag articles"],
147
+ )
148
+
149
+ def _register_options(self) -> None:
150
+ self.register_option(limit_option())
151
+ self.register_option(
152
+ OptionDeclaration(
153
+ name="skip",
154
+ layer=OptionLayer.COMMAND,
155
+ type=int,
156
+ default=0,
157
+ help_text="Skip the first N results (pagination).",
158
+ )
159
+ )
160
+
161
+ def execute(
162
+ self, args: list[str], options: ResolvedOptions, client: Any = None
163
+ ) -> CommandResult:
164
+ query = " ".join(args).strip()
165
+ if not query:
166
+ raise ValidationError("Usage: kd /article search QUERY")
167
+ limit = options.command.get("limit", 50)
168
+ skip = options.command.get("skip", 0)
169
+ articles = client.articles.search(query, top=limit, skip=skip)
170
+ return [_article_to_row(a) for a in articles]
171
+
172
+
121
173
  class ArticleShowCommand(Command):
122
174
  """Show article details."""
123
175
 
@@ -1058,6 +1110,7 @@ def create_article_namespace() -> Namespace:
1058
1110
  """Build the /article namespace."""
1059
1111
  ns = Namespace("article", "Knowledge base articles", path="article")
1060
1112
  ns.register_command("list", ArticleListCommand())
1113
+ ns.register_command("search", ArticleSearchCommand())
1061
1114
  ns.register_command("show", ArticleShowCommand())
1062
1115
  ns.register_command("pull", ArticlePullCommand())
1063
1116
  ns.register_command("push", ArticlePushCommand())
@@ -131,8 +131,11 @@ class CacheRefreshCommand(Command):
131
131
  "hydration verb — the cache is rehydratable upstream-"
132
132
  "mirror data and no command silently reads through it. "
133
133
  "Selectors are mutually exclusive: --primer-scope "
134
- "hydrates primer-pinned articles plus the primer-scope "
135
- "tag-entities query; --issue ID hydrates a single issue "
134
+ "hydrates primer-pinned articles, primer-pinned issues "
135
+ "(via the same single-issue projection at "
136
+ ".kd/cache/issues/<id>.yaml; comments not projected), "
137
+ "plus the primer-scope tag-entities query; --issue ID "
138
+ "hydrates a single issue "
136
139
  "into .kd/cache/issues/<id>.yaml; --article ID hydrates "
137
140
  "a single article; --query NAME runs a saved query and "
138
141
  "writes the issue list to .kd/cache/queries/<slug>.yaml "