Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ jobs:
"dist/apple-docs-${{ matrix.platform }}" \
types view String --technology Swift --json \
> "$RUNNER_TEMP/string.json"
jq -e '.metadata.title == "String"' "$RUNNER_TEMP/string.json"
jq -e '.title == "String"' "$RUNNER_TEMP/string.json"

- name: Upload artifact
uses: actions/upload-artifact@v7
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- Use a gated `Dependencies` constraint when a default implementation needs injectable collaborators.
- Keep `TypesViewCommandRunner` focused on orchestration. HTTP access and output rendering belong behind their respective abstractions.
- Validate HTTP status and response types at the external boundary.
- Preserve Apple’s response bytes unchanged for `--json` output.
- Derive `--json` output from the shared semantic presentation model. `--agent` selects the audience independently.

## Workflow

Expand Down
31 changes: 17 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

- Read APIs, inspect declarations, platform availability, conformances, and documented members.
- Discover symbols, list technologies and search their documentation collections.
- Use in scripts and agents to retrieve structured results, unchanged DocC documents, and bundled Agent Skills.
- Use in scripts and agents to retrieve structured results, agent Markdown, and bundled Agent Skills.

> [!NOTE]
> This project is not affiliated with, endorsed by, or sponsored by Apple Inc. It is an independent tool for accessing Apple Developer documentation.
Expand Down Expand Up @@ -123,7 +123,7 @@ The terminal output includes available information such as:
- Documented members and related APIs
- Canonical Apple Developer URL

Text rendering also supports sparse article and collection pages, resolves reference links, and strips remote terminal control characters. It uses normalized documentation content, which does not cover every upstream DocC field. Use `--json` when you need the original document.
Text rendering also supports sparse article and collection pages, resolves reference links, and strips remote terminal control characters. It uses normalized documentation content, which does not cover every upstream DocC field. JSON uses the same normalized content, not the original DocC document.

Every `types` command requires `--technology`. The CLI does not persist a selected framework. Nested symbols accept either dotted Swift spelling or slash-separated DocC paths:

Expand All @@ -144,31 +144,34 @@ apple-docs technologies list --agent

Agent output uses the same normalized documentation content as human text. Follow-up commands are included only for destinations the CLI can represent safely. All commands remain one-shot, even when run in a terminal.

`--agent` no longer aliases `--json`. Existing scripts that require JSON should use `--json`, which takes precedence if both flags are supplied.
`--agent` selects the audience and `--json` selects the format independently. Combine them for JSON with agent navigation commands.

### JSON output

The documentation commands accept `--json`, but their output contracts differ:
The documentation commands accept `--json` for normalized semantic output:

| Command | JSON output |
| ----------------------------- | ------------------------------------------------------------------------- |
| `types view` | Apple's upstream DocC document, with its response bytes unchanged. |
| `types list` / `types search` | An array of symbol results with `name`, `kind`, `path`, and `url` fields. |
| `technologies list` | The sorted catalog as an array of `name` and `identifier` objects. |
| Command | JSON output |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `types view` | An object with `title`, `kind`, `technology`, `path`, `url`, `modules`, `abstract`, `deprecation`, `declarations`, `availability`, `content`, `relationships`, `topics`, and `seeAlso`. |
| `types list` / `types search` | An array with `name`, `kind`, `path`, and `url` fields. |
| `technologies list` | A sorted array of `name` and `identifier` objects. |

Add `--agent` to include `navigation` commands for supported destinations. Content blocks retain semantic types and links distinguish documentation, external, and unavailable targets. Availability includes `introducedAt`, `deprecatedAt`, `obsoletedAt`, `isBeta`, and `isUnavailable` when applicable. Inapplicable optional fields are omitted.

```bash
apple-docs types view MXHangDiagnostic --technology MetricKit --json
apple-docs types list --technology MetricKit --json
apple-docs types search Button --technology SwiftUI --json
apple-docs types view String --technology Swift --json
apple-docs types view String --technology Swift --agent --json
apple-docs types search Button --technology SwiftUI --agent --json
```

For example, extract the documented title using [jq](https://jqlang.org/), installed separately:

```bash
apple-docs types view MXHangDiagnostic --technology MetricKit --json \
| jq -r '.metadata.title'
apple-docs types view String --technology Swift --json | jq -r '.title'
```

This is a breaking change: page JSON is normalized rather than raw DocC, and title extraction uses `.title` instead of `.metadata.title`. There is no raw-DocC export flag. Normalization does not preserve every upstream field, so missing normalized content is not evidence that Apple supplies no such information. JSON stdout contains only the result, with warnings and verbose diagnostics sent to stderr.

### Cache

Stateless command selection does not mean responses are never cached. To clear cached Apple documentation responses:
Expand Down
4 changes: 2 additions & 2 deletions Sources/CLI/documentation/DocumentationPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ indirect enum DocumentationBlock: Equatable, Sendable {
case aside(content: [DocumentationBlock], style: String, name: String?)
}

struct DocumentationDeclaration: Equatable, Sendable {
struct DocumentationDeclaration: Encodable, Equatable, Sendable {
let languages: [String]
let text: String
}

struct DocumentationAvailability: Equatable, Sendable {
struct DocumentationAvailability: Encodable, Equatable, Sendable {
let name: String
let introducedAt: String?
let deprecatedAt: String?
Expand Down
6 changes: 3 additions & 3 deletions Sources/CLI/main/OutputOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ enum OutputFormat: Equatable, Sendable {
}

struct OutputOptions: ParsableArguments {
@Flag(help: "Print JSON. Page output preserves Apple's raw DocC document, even with --agent.")
@Flag(help: "Print normalized semantic JSON. Combine with --agent to include follow-up commands.")
var json = false

@Flag(help: "Print agent-oriented Markdown with follow-up commands. --json takes precedence.")
@Flag(help: "Print agent-oriented Markdown with follow-up commands. Combine with --json for structured output.")
var agent = false

var audience: OutputAudience { agent && !json ? .agent : .human }
var audience: OutputAudience { agent ? .agent : .human }
var format: OutputFormat { json ? .json : .text }
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ struct DefaultDocumentationTypeListRenderer: Sendable {
}
return terminalSafeText(renderTable(types))
case .json:
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
// JSONEncoder produces valid UTF-8, so preserve a non-optional rendering contract.
// swiftlint:disable:next optional_data_string_conversion
return String(decoding: try encoder.encode(types), as: UTF8.self)
let presentation = DocumentationPresenter().symbols(types, technology: technology, audience: audience)
return try StructuredDocumentationRenderer().render(presentation)
}
}

Expand Down
7 changes: 2 additions & 5 deletions Sources/CLI/renderer/DefaultTechnologyListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ struct DefaultTechnologyListRenderer: Sendable {
}
return terminalSafeText(renderTable(technologies))
case .json:
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
// JSONEncoder produces valid UTF-8, so preserve a non-optional rendering contract.
// swiftlint:disable:next optional_data_string_conversion
return String(decoding: try encoder.encode(technologies), as: UTF8.self)
let presentation = DocumentationPresenter().technologies(technologies, audience: audience)
return try StructuredDocumentationRenderer().render(presentation)
}
}

Expand Down
14 changes: 8 additions & 6 deletions Sources/CLI/renderer/DefaultTypeDocumentationRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ struct DefaultTypeDocumentationRenderer: Sendable {
var audience: OutputAudience = .human

func render(_ document: TypeDocumentationDocument) throws -> String {
if output == .json {
return RawJSONTypeDocumentationRenderer().render(document)
}
let page = try DocumentationPageDecoder().decode(document.data, destination: document.destination)
let presentation = DocumentationPresenter().page(page, audience: audience)
return audience == .agent
? AgentDocumentationRenderer().render(presentation)
: TextTypeDocumentationRenderer().render(page)
switch output {
case .json:
return try StructuredDocumentationRenderer().render(presentation)
case .text:
return audience == .agent
? AgentDocumentationRenderer().render(presentation)
: TextTypeDocumentationRenderer().render(page)
}
}
}
137 changes: 131 additions & 6 deletions Sources/CLI/renderer/DocumentationPresentation.swift
Original file line number Diff line number Diff line change
@@ -1,39 +1,164 @@
struct AgentNavigation: Equatable, Sendable {
import Foundation

struct AgentNavigation: Encodable, Equatable, Sendable {
let technology: String
let path: String?
let command: String
}

struct PagePresentation: Sendable {
struct PagePresentation: Encodable, Sendable {
let document: DocumentationPage
let audience: OutputAudience
let navigation: AgentNavigation?
let relationships: [GroupPresentation]
let topics: [GroupPresentation]
let seeAlso: [GroupPresentation]

private enum CodingKeys: CodingKey {
case title, kind, technology, path, url, modules, abstract, deprecation, declarations, availability, content
case relationships, topics, seeAlso, navigation
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(document.title, forKey: .title)
try container.encode(document.kind, forKey: .kind)
try container.encode(document.destination.technology, forKey: .technology)
try container.encode(document.destination.path, forKey: .path)
try container.encode(document.url, forKey: .url)
try container.encode(document.modules, forKey: .modules)
try container.encode(document.abstract, forKey: .abstract)
try container.encode(document.deprecation, forKey: .deprecation)
try container.encode(document.declarations, forKey: .declarations)
try container.encode(document.availability, forKey: .availability)
try container.encode(document.content, forKey: .content)
try container.encode(relationships, forKey: .relationships)
try container.encode(topics, forKey: .topics)
try container.encode(seeAlso, forKey: .seeAlso)
try container.encodeIfPresent(navigation, forKey: .navigation)
}
}

struct GroupPresentation: Sendable {
struct GroupPresentation: Encodable, Sendable {
let id: String
let title: String
let references: [ReferencePresentation]
}

struct ReferencePresentation: Sendable {
struct ReferencePresentation: Encodable, Sendable {
let reference: DocumentationReference
let navigation: AgentNavigation?

private enum CodingKeys: CodingKey {
case id, title, kind, abstract, target, navigation
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(reference.id, forKey: .id)
try container.encode(reference.title, forKey: .title)
try container.encode(reference.kind, forKey: .kind)
try container.encode(reference.abstract, forKey: .abstract)
try container.encode(PresentedLinkTarget(target: reference.target), forKey: .target)
try container.encodeIfPresent(navigation, forKey: .navigation)
}
}

struct SymbolPresentation: Sendable {
struct SymbolPresentation: Encodable, Sendable {
let name: String
let kind: String
let path: String
let url: String
let navigation: AgentNavigation?
}

struct TechnologyPresentation: Sendable {
struct TechnologyPresentation: Encodable, Sendable {
let name: String
let identifier: String
let navigation: AgentNavigation?
}

private struct PresentedLinkTarget: Encodable {
let target: DocumentationLinkTarget

private enum CodingKeys: CodingKey {
case type, technology, path, fragment, url, label
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch target {
case .documentation(let destination):
try container.encode("documentation", forKey: .type)
try container.encode(destination.technology, forKey: .technology)
try container.encode(destination.path, forKey: .path)
try container.encodeIfPresent(destination.fragment, forKey: .fragment)
try container.encode(destination.url, forKey: .url)
case .external(let url):
try container.encode("external", forKey: .type)
try container.encode(url, forKey: .url)
case .unavailable(let label):
try container.encode("unavailable", forKey: .type)
try container.encode(label, forKey: .label)
}
}
}

extension DocumentationInline: Encodable {
private enum CodingKeys: CodingKey {
case type, text, target
}

var text: String {
switch self {
case .text(let text), .code(let text): return text
case .link(let label, _): return label.map(\.text).joined()
}
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(text, forKey: .text)
switch self {
case .text: try container.encode("text", forKey: .type)
case .code: try container.encode("code", forKey: .type)
case .link(_, let target):
try container.encode("link", forKey: .type)
try container.encode(PresentedLinkTarget(target: target), forKey: .target)
}
}
}

extension DocumentationBlock: Encodable {
private enum CodingKeys: CodingKey {
case type, inlineContent, text, code, syntax, items, startIndex, content, style, name
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .paragraph(let content):
try container.encode("paragraph", forKey: .type)
try container.encode(content, forKey: .inlineContent)
case .heading(let text):
try container.encode("heading", forKey: .type)
try container.encode(text, forKey: .text)
case .codeListing(let code, let syntax):
try container.encode("codeListing", forKey: .type)
try container.encode(code, forKey: .code)
try container.encodeIfPresent(syntax, forKey: .syntax)
case .orderedList(let items, let start):
try container.encode("orderedList", forKey: .type)
try container.encode(items, forKey: .items)
try container.encode(start, forKey: .startIndex)
case .unorderedList(let items):
try container.encode("unorderedList", forKey: .type)
try container.encode(items, forKey: .items)
case .aside(let content, let style, let name):
try container.encode("aside", forKey: .type)
try container.encode(content, forKey: .content)
try container.encode(style, forKey: .style)
try container.encodeIfPresent(name, forKey: .name)
}
}
}
7 changes: 0 additions & 7 deletions Sources/CLI/renderer/RawJSONTypeDocumentationRenderer.swift

This file was deleted.

10 changes: 10 additions & 0 deletions Sources/CLI/renderer/StructuredDocumentationRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

struct StructuredDocumentationRenderer: Sendable {
func render(_ presentation: some Encodable) throws -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
// JSONEncoder guarantees UTF-8, so this conversion cannot fail.
return String(data: try encoder.encode(presentation), encoding: .utf8)!
}
}
Loading
Loading