zard 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0551d23261849efc4c7967195baade2229626abf6b5dcdd653681787c968e19d
4
- data.tar.gz: e08b3a10d55ec03987f22b505116f1e839d49769522d76f12c66efb9b7929990
3
+ metadata.gz: 9c9ab0c9576a9523bbbf9e28e700547c6867639e731cf7fb3365f5b1e6384c4f
4
+ data.tar.gz: 37182c341bb8ca6cb2cc1f4f27527d0551a28d78cf0c9d47867db31fcb8ae216
5
5
  SHA512:
6
- metadata.gz: e6631158d5562709a1846e7088384139894e1774f8c74478de4fa776336922af76098532282bcccb6cbb3821640a63f27a2c70c8835faa3dc381f4d4a6ec00c4
7
- data.tar.gz: efe0e597d3d2e4fbb255aaf0edeb01ea0242b4d2022d11816be0e045526cf78b8b36c11990bf2d68d1a4a05338a61b241d747872a9a98a536e7837ba6b2a387c
6
+ metadata.gz: adcab0e387452ebea5e0fa5b978600ecfcccfd1938bef002b19ea75699793680338cfcd0a2a08b481bb09a430f5931e37154b7f551d750e0709a27abef283066
7
+ data.tar.gz: b5b76cd72e4b9a03530f187719ea891597f52cbdb67abee884df86c90a20f0afe8b0b7e88cb323759fd59175ced12c751aa0eb9bd197f5c887dad1596719a2af
data/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.0.2] - 2026-10-19
10
+
11
+ Generate scoped API documentation and versioned JSON for static sites, optionally enriched by released Rigor lens facts. This release also adds annotation formatting and an unstyled Astro presentation package; consumer sites own styling and publication.
12
+
13
+ ### Added
14
+
15
+ - Build hierarchical Markdown documentation for explicitly configured monorepo scopes, with namespace or file organization, direct dependency links, navigation indexes, and managed output updates.
16
+ - Link YARD-style `@see` references and URLs in generated Markdown, including cross-file CLI references and equivalent `.#` and `?.` module-function spellings.
17
+ - Link BitClust Markdown and RD-compatible references in prose and documentation tags while preserving code regions, existing Markdown links, and source provenance.
18
+ - Format annotation layout through `Zard.format` and `zard-doc format`, with safe contract ordering, redundant-marker removal, check/write modes, and reparsed source provenance.
19
+ - Export experimental `zard-doc/v0` revision 1 JSON with public declarations, exact Ruby parameter syntax, source snapshots, scoped links and written superclass/mixin relationships ([#22](https://github.com/rigortype/zard/pull/22), [#23](https://github.com/rigortype/zard/pull/23), [#25](https://github.com/rigortype/zard/pull/25)).
20
+ - Enrich exports through the optional `zard-rigor` gem and released Rigor 0.4.5 CLI, preserving shared signatures, per-definition returns, provenance and explicit unavailable reasons without loading analyzer internals ([#24](https://github.com/rigortype/zard/pull/24), [#27](https://github.com/rigortype/zard/pull/27)).
21
+ - Consume constant value types, aliases, module functions and readable declared block types with exact source matching and separate instance/singleton member bindings; only Rigor 0.4.5 is supported.
22
+ - Provide the experimental, unpublished `zard-doc-astro` package with unstyled accessible HTML, Starlight routes, grouped member navigation, definition/name sorting, native documentation disclosures and class/module relationship browsing.
23
+
9
24
  ## [0.0.1] - 2026-10-11
10
25
 
11
26
  The first ZARD release separates Ruby API documentation from RBS and Rigor contracts. Parse Ruby source into a versioned model, generate public API Markdown, and lint documentation from files, directories, or standard input without installing Rigor.
@@ -23,5 +38,6 @@ The first ZARD release separates Ruby API documentation from RBS and Rigor contr
23
38
  - Provide `zard-doc render` and `zard-doc lint` for files, recursive directories, and standard input, with deterministic path ordering and no partial Markdown on input errors.
24
39
  - Distribute `zard` and `zard-doc` as separate gems for Ruby 3.2 and newer, with a usage guide and a complete multi-file example.
25
40
 
26
- [Unreleased]: https://github.com/rigortype/zard/compare/v0.0.1...HEAD
41
+ [Unreleased]: https://github.com/rigortype/zard/compare/v0.0.2...HEAD
42
+ [0.0.2]: https://github.com/rigortype/zard/compare/v0.0.1...v0.0.2
27
43
  [0.0.1]: https://github.com/rigortype/zard/tree/v0.0.1
data/CONTEXT.md CHANGED
@@ -4,6 +4,22 @@ ZARD is a Ruby self-documentation notation for people and AI agents. It uses pla
4
4
 
5
5
  ## Language
6
6
 
7
+ **Source mapping**:
8
+ A correspondence between a source tree and a destination within the API documentation tree.
9
+ _Avoid_: Ruby module, gem boundary
10
+
11
+ **Documentation scope**:
12
+ A named, explicitly defined API documentation context containing one or more source trees and their declarations, contracts, and facts. Applications have separate documentation scopes by default, even when they share Ruby namespace names.
13
+ _Avoid_: Ruby namespace, source mapping, automatically detected application
14
+
15
+ **Documentation dependency**:
16
+ An explicitly declared documentation scope whose API declarations may be referenced from another scope.
17
+ _Avoid_: runtime dependency, implicit global lookup
18
+
19
+ **Documentation root**:
20
+ The common destination for API documentation trees produced by source mappings.
21
+ _Avoid_: source root, repository root
22
+
7
23
  **ZARD**:
8
24
  The self-documentation language defined by this project.
9
25
  _Avoid_: YARD, RDoc
data/README.md CHANGED
@@ -16,7 +16,7 @@ ZARD separates contracts from API documentation:
16
16
  - **API Documentation (`@param`, `@return`, etc.)**: Human- and AI-facing prose explains *what* and *why* without repeating type signatures.
17
17
  - **Structured Declaration Model**: The parser captures declarations, contracts, documentation, and diagnostics with precise source spans and provenance—ideal for IDEs, automated tooling, and AI agents.
18
18
 
19
- The current release preserves contract payloads as text. It does not parse or validate their types, check documentation claims against contracts, or integrate with the Rigor lens.
19
+ The current release preserves contract payloads as text. It does not parse or validate their types or check documentation claims against contracts. The development checkout also provides an optional experimental Rigor lens adapter, described below.
20
20
 
21
21
  ---
22
22
 
@@ -46,31 +46,24 @@ end
46
46
 
47
47
  ## Architecture & Packages
48
48
 
49
- This repository provides two coordinated gems:
49
+ The 0.0.2 release contains parser, documentation and optional adapter gems:
50
50
 
51
51
  | Gem | Role | Description |
52
52
  | --- | --- | --- |
53
53
  | [`zard`](.) | Parser & Model | Parses Ruby source into a versioned, static document model (`Zard::Model::V1`) with source spans and provenance. |
54
54
  | [`zard-doc`](zard-doc) | Renderer & CLI | Renders API documentation from the ZARD model into Markdown, and provides the `zard-doc` CLI tool. |
55
+ | [`zard-rigor`](zard-rigor) | Optional adapter | Enriches JSON exports through the released Rigor 0.4.5 CLI (`lens/v0` revision 1), without loading analyzer internals. |
55
56
 
56
57
  ---
57
58
 
58
59
  ## Installation
59
60
 
60
- The `0.0.1` release candidate is not yet published on RubyGems. Use the repository as the Gem source:
61
+ Install [zard](https://rubygems.org/gems/zard) and [zard-doc](https://rubygems.org/gems/zard-doc) 0.0.2 from RubyGems by adding the pair to your Gemfile:
61
62
 
62
63
  ```ruby
63
64
  # Gemfile
64
- gem "zard", github: "rigortype/zard"
65
- gem "zard-doc", github: "rigortype/zard"
66
- ```
67
-
68
- Once published, install with:
69
-
70
- ```ruby
71
- # Gemfile
72
- gem "zard", "~> 0.0.1"
73
- gem "zard-doc", "~> 0.0.1"
65
+ gem "zard", "~> 0.0.2"
66
+ gem "zard-doc", "~> 0.0.2"
74
67
  ```
75
68
 
76
69
  Run `bundle install` after adding the gems to your Gemfile.
@@ -127,6 +120,10 @@ markdown = Zard::Doc.render(document)
127
120
 
128
121
  The renderer emits documented public declarations and supports class, module, constant, attribute, and method prose, as well as `@param`, `@return`, `@yieldparam`, `@yieldreturn`, `@option`, `@raise`, `@note`, `@see`, `@deprecated`, and `@example`. Contracts remain in the model and are not duplicated into API prose.
129
122
 
123
+ Version 0.0.2 provides `zard-doc build --config zard-doc.yml --format json` for static-site consumers. It exports public declarations, documentation, unparsed contracts, source provenance, and scoped page/link metadata to stdout without publishing files. This experimental [JSON export](docs/json-export.md) works independently of Rigor; install the optional `zard-rigor` gem to enrich it with reviewed lens facts.
124
+
125
+ The experimental [`zard-doc-astro`](zard-doc-astro/README.md) JavaScript package owns unstyled API presentation and a separate Starlight adapter. Consumer sites own CSS, source acquisition, localization and publication; the Ruby gems do not depend on Astro.
126
+
130
127
  ---
131
128
 
132
129
  ## Supported Ruby Semantics
@@ -14,6 +14,7 @@ ZARD uses an em dash `—` when a named documentation tag or an optional type mu
14
14
  - `@option options :format — Output format.` is ZARD documentation.
15
15
  - `@option options :format [Symbol] — Output format.` is ZARD documentation with a claim.
16
16
  - Tags with no name and no type, such as `@note`, `@see`, `@deprecated`, and `@example`, write their description directly.
17
+ - For YARD compatibility, `@see` descriptions may contain a reference followed by a whitespace-separated display label; this form does not require `—`.
17
18
  - `@param value [String] Input text.` is preserved as raw text, not as a type hint.
18
19
  - Named tags such as `@param`, `@option`, `@yieldparam`, and `@raise` require `—` before their description.
19
20
  - `zard-doc lint` warns about YARD-like tags and can promote the warning to an error.
@@ -0,0 +1,112 @@
1
+ # Use BitClust references and YARD-compatible see tags
2
+
3
+ ZARD adopts BitClust Markdown references, accepts their RD link forms for compatibility, and supports YARD-style shorthand in `@see`. All forms share reference resolution, while original comment text and source provenance remain available. Compatibility makes existing Ruby documentation familiar and avoids a separate ZARD spelling for each kind of method.
4
+
5
+ Status: Accepted. The current implementation covers shorthand `@see` references, URLs, and the explicit BitClust and RD forms below. Availability describes repository support rather than a published gem version.
6
+
7
+ ## Problem Statement
8
+
9
+ Authors need links between Ruby declarations in API documentation. BitClust and YARD provide familiar conventions, but differ in delimiters, labels, and module-function spelling. Documentation must retain its original text even when a reference cannot be linked.
10
+
11
+ ## Solution
12
+
13
+ Use explicit BitClust forms in prose and allow concise YARD-compatible targets in `@see`. Accept both `Kernel.#puts` and `Kernel?.puts` with identical meaning. Generated documentation links only to destinations it can identify reliably.
14
+
15
+ ## User Stories
16
+
17
+ 1. As an author, I want to link an instance method, so that readers can find related behavior.
18
+ 2. As an author, I want to link a class method, so that readers can find construction and utility APIs.
19
+ 3. As an author, I want either module-function spelling to work, so that existing comments remain usable.
20
+ 4. As an author, I want relative method targets, so that comments need not repeat their owner.
21
+ 5. As an author, I want an absolute target, so that a local namespace cannot change its meaning.
22
+ 6. As an author, I want a custom label, so that references fit the surrounding explanation.
23
+ 7. As an author, I want RD links accepted, so that migrating documentation does not require rewriting every reference.
24
+ 8. As an author, I want links in prose and documentation tags, so that related APIs can be mentioned where they matter.
25
+ 9. As an author, I want code examples preserved, so that literal reference syntax is not expanded inside code.
26
+ 10. As a reader, I want links across generated input files, so that related declarations remain reachable.
27
+ 11. As a reader, I want unresolved references preserved, so that incomplete documentation retains useful context.
28
+ 12. As a tool author, I want original comments and source spans, so that normalization does not erase provenance.
29
+
30
+ ## Implementation Decisions
31
+
32
+ ### Accepted forms
33
+
34
+ | Form | Meaning | Availability |
35
+ | --- | --- | --- |
36
+ | `[m:Foo#bar]` | Explicit instance-method reference | Implemented |
37
+ | `[m:Foo.bar]` | Explicit class-method reference | Implemented |
38
+ | `[m:Kernel?.puts]` | Explicit module-function reference | Implemented |
39
+ | `[label](m:Foo#bar)` | Explicit reference with a display label | Implemented |
40
+ | `[c:Foo]` | Class or module reference | Implemented |
41
+ | `[m:Math::PI]` | Constant reference | Implemented |
42
+ | `[[m:Foo#bar]]` | RD-compatible method reference | Implemented |
43
+ | `[[m:Kernel.#puts]]` | RD-compatible module-function reference | Implemented |
44
+ | `[[c:Foo]]` | RD-compatible class or module reference | Implemented |
45
+ | `@see Foo#bar` | Shorthand instance-method reference | Implemented |
46
+ | `@see Foo` / `@see Foo::CONST` | Shorthand class, module, or constant reference | Implemented |
47
+ | `@see Foo.bar Label` | Shorthand class-method reference with a label | Implemented |
48
+ | `@see #bar` / `@see .bar` | Reference relative to the current owner | Implemented |
49
+ | `@see Kernel.#puts` / `@see Kernel?.puts` | Equivalent module-function references | Implemented |
50
+ | `@see https://example.com Label` | External URL with an optional label | Implemented |
51
+
52
+ The `@see` examples are comment payloads, written as `# @see ...` in Ruby source. Explicit forms also belong in `@see` descriptions; they are not shorthand target tokens and must be recognized before shorthand parsing.
53
+
54
+ ### Targets and normalization
55
+
56
+ - `Foo#bar` selects an instance method. `Foo.bar` selects a singleton method.
57
+ - `Kernel.#puts` and `Kernel?.puts` select the same module-function reference and link to its public singleton copy. Normalization preserves the original spelling for display and provenance. Relative `.#puts` and `?.puts` follow the same rule.
58
+ - A leading `::` requests a root target. Relative `#bar` and `.bar` use the current class or module; shorthand top-level instance methods use the empty owner.
59
+ - Qualified targets search the current namespace and its namespace prefixes. This matches YARD's namespace-based convention, including qualified openings such as `class Outer::Caller`; it does not reproduce Ruby's lexical constant lookup.
60
+ - A nearer declared owner shadows outer owners. If its requested member is missing, private, or undocumented, resolution stops and preserves the reference as text.
61
+ - Attribute writers use their actual method name with `=`. An accessor exposes reader and writer references, which may share one rendered declaration. A replacement reader remains distinct from a retained writer.
62
+ - Valid Unicode owners and method names participate in lookup. Method punctuation remains part of the target; normalization changes only the owner/method separator.
63
+ - Explicit constant receiver paths are matched against declared namespace candidates. Dynamic receiver expressions and runtime aliases do not establish object identity.
64
+
65
+ ### Labels and comment boundaries
66
+
67
+ - Shorthand `@see` uses the first whitespace-separated token as its target and the remaining text as an optional display label. Leading and trailing whitespace is trimmed for rendering. No `—` is required, as clarified by ADR 0002.
68
+ - If no label is supplied, the original target spelling supplies the display text. Labels and destinations must be escaped appropriately for generated Markdown.
69
+ - Existing documentation continuation rules apply. Continuation lines remain part of the same `@see` description and, for a resolved shorthand reference, its label.
70
+ - Explicit Markdown forms use `[label](m:target)` for labels. A label consisting entirely of a backtick code span keeps that code presentation, following BitClust.
71
+ - Method names containing brackets use BitClust escaping: `[m:Hash#\[\]]`. RD compatibility accepts the historical form `[[m:Hash#[] ]]`, including its delimiter-separating space.
72
+ - References are recognized in prose, including documentation tag descriptions, while inline code and fenced or indented code blocks remain literal. An `@example` code region is not implicitly a reference region.
73
+ - RD compatibility covers link syntax only. It does not reinterpret the entire comment as an RD document.
74
+
75
+ ### Resolution and output
76
+
77
+ - Resolve against explicit declarations in the generated input set. The single-document API uses that document; the CLI uses all input files in its combined output.
78
+ - A destination must identify exactly one public declaration with rendered API documentation. Private, undocumented, ambiguous, unresolved, and refinement targets remain text with their original description intact.
79
+ - Equivalent forms resolve to the same destination. References to a shared declaration use one stable, collision-free anchor, independent of parameter display and heading slug rules.
80
+ - URL shorthand supports HTTP, HTTPS, and mailto. It does not require declaration lookup.
81
+ - Declaration lookup does not follow inheritance or mixins, evaluate Ruby, infer runtime receiver identity, or fetch external API inventories. These limits are deliberate even where YARD can resolve more broadly.
82
+ - The core retains documentation descriptions, raw source, and spans. Reference interpretation belongs to documentation generation and does not turn a reference into a contract or documentation claim.
83
+ - A future structured reference representation must preserve provenance and respect the versioned model boundary; this ADR does not add a public model field or resolver API.
84
+
85
+ ## Testing Decisions
86
+
87
+ Use the existing parse API, Markdown generation API, and CLI as the acceptance seams. Assert rendered destinations and preserved source text rather than requiring a new resolver interface.
88
+
89
+ - Cover qualified and relative instance/class methods, root targets, namespace-prefix lookup, and shadowing.
90
+ - Verify that both module-function spellings, with or without an owner, produce the same destination and one anchor.
91
+ - Cover reader/writer accessors, aliases, operator methods, Unicode owners, and Unicode method names.
92
+ - Verify labels, continuation lines, spacing, URL escaping, empty descriptions, and punctuation-only unresolved targets.
93
+ - Verify private, undocumented, missing, ambiguous, dynamic-receiver, and refinement cases retain text rather than linking to an unrelated declaration.
94
+ - Exercise multiple CLI input files, deterministic output, and cross-file ambiguity through the existing CLI.
95
+ - Verify BitClust/RD equivalence, bracket escaping, custom labels, literal code regions, and unchanged raw comments and source spans through the same seams.
96
+
97
+ ## Out of Scope
98
+
99
+ Full RD rendering, YARD's `{target}` prose syntax, documentation copying via `(see target)`, runtime ancestry resolution, external reference databases, BitClust schemes beyond `m:` and `c:`, BitClust document/method fragment schemes, and the proposed `[label][m:target]` reference-style form are outside this decision. Unresolved-reference lint diagnostics are a separate decision; this ADR requires lossless fallback rather than a new warning policy.
100
+
101
+ ## Further Notes
102
+
103
+ The explicit forms are a compatibility subset, not a claim of complete BitClust or YARD rendering compatibility. ADR 0001 keeps API documentation separate from contracts, ADR 0002 governs documentation markers and continuation, and ADR 0003 preserves the core/documentation package boundary.
104
+
105
+ Primary references:
106
+
107
+ - [BitClust Markdown specification, cross references](https://github.com/rurema/bitclust/blob/581943661862910ff2035df2974709aef816a1c0/doc/markdown-samples/MARKUP_SPEC.md#7-クロスリファレンスハイパーリンク).
108
+ - [BitClust Markdown reference implementation](https://github.com/rurema/bitclust/blob/581943661862910ff2035df2974709aef816a1c0/lib/bitclust/mdcompiler.rb#L849).
109
+ - [Rurema Markdown migration notes](https://blog.n-z.jp/blog/2026-07-23-rurema-to-markdown.html).
110
+ - [YARD reference syntax and automatic `@see` linking](https://github.com/lsegal/yard/blob/3d8670543d2408baea3a1360b2258fde2ee65917/docs/GettingStarted.md#L395-L409).
111
+ - [YARD `@see` tag definition](https://github.com/lsegal/yard/blob/3d8670543d2408baea3a1360b2258fde2ee65917/lib/yard/tags/library.rb#L503-L513).
112
+ - [YARD namespace lookup](https://github.com/lsegal/yard/blob/3d8670543d2408baea3a1360b2258fde2ee65917/lib/yard/registry_resolver.rb#L50-L100).
@@ -0,0 +1,28 @@
1
+ # Format annotations with semantic preservation checks
2
+
3
+ Status: Accepted.
4
+
5
+ ZARD formatting normalizes annotation layout without interpreting contract payloads or converting ambiguous documentation into claims. Moving a contract can change the ownership of documentation continuation lines, so source layout alone is not sufficient to establish a safe edit.
6
+
7
+ ## Interface and package
8
+
9
+ The core provides `Zard.format(source, path:)`. Its result exposes `source`, `document`, `diagnostics`, and `changed?`. Formatting does not mutate the input or read or write files. The result contains a freshly parsed document whose raw text and spans refer to the formatted source. Invalid byte encodings that cannot enter the parser produce an empty document with an error diagnostic. Formatting diagnostics also refer to the returned source.
10
+
11
+ The existing `zard-doc` executable provides `format PATH`, `format --check PATH...`, and `format --write PATH...`. Plain mode prints one formatted input; `-` reads standard input. Check and write modes support recursive directory inputs. Write mode rejects standard input. Check and write are mutually exclusive.
12
+
13
+ Exit status is 0 on success, 1 for source errors or changes required in check mode, and 2 for invocation or I/O errors. Warnings, including ambiguous documentation left unchanged, do not fail formatting. All inputs are read and validated before write mode changes any file. It leaves unchanged files untouched. Filesystem write failures are reported, but multi-file writes are not transactional.
14
+
15
+ ## Allowed transformations
16
+
17
+ - Normalize the comment prefix of standalone `@extrbs` annotations to `# @extrbs`, retaining indentation, payloads, notes, and trailing whitespace. A noncanonical annotation with nonblank continuation text is left unchanged because its intended boundary is ambiguous.
18
+ - Stably move `@extrbs` lines before other annotations within an attached contiguous comment block. Preserve the relative order of contract lines and all other lines. Preserve the original sequence of line terminators, including mixed LF/CRLF and an absent final newline.
19
+ - Remove a redundant em dash from parsed `@return` and `@yieldreturn` tags without claims, except when removal would reinterpret the description's leading bracket or em dash. Keep those ambiguous descriptions with the existing diagnostic. Canonical named or typed markers already have their required delimiter and need no conversion.
20
+ - Keep missing markers, YARD-like tags, unknown tags, indented documentation, examples, string literals, embedded documents, and detached comments unchanged. Retain the parser's diagnostics rather than guessing a documentation boundary.
21
+
22
+ Before accepting a contract reorder, reparse it and compare declarations, documentation fields and order, and contract payloads/notes. Contract ordering may change only through stable `@extrbs` placement. Reject a reorder that changes these values, keep the safe spacing/marker edits, and emit `format.unsafe-extrbs-order`. This protects continuation ownership and declaration attachment. Noncanonical `@extrbs` prefix repair intentionally makes a previously raw annotation a contract; it is the only such conversion.
23
+
24
+ Malformed Ruby and non-UTF-8 source are returned unchanged with errors. The formatter does not validate types, rewrite Ruby code, generalize comment spacing, format RBS payloads, infer missing documentation markers, or migrate YARD documentation.
25
+
26
+ ## Verification
27
+
28
+ Use the formatting API, existing parse API, and real CLI. Verify payloads, notes, multiline descriptions, raw text, byte spans, indentation, line terminators, safe and unsafe reorder cases, ambiguous comments, and a byte-identical second pass. Exercise stdout, stdin, check and write modes, directories, read errors, and invalid inputs before writing.
@@ -0,0 +1,85 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+
5
+ # Separate documentation scopes from page organization
6
+
7
+ Monorepos need separate application documentation even when applications share Ruby namespace names. Applications have separate documentation scopes by default. Each scope is named and explicitly configured; directory conventions and gemspecs do not automatically establish scope boundaries. Multiple source trees assigned to one scope aggregate together. A shared documentation root accommodates hierarchical output, while page organization can use source files or Ruby namespaces; a CLI option overrides the page organization selected in the configuration file. Contracts and Rigor facts must retain their association with declarations independently of page organization.
8
+
9
+ Source files without publishable declarations produce no API page. In namespace organization, documented top-level methods and constants appear on a dedicated top-level page. Script explanations and execution instructions belong in separate guides.
10
+
11
+ References first resolve within their own scope. If no local target is found, lookup searches explicitly configured documentation dependencies. Sharing Ruby namespace names does not implicitly enable references between scopes.
12
+
13
+ Each source mapping selects files using include and exclude globs relative to its source root, independently of Git tracking and gemspec contents. Namespace organization is the default; file organization remains selectable. Both modes resolve references against all selected files in their scope.
14
+
15
+ Shared source files may belong to multiple scopes, with separate scope-specific declaration identities. Within one scope, overlapping inputs parse the same physical source file once. Ambiguous local references do not fall back to dependencies; ambiguous dependency references do not select the first match. Ambiguous references remain literal and produce warnings identifying candidate scopes and source locations.
16
+
17
+ Generation validates all selected scopes before updating output. Parse errors, unreadable inputs, and output collisions prevent publication of the entire selected output set; warnings alone permit generation. Filesystem publication failures use the recovery policy below.
18
+
19
+ ## Configuration
20
+
21
+ Use YAML with `docroot`, `layout`, and named `scopes`. Each scope defines a `destination`, explicit `references`, and `sources` containing a `root` with include and exclude globs. Paths are relative to the configuration file. Source mappings may define an output prefix to distinguish source trees in file organization. CLI options override configured page organization and select scopes to generate.
22
+
23
+ ```yaml
24
+ docroot: docs/api
25
+ layout: namespace
26
+ scopes:
27
+ rigor:
28
+ destination: rigor
29
+ sources:
30
+ - root: lib
31
+ include: ["**/*.rb"]
32
+ - root: plugins
33
+ include: ["*/lib/**/*.rb"]
34
+ playground:
35
+ destination: playground
36
+ references: [rigor]
37
+ sources:
38
+ - root: apps/rigor-playground/lib
39
+ include: ["**/*.rb"]
40
+ ```
41
+
42
+ ## Aggregation and initial delivery
43
+
44
+ Reopened classes and modules share one namespace page, retaining descriptions, contracts, and provenance from each definition rather than replacing one description with another. Repeated method definitions remain separately represented; no runtime winner is inferred, and references to multiple competing method definitions are ambiguous. A namespace reference identifies the aggregate namespace page.
45
+
46
+ The initial implementation covers configuration, declaration aggregation, page organization, and cross-scope links using the existing parse model. External RBS ingestion requires a separate specification. Rigor lens integration is deferred to the `v0.1.0` goal and is not required for this workflow. Existing contract payloads retain their association with source declarations; this work does not introduce contract text rendering or type inference.
47
+
48
+ ## Generated output ownership
49
+
50
+ A generation manifest records ZARD-managed pages. Regeneration removes obsolete managed pages while preserving unowned files, including handwritten guides. If a managed page has been edited since generation, publication stops before overwriting or deleting it. Output is prepared in a staging area; publication failures trigger recovery attempts. Failed recovery produces a non-success exit and actionable recovery information. This does not promise an atomic filesystem transaction across all pages.
51
+
52
+ ## CLI and dependency generation
53
+
54
+ Add `zard-doc build --config zard-doc.yml`; retain the existing `render PATH...` combined Markdown stream and existing APIs. Configuration is explicitly selected. Without `--scope`, build all configured scopes; repeated `--scope` selects named scopes. `--layout namespace|file` overrides the configured layout.
55
+
56
+ ```sh
57
+ zard-doc build --config zard-doc.yml
58
+ zard-doc build --config zard-doc.yml --scope playground --layout file
59
+ ```
60
+
61
+ Build selected scopes together with their recursive documentation dependency closure so generated links have destinations. Reference lookup still searches only the current scope and its directly declared dependencies; transitive generation does not expand lookup visibility. Dependency cycles are allowed, and each scope is generated once.
62
+
63
+ ## Path validation and navigation
64
+
65
+ Missing source roots, unknown documentation dependencies, scopes selecting no Ruby files, and conflicting output paths are errors before publication. Output destinations must stay within `docroot`. Input symlinks must not lead outside explicitly declared input roots. Within one scope, repeated inclusion of one physical file is deduplicated when the output assignment agrees, but conflicting output assignments are errors.
66
+
67
+ Generate `docroot/index.md` listing scopes and a scope-local `index.md` listing its pages. In namespace organization, publishable top-level declarations use `_top-level.md` within their scope. Links use relative output paths and stable anchors. Collisions between API pages and reserved navigation paths are errors.
68
+
69
+ ## Acceptance scenarios
70
+
71
+ - Generate two applications defining the same namespace without mixing declarations or links.
72
+ - Aggregate multiple source trees and reopened namespaces within one scope while preserving all definition provenance.
73
+ - Generate namespace and file layouts from the same selected declarations; CLI layout overrides configuration.
74
+ - Honor include and exclude globs independently of Git and gem packaging.
75
+ - Include shared source in separate scopes without cross-scope identity leakage; deduplicate identical assignments within one scope.
76
+ - Resolve local references before direct documentation dependencies; preserve and warn on ambiguous targets.
77
+ - Generate selected scopes and recursive dependency closure, including cycles, without making transitive dependencies visible to reference lookup.
78
+ - Omit API pages for files without publishable declarations and place top-level declarations in the dedicated namespace-layout page.
79
+ - Reject invalid roots, dependencies, empty source selections, escaped paths, and output collisions before changing output.
80
+ - Generate relative links and navigation pages; reject collisions with reserved paths.
81
+ - Remove obsolete managed pages, preserve handwritten guides, and reject overwriting edited managed pages.
82
+ - Preserve existing output when pre-publication validation fails; exercise publication failure recovery and report unrecovered failures.
83
+ - Preserve existing parse, render, lint, and format behavior outside the new build workflow.
84
+
85
+ The initial file-oriented example maps `app/foo/hoge/fuga.rb` to `docs/api/foo/hoge/fuga.md` and `ext/foo-bar/hoge/fuga_foo.rb` to `docs/api/foo-bar/hoge/fuga_foo.md`. It illustrates file organization, rather than requiring that hierarchy for namespace organization. External contract and fact ingestion is deferred to a separate design.
@@ -0,0 +1,103 @@
1
+ # JSON API documentation export
2
+
3
+ Repository builds support an experimental static-site producer:
4
+
5
+ ```sh
6
+ zard-doc build --config zard-doc.yml --format json > api.json
7
+ ```
8
+
9
+ This is ZARD documentation data, not a Rigor lens result. Plain generation does not execute Ruby source, parse contract types, infer signatures, or require Rigor. The optional `zard-rigor` adapter can associate facts with source declarations without replacing their documentation or provenance.
10
+
11
+ Framework-specific HTML lives in [`zard-doc-astro`](../zard-doc-astro/README.md), with Starlight conventions isolated in its adapter. Styling, source acquisition and publication belong to each consumer. This export does not embed an Astro component or stylesheet.
12
+
13
+ ## Invocation and publication
14
+
15
+ The existing build configuration, source selection, explicit documentation scopes, dependency closure and `--layout` / `--scope` options apply. `--format markdown` remains the default and retains its managed-page behavior.
16
+
17
+ JSON exports the selected scopes and their recursive documentation dependencies as one document to stdout, after validation. It does not read a previous generation manifest or create, edit or remove anything under `docroot`. The export is self-contained for those selected scopes; it does not merge a previous partial export. Consumers own their output publication. Shell redirection itself can truncate an existing destination before ZARD runs; callers that publish artifacts should write a temporary file and replace the artifact only after success.
18
+
19
+ Source errors return 1; invalid configuration or invocation, unreadable inputs, output-path collisions and serialization errors return 2. Neither emits partial JSON. Warnings permit export and also appear on stderr. A process or stdout I/O failure can leave truncated output, so consumers must check both exit status and JSON parsing before accepting an artifact.
20
+
21
+ ## Envelope
22
+
23
+ | Field | Meaning |
24
+ | --- | --- |
25
+ | `schema` | Exactly `zard-doc/v0`. |
26
+ | `revision` | Integer `1` for this experimental representation. |
27
+ | `producer` | `zard` and `zard_doc` gem versions. |
28
+ | `layout` | `namespace` or `file`. |
29
+ | `scopes` | Selected documentation scopes, ordered by name. |
30
+ | `links` | Resolved documentation links grouped by source page path, with target `path` and `anchor`. |
31
+ | `warnings` | Reference-resolution warning strings, including ambiguous candidates; diagnostic prose is not a machine-readable enum. |
32
+
33
+ Consumers should reject unsupported schema/revision pairs. This format is experimental; a breaking change increments the revision before any stable schema commitment. It has its own lifecycle, independent of `lens/v0`, `lens/v1` and the `rigor:v1` type grammar revision. No generation timestamp, absolute configuration path or inferred fact is added. Repeated exports of identical inputs with the same producer versions are byte-identical.
34
+
35
+ ## Documentation scopes and pages
36
+
37
+ Each scope has `name`, `destination`, its explicit direct documentation dependency names in `references`, `files`, `declarations`, and `pages`.
38
+
39
+ Each page has a `path` and an ordered array of declaration `id` values. Reopened classes/modules share a namespace page while retaining distinct source declarations. Namespace pages can exist for members of an undocumented container. Top-level declarations use the existing `_top-level.md` assignment. File organization uses source mappings and prefixes. Page paths are logical paths relative to `docroot`, with Markdown extensions, not a promise of emitted Markdown files or final website URLs. Website consumers map these paths to their routes.
40
+
41
+ The site can derive scope landing pages and navigation from this metadata. Generated Markdown index pages are not included in `pages` because they own no declarations. A file without public declarations produces no page.
42
+
43
+ JSON exports all public declarations, including those without API prose, so a site can present the selected API surface before documentation is complete. Private/protected declarations remain available to internal reference lookup but are not exported or linked. The Markdown format continues to publish only documented public declarations. This publication distinction is intentional and is not an inference that a Ruby-public declaration is a stable supported product API.
44
+
45
+ ## Declarations
46
+
47
+ | Fields | Meaning |
48
+ | --- | --- |
49
+ | `id`, `page`, `anchor` | Scope-specific identity and logical page destination; `id` combines the documentation scope with the existing stable anchor. |
50
+ | `kind`, `name`, `namespace`, `display_name`, `visibility` | Original declaration kind/name, lexical namespace, display/reference name and visibility. |
51
+ | `parameters` | Existing source parameter labels, not a structured typed signature. |
52
+ | `parameter_details` | Additive Ruby syntax metadata: ordered `kind`, nullable `name`, exact `source`, source `span`, nullable default-expression `default` and `default_span`. No types or evaluated values. |
53
+ | `references` | Names by which this declaration participates in documentation reference lookup. |
54
+ | `span`, `comment_span` | Declaration and attached comment provenance. |
55
+ | `documentation` | Original tag data plus resolved Markdown descriptions. |
56
+ | `contracts` | Unparsed contract channels, payloads, notes and provenance. |
57
+ | `receiver`, `refinement`, `superclass`, `container_builder` | Written source expressions with their corresponding `*_span` fields. |
58
+ | `alias_target` | Recorded alias target, without copying the target's documentation. |
59
+ | `mixins` | Written mixin `kind`, `target` and `span`. |
60
+
61
+ Absent optional fields are `null`; arrays remain arrays. A repeated method definition has its own identity/anchor. Nothing chooses a runtime-winning definition, resolves dynamic receivers, or asserts effective ancestry. Attribute accessors remain a source declaration with multiple reference names, rather than separate inferred getter/setter signatures.
62
+
63
+ `parameter_details` is additive within revision 1; older exports may omit it and consumers fall back to `parameters`. It is `null` when no source-backed parameter list is available (including aliases and generated attributes), and `[]` for an explicit parameterless method. Kinds are `required`, `optional`, `rest`, `post`, `required_keyword`, `optional_keyword`, `keyword_rest`, `block`, `forwarding`, `no_keywords` and `implicit_rest`. Anonymous and destructured forms retain their exact source patterns with a nullable name; destructured required/post parameters keep their positional kind. Visibility changes and module-function copies retain metadata. Default expressions are never executed. A heredoc default's body is not contiguous with its parameter header: its header remains in `source`, `default`/`default_span` are unavailable and `parameter.unsupported-default` is emitted rather than presenting the opener as a complete expression.
64
+
65
+ Documentation entries carry `name`, `owner`, `subject`, optional `claim`, `description`, `raw`, and `span`, matching the source model. `rendered_description` contains Markdown with resolvable ZARD references linked, using the same resolution rules as a documentation build. Original descriptions remain unchanged. Examples, raw text and inline/fenced code preserve literal reference syntax. Consumers must use their normal safe Markdown rendering policy; strings in JSON are not trusted HTML.
66
+
67
+ Contract entries carry `channel`, `payload`, `note`, `raw`, and `span`. They are source text, not parsed or validated types. A documentation claim does not become a contract, signature or Rigor fact.
68
+
69
+ ## Source provenance and diagnostics
70
+
71
+ Each selected source file has `path`, its full SHA-256 `digest` over the bytes parsed, and structured parser `diagnostics` with `code`, `severity`, `message` and `span`. Files without public declarations remain listed for provenance and diagnostics.
72
+
73
+ All generated source paths are relative to the real directory containing the build configuration. Explicit source roots outside that directory can produce `../` paths; these are source identifiers, not output destinations. No absolute checkout paths are generated in provenance or reference warnings. Source-authored prose and raw text are preserved, including any paths the author wrote.
74
+
75
+ Spans retain the model's 1-based lines, 0-based columns and byte offsets, including an exclusive `end_offset`. Digests cover the entire source file, including documentation comments. Neither the digest nor a declaration ID proves the semantic inputs of a future Rigor analysis are unchanged.
76
+
77
+ Reference lookup searches the current documentation scope, then only its explicit direct documentation dependencies. Transitive dependencies are generated without becoming visible to lookup. Ambiguous references remain literal with warnings. The `links` entries target actual exported pages/anchors and provide route mapping input without requiring consumers to reverse-engineer references from text.
78
+
79
+ ## Written class and module relationships
80
+
81
+ The additive `relationships` array on each declaration records written
82
+ `superclass`, `include`, `prepend`, `extend` and `container_builder` references.
83
+ Each entry carries the original `expression` and `span`, a `status` (`resolved`,
84
+ `unresolved` or `expression`), and `targets`. Each target has `scope`,
85
+ `declaration_id`, `path` and `anchor`, and names a published class or module.
86
+ Resolved reopenings retain all published definition targets, including file
87
+ layout. Older exports may omit the field.
88
+
89
+ Resolution uses the existing documentation lookup: local before direct scope
90
+ dependencies, without transitive fallback or first-match ambiguity resolution.
91
+ Superclass expressions use the enclosing lexical namespace; mixins use the
92
+ class/module body context. A local constant or unpublished target prevents
93
+ fallthrough to a dependency. Non-constant expressions and container builders
94
+ remain literal; no application code is loaded or evaluated. Unresolved
95
+ relationships produce diagnostics. Target links are included in `links`.
96
+
97
+ These are documentation relationships, not runtime ancestry, lookup precedence,
98
+ overrides or inherited member facts. Consumers must retain conflicting written
99
+ parents and guard traversal against cycles. The Astro presentation generates a
100
+ per-scope hierarchy with separate module usage, reports cycles/conflicts, and
101
+ preserves each definition's own source location. The `hierarchy` route within a
102
+ scope is reserved when the extension is present; route collisions fail before
103
+ publication. Legacy exports without the extension keep their existing routes.
data/docs/usage.md CHANGED
@@ -1,17 +1,10 @@
1
- # ZARD 0.0.1 usage
1
+ # ZARD 0.0.2 usage
2
2
 
3
- ZARD parses Ruby source into a versioned model. The separate `zard-doc` gem renders the model's human-facing API documentation as Markdown. Both `0.0.1` gems are release candidates and are not yet published on RubyGems. Until publication, use the repository as the Gem source:
3
+ ZARD parses Ruby source into a versioned model. The separate `zard-doc` gem renders human-facing API documentation. Install the 0.0.2 pair with Bundler:
4
4
 
5
5
  ```ruby
6
- gem "zard", github: "rigortype/zard"
7
- gem "zard-doc", github: "rigortype/zard"
8
- ```
9
-
10
- After `0.0.1` is published, install the released pair with Bundler:
11
-
12
- ```ruby
13
- gem "zard", "~> 0.0.1"
14
- gem "zard-doc", "~> 0.0.1"
6
+ gem "zard", "~> 0.0.2"
7
+ gem "zard-doc", "~> 0.0.2"
15
8
  ```
16
9
 
17
10
  The gems require Ruby 3.2 or newer. The project is pre-1.0: the model has an explicit `Zard::Model::V1` namespace, but no general compatibility guarantee has been established yet. Review release notes when upgrading; pre-1.0 releases may change APIs and behavior.
@@ -29,6 +22,30 @@ markdown = Zard::Doc.render(document)
29
22
 
30
23
  `Zard.parse` accepts Ruby source text and a required `path:` used in provenance and diagnostics. The path labels the source; the parser does not read that file. For in-memory input, use a label such as `"example.rb"`. `Zard::Doc.render` accepts the resulting document and returns Markdown. The renderer emits documented public declarations; it does not print contracts as API prose.
31
24
 
25
+ ## Format annotations
26
+
27
+ Version 0.0.2 provides `Zard.format` and `zard-doc format`:
28
+
29
+ ```ruby
30
+ result = Zard.format(source, path: "lib/example.rb")
31
+ puts result.source if result.changed?
32
+ document = result.document
33
+ diagnostics = result.diagnostics
34
+ ```
35
+
36
+ The result contains formatted source and a freshly parsed document with spans and raw text referring to that source. The input remains unchanged. `diagnostics` includes parser diagnostics and formatting warnings.
37
+
38
+ ```sh
39
+ zard-doc format lib/example.rb
40
+ zard-doc format --check lib
41
+ zard-doc format --write lib
42
+ printf '# @return — Value.\ndef read = nil\n' | zard-doc format -
43
+ ```
44
+
45
+ Plain mode prints one input to stdout. Check mode returns 1 if any input would change; write mode updates changed files after reading and validating all inputs. Both modes accept files and recursive directories. Standard input is supported in plain and check modes. Invocation and I/O errors return 2; source errors return 1. Warnings are printed to stderr and do not fail formatting.
46
+
47
+ Formatting normalizes standalone `@extrbs` prefixes and moves them before other annotations only when reparsing preserves documentation and contract content. It removes redundant markers from untyped `@return` and `@yieldreturn`. Payloads, notes, multiline descriptions, unrelated bytes, and the original sequence of line endings are retained. YARD-like tags and ambiguous continuation boundaries are diagnosed without conversion. Invalid Ruby and non-UTF-8 source remain unchanged with errors. See [ADR 0005](adr/0005-safe-annotation-formatting.md) for the transformation limits.
48
+
32
49
  ## Inspect the model
33
50
 
34
51
  The document is available through `document.path`, `document.declarations`, and `document.diagnostics`. A declaration exposes `kind`, `name`, `namespace`, `visibility`, `parameters`, `receiver`, `receiver_span`, `refinement`, `refinement_span`, `alias_target`, `superclass`, `superclass_span`, `container_builder`, `container_builder_span`, `mixins`, `span`, `comment_span`, `documentation`, and `contracts`.
@@ -72,6 +89,41 @@ For example, `@param path [String] — The file path.` carries an optional docum
72
89
 
73
90
  Tags without a name or type write their description directly: `@return Normalized value.`, `@note Keep this in mind.`, and `@example Call the method.` Named tags such as `@param`, `@yieldparam`, `@raise`, and `@option` require the marker. `@option options :format — Output format.` documents an option. `@yieldreturn yielded value.` describes a yielded result. Supported tags include `@param`, `@return`, `@yieldparam`, `@yieldreturn`, `@option`, `@raise`, `@note`, `@see`, `@deprecated`, and `@example`.
74
91
 
92
+ ### See also references
93
+
94
+ The complete syntax decision, including BitClust and RD compatibility forms, is recorded in [ADR 0004](adr/0004-documentation-reference-syntax.md).
95
+
96
+ `@see` accepts YARD-style references followed by an optional display label, without an em dash:
97
+
98
+ ```ruby
99
+ # @see Foo#bar
100
+ # @see Foo.build Build a Foo
101
+ # @see #bar Related instance method
102
+ # @see .build Related class method
103
+ # @see Kernel.#puts Legacy module-function spelling
104
+ # @see Kernel?.puts Modern module-function spelling
105
+ # @see https://example.com/reference Reference guide
106
+ ```
107
+
108
+ `#` selects an instance method and `.` selects a class method. `.#` and `?.` are equivalent module-function spellings and link to the public singleton copy. Relative references use the current class or module; qualified references search namespace prefixes as YARD does, and a leading `::` starts at the root. This lookup also applies to qualified class openings such as `class Outer::Caller`; it does not reproduce Ruby's lexical constant lookup. A nearer declared owner prevents fallback to an unrelated outer owner when its member is missing. The first whitespace-separated token is the target and the remaining text is the display label. HTTP, HTTPS, and mailto targets become external links.
109
+
110
+ `zard-doc` links references to uniquely identified public declarations with documentation in the generated output. The CLI resolves references across all input files. Unresolved, undocumented, private, ambiguous, and refinement references remain plain text. Lookup uses explicit declarations, without following inheritance or mixins or evaluating dynamic receivers. The document model retains the original description, raw comment, and source span.
111
+
112
+ Explicit references work in prose and documentation tag descriptions, including `@see`:
113
+
114
+ ```ruby
115
+ # Read [m:Foo#bar] or [[m:Foo#bar]].
116
+ # @note See [c:Foo] and [m:Foo::LIMIT].
117
+ # @see [Read a value](m:Foo#bar)
118
+ # @see [`Foo#bar()`](m:Foo#bar)
119
+ # @see [m:Kernel?.puts]
120
+ # @see [[m:Kernel.#puts]]
121
+ ```
122
+
123
+ `m:` selects methods or constants; `c:` selects classes or modules. BitClust's single brackets and RD's double brackets resolve to the same destinations. `[label](m:target)` supplies a display label; a label consisting entirely of a backtick code span keeps its code presentation. Escape operator brackets as `[m:Foo#\[\]]`, or use the historical RD form `[[m:Foo#[] ]]` with its delimiter-separating space.
124
+
125
+ Inline code, fenced and indented code blocks, and `@example` content retain literal reference syntax. Existing Markdown links and images are preserved. Explicit forms in `@see` take precedence over shorthand parsing; unresolved forms retain their original text.
126
+
75
127
  Plain comment lines continue the preceding canonical documentation tag until another annotation or contract begins. No continuation marker is needed. Contract notes use ` -- ` after the contract payload and remain contract notes, not API prose. Familiar YARD-like tags without the em dash can be retained as raw text, but are not treated as documentation claims.
76
128
 
77
129
  ZARD documentation requires UTF-8 source text. Ruby source encoding declarations are respected; non-UTF-8 source can still contain contracts, but documentation text in it receives a diagnostic.
@@ -84,6 +136,52 @@ Visibility forms supported by the parser include bare visibility calls, inline f
84
136
 
85
137
  ## Command line
86
138
 
139
+ ### Build a documentation tree
140
+
141
+ Version 0.0.2 provides `zard-doc build`. Define named documentation scopes in a YAML file:
142
+
143
+ ```yaml
144
+ docroot: docs/api
145
+ layout: namespace
146
+ scopes:
147
+ core:
148
+ destination: core
149
+ sources:
150
+ - root: lib
151
+ include: ["**/*.rb"]
152
+ exclude: ["fixtures/**"]
153
+ playground:
154
+ destination: playground
155
+ references: [core]
156
+ sources:
157
+ - root: apps/playground/lib
158
+ include: ["**/*.rb"]
159
+ ```
160
+
161
+ ```sh
162
+ zard-doc build --config zard-doc.yml
163
+ zard-doc build --config zard-doc.yml --scope playground --layout file
164
+ zard-doc build --config zard-doc.yml --format json > api.json
165
+ ```
166
+
167
+ Paths are relative to the configuration file. The default layout is `namespace`: reopened classes and modules share a page, preserving each definition's description and source location. `file` preserves each source root's relative file hierarchy with `.md` extensions. A source mapping can set `prefix` to distinguish multiple roots in file layout. `include` defaults to `["**/*.rb"]`; `exclude` defaults to `[]`. Source selection uses globs independently of Git and gemspecs.
168
+
169
+ CLI layout overrides configuration. With no `--scope`, all scopes are built; repeat `--scope` to select several. Selected scopes include their recursive documentation dependencies for generation. References search the current scope first, then only directly declared dependencies. Separate scopes do not merge declarations even when Ruby namespace names match. Ambiguous references remain text with warnings; local shadowing prevents fallback to unrelated dependency declarations.
170
+
171
+ The default `--format markdown` publishes managed Markdown pages. `--format json` instead prints one experimental, versioned JSON document after validation and leaves `docroot` untouched. JSON includes public declarations even without prose, their original documentation and unparsed contracts, source spans and digests, page assignments, stable anchors, and resolved documentation links. Private and protected declarations are excluded. See the [JSON export contract](json-export.md) for the schema, path conventions, and differences from Markdown publication. This export does not require Rigor or a Rigor lens result.
172
+
173
+ `Declaration#parameter_details` additionally records ordered Ruby parameter syntax and default-expression provenance. `Declaration#parameters` retains its existing labels for compatibility, and the Markdown renderer remains unchanged. Astro presentation uses the richer syntax when available, without assigning parameter types or evaluating defaults.
174
+
175
+ Each scope has an index. Namespace pages follow Ruby namespace hierarchy, with documented top-level members in `_top-level.md`. Files without publishable declarations produce no API page. The documentation root has an index linking the scopes. Links use relative paths and stable anchors.
176
+
177
+ Build reports diagnostics to stderr and does not print generated Markdown to stdout. Source errors return 1; configuration, input, ownership, and publication errors return 2. Warnings permit generation. All selected inputs and destinations are validated before publication. Missing roots or dependencies, empty source selections, conflicting destinations, and paths escaping declared roots are errors.
178
+
179
+ The output manifest tracks generated pages and resolved API links. Regeneration removes obsolete managed pages, retains unrelated handwritten files, and refuses to overwrite edited managed pages or existing unowned destination files. Partial scope builds retain other scopes' managed pages and refuse changes that would invalidate their generated links; include the affected scopes or run a full build instead. Publication prepares output before writing, attempts recovery on filesystem failures, and reports recovery failures; it is not a filesystem-wide atomic transaction.
180
+
181
+ This workflow uses the existing ZARD parse model. The optional `zard-rigor` gem enriches JSON through the released Rigor CLI; see its [setup and limitations](../zard-rigor/README.md). External RBS ingestion and stable lens semantics remain separate work. See [ADR 0006](adr/0006-hierarchical-documentation-output.md).
182
+
183
+ ### Lint and render to stdout
184
+
87
185
  `zard-doc lint PATH...` checks Ruby files. Directories are searched recursively for `*.rb`; paths are sorted and duplicates removed. Warnings do not fail by default. Use `--fail-on warning` to make warnings fail:
88
186
 
89
187
  ```sh
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zard
4
+ class FormatResult
5
+ attr_reader :source, :document, :diagnostics
6
+
7
+ def initialize(source:, document:, diagnostics:, changed:)
8
+ @source = source.dup.freeze
9
+ @document = document
10
+ @diagnostics = diagnostics.freeze
11
+ @changed = changed
12
+ freeze
13
+ end
14
+
15
+ def changed?
16
+ @changed
17
+ end
18
+ end
19
+
20
+ module Formatting
21
+ class Formatter
22
+ def self.call(source, path:)
23
+ new(source, path).call
24
+ end
25
+
26
+ def initialize(source, path)
27
+ @source = source
28
+ @path = path
29
+ end
30
+
31
+ def call
32
+ unless @source.encoding.ascii_compatible?
33
+ return encoding_error("format.unsupported-encoding", "Formatting requires UTF-8 source text.")
34
+ end
35
+ parsed = Prism.parse(@source.b)
36
+ unless @source.dup.force_encoding(parsed.encoding).valid_encoding?
37
+ return encoding_error("format.invalid-encoding", "Source bytes are invalid for the declared encoding.")
38
+ end
39
+ original = Zard.parse(@source, path: @path)
40
+ if original.diagnostics.any? { |diagnostic| diagnostic.severity == :error }
41
+ return result(@source, original)
42
+ end
43
+ unless parsed.encoding == Encoding::UTF_8
44
+ diagnostic = format_diagnostic("format.unsupported-encoding", :error, "Formatting requires UTF-8 source text.", original.declarations.first&.span)
45
+ return result(@source, original, [diagnostic])
46
+ end
47
+
48
+ output = @source.b.dup
49
+ unsafe_lines = []
50
+ blocks = original.declarations.filter_map(&:comment_span).uniq { |span| [span.start_offset, span.end_offset] }
51
+ blocks.sort_by(&:start_offset).reverse_each do |span|
52
+ start = line_start(span.start_offset)
53
+ ending = (@source.b.index("\n", span.end_offset) || (@source.bytesize - 1)) + 1
54
+ block = @source.byteslice(start, ending - start).dup.force_encoding(parsed.encoding)
55
+ normalized = normalize(block, span, original)
56
+ ordered = reorder(normalized, literal_lines(normalized))
57
+ if ordered != normalized
58
+ baseline = replace(output, start, block.bytesize, normalized)
59
+ candidate = replace(output, start, block.bytesize, ordered)
60
+ if snapshot(Zard.parse(baseline.force_encoding(@source.encoding), path: @path)) == snapshot(Zard.parse(candidate.force_encoding(@source.encoding), path: @path))
61
+ normalized = ordered
62
+ else
63
+ unsafe_lines << span.start_line
64
+ end
65
+ end
66
+ output = replace(output, start, block.bytesize, normalized)
67
+ end
68
+ output.force_encoding(@source.encoding)
69
+ document = Zard.parse(output, path: @path)
70
+ diagnostics = unsafe_lines.sort.map do |line|
71
+ span = document.declarations.filter_map(&:comment_span).find { |item| item.start_line == line }
72
+ format_diagnostic("format.unsafe-extrbs-order", :warning, "Leave @extrbs in place because moving it would change documentation or contract content.", span)
73
+ end
74
+ result(output, document, diagnostics)
75
+ end
76
+
77
+ private
78
+
79
+ def result(source, document, diagnostics = [])
80
+ FormatResult.new(source: source, document: document, diagnostics: document.diagnostics + diagnostics, changed: source.b != @source.b)
81
+ end
82
+
83
+ def encoding_error(code, message)
84
+ diagnostic = format_diagnostic(code, :error, message, nil)
85
+ document = Model::V1::Document.new(path: @path, declarations: [].freeze, diagnostics: [diagnostic].freeze)
86
+ result(@source, document)
87
+ end
88
+
89
+ def line_start(offset)
90
+ offset.zero? ? 0 : (@source.b.rindex("\n", offset - 1) || -1) + 1
91
+ end
92
+
93
+ def replace(source, start, length, replacement)
94
+ source.byteslice(0, start).b + replacement.b + source.byteslice(start + length, source.bytesize).b
95
+ end
96
+
97
+ def normalize(block, span, document)
98
+ literal = literal_lines(block)
99
+ blocked = document.declarations.flat_map do |declaration|
100
+ declaration.documentation.each_with_index.filter_map do |tag, index|
101
+ next unless tag.name == :raw
102
+
103
+ continuation = tag.raw.lines.drop(1).any? { |line| !line.sub(/\A[ \t]*# ?/, "").strip.empty? }
104
+ example = index.positive? && declaration.documentation[index - 1].name == :example
105
+ tag.span.start_line if continuation || example
106
+ end
107
+ end
108
+ redundant = document.diagnostics.select { |diagnostic| diagnostic.code == "documentation.redundant-marker" }.map { |diagnostic| diagnostic.span.start_line }
109
+ ambiguous = document.declarations.flat_map(&:documentation).select do |tag|
110
+ %i[return yieldreturn].include?(tag.name) && tag.claim.nil? && tag.description.start_with?("[", "—")
111
+ end.map { |tag| tag.span.start_line }
112
+ block.lines.each_with_index.map do |line, index|
113
+ number = span.start_line + index
114
+ if literal.include?(index)
115
+ line
116
+ elsif !blocked.include?(number) && line.match?(/\A[ \t]*#[ \t]*@extrbs(?:[ \t]|\r?\n|\z)/)
117
+ line.sub(/\A([ \t]*)#[ \t]*@extrbs/, '\1# @extrbs')
118
+ elsif redundant.include?(number) && !ambiguous.include?(number)
119
+ line.sub(/(@(?:return|yieldreturn)[ \t])— ?/, '\1')
120
+ else
121
+ line
122
+ end
123
+ end.join
124
+ end
125
+
126
+ def literal_lines(block)
127
+ protected = []
128
+ example = false
129
+ fence = nil
130
+ block.lines.each_with_index do |line, index|
131
+ body = line.sub(/\A[ \t]*# ?/, "")
132
+ content = fence ? body : body.sub(/\A@(?:example|note|deprecated|see)(?:[ \t]|\z)/, "")
133
+ marker = content.match(/\A {0,3}(`{3,}|~{3,})([^\r\n]*)/)
134
+ if fence
135
+ protected << index
136
+ fence = nil if marker && marker[1][0] == fence[0] && marker[1].length >= fence.length && marker[2].strip.empty?
137
+ next
138
+ end
139
+ canonical = line.match?(/\A[ \t]*#(?: @|:)/)
140
+ if canonical
141
+ example = body.match?(/\A@example(?:[ \t]|\r?\n|\z)/)
142
+ elsif example
143
+ protected << index
144
+ end
145
+ if marker && (marker[1][0] != "`" || !marker[2].include?("`"))
146
+ fence = marker[1]
147
+ protected << index
148
+ end
149
+ end
150
+ protected
151
+ end
152
+
153
+ def reorder(block, protected)
154
+ lines = block.lines
155
+ first = lines.index { |line| line.match?(/\A[ \t]*#(?: @|:)/) }
156
+ return block unless first
157
+
158
+ terminators = lines.map { |line| line[/\r?\n\z/].to_s }
159
+ bodies = lines.map { |line| line.delete_suffix(line[/\r?\n\z/].to_s) }
160
+ annotations = bodies.drop(first)
161
+ extrbs, remaining = annotations.each_with_index.partition { |line, index| !protected.include?(first + index) && line.match?(/\A[ \t]*# @extrbs(?:[ \t]|\z)/) }
162
+ (bodies.take(first) + (extrbs + remaining).map(&:first)).each_with_index.map { |line, index| line + terminators[index] }.join
163
+ end
164
+
165
+ def snapshot(document)
166
+ document.declarations.map do |declaration|
167
+ tags = declaration.documentation.map { |tag| [tag.name, tag.owner, tag.subject, tag.claim, tag.description] }
168
+ contracts = declaration.contracts.map { |contract| [contract.channel, contract.payload, contract.note] }.sort_by(&:inspect)
169
+ [declaration.kind, declaration.name, declaration.namespace, declaration.receiver, declaration.refinement, tags, contracts]
170
+ end
171
+ end
172
+
173
+ def format_diagnostic(code, severity, message, span)
174
+ span ||= Model::V1::SourceSpan.new(path: @path, start_line: 1, start_column: 0, end_line: 1, end_column: 0, start_offset: 0, end_offset: 0)
175
+ Model::V1::Diagnostic.new(code: code, severity: severity, message: message, span: span)
176
+ end
177
+ end
178
+ end
179
+ end
data/lib/zard/model/v1.rb CHANGED
@@ -69,15 +69,30 @@ module Zard
69
69
  end
70
70
  end
71
71
 
72
+ class Parameter
73
+ attr_reader :kind, :name, :source, :span, :default, :default_span
74
+
75
+ def initialize(kind:, name:, source:, span:, default: nil, default_span: nil)
76
+ @kind = kind
77
+ @name = name
78
+ @source = source
79
+ @span = span
80
+ @default = default
81
+ @default_span = default_span
82
+ freeze
83
+ end
84
+ end
85
+
72
86
  class Declaration
73
- attr_reader :kind, :name, :namespace, :visibility, :parameters, :receiver, :receiver_span, :refinement, :refinement_span, :alias_target, :superclass, :superclass_span, :container_builder, :container_builder_span, :mixins, :span, :comment_span, :documentation, :contracts
87
+ attr_reader :kind, :name, :namespace, :visibility, :parameters, :parameter_details, :receiver, :receiver_span, :refinement, :refinement_span, :alias_target, :superclass, :superclass_span, :container_builder, :container_builder_span, :mixins, :span, :comment_span, :documentation, :contracts
74
88
 
75
- def initialize(kind:, name:, namespace:, visibility:, parameters:, span:, comment_span:, documentation:, contracts:, receiver: nil, receiver_span: nil, refinement: nil, refinement_span: nil, alias_target: nil, superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil, mixins: [].freeze)
89
+ def initialize(kind:, name:, namespace:, visibility:, parameters:, span:, comment_span:, documentation:, contracts:, parameter_details: nil, receiver: nil, receiver_span: nil, refinement: nil, refinement_span: nil, alias_target: nil, superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil, mixins: [].freeze)
76
90
  @kind = kind
77
91
  @name = name
78
92
  @namespace = namespace
79
93
  @visibility = visibility
80
94
  @parameters = parameters
95
+ @parameter_details = parameter_details
81
96
  @receiver = receiver
82
97
  @receiver_span = receiver_span
83
98
  @refinement = refinement
@@ -924,6 +924,7 @@ module Zard
924
924
  namespace: namespace,
925
925
  visibility: visibility,
926
926
  parameters: parameters,
927
+ parameter_details: declaration_parameters(node, kind, alias_target),
927
928
  receiver: receiver,
928
929
  receiver_span: receiver_span,
929
930
  refinement: refinement,
@@ -1315,6 +1316,7 @@ module Zard
1315
1316
  namespace: declaration.namespace,
1316
1317
  visibility: visibility,
1317
1318
  parameters: parameters,
1319
+ parameter_details: declaration.parameter_details,
1318
1320
  receiver: declaration.receiver,
1319
1321
  receiver_span: declaration.receiver_span,
1320
1322
  refinement: declaration.refinement,
@@ -1332,6 +1334,47 @@ module Zard
1332
1334
  )
1333
1335
  end
1334
1336
 
1337
+ def declaration_parameters(node, kind, alias_target)
1338
+ return if alias_target || !%i[instance_method singleton_method].include?(kind)
1339
+
1340
+ parameters = if node.is_a?(Prism::DefNode)
1341
+ node.parameters
1342
+ elsif node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
1343
+ node.block.parameters&.parameters
1344
+ else
1345
+ return
1346
+ end
1347
+ return [].freeze unless parameters
1348
+
1349
+ groups = [[parameters.requireds, :required], [parameters.optionals, :optional],
1350
+ [[parameters.rest].compact, :rest], [parameters.posts, :post],
1351
+ [parameters.keywords, :required_keyword], [[parameters.keyword_rest].compact, :keyword_rest],
1352
+ [[parameters.block].compact, :block]]
1353
+ groups.flat_map do |nodes, kind|
1354
+ nodes.map { |parameter| parameter_detail(parameter, kind) }
1355
+ end.sort_by { |parameter| parameter.span.start_offset }.freeze
1356
+ end
1357
+
1358
+ def parameter_detail(parameter, kind)
1359
+ kind = case parameter
1360
+ when Prism::OptionalKeywordParameterNode then :optional_keyword
1361
+ when Prism::ForwardingParameterNode then :forwarding
1362
+ when Prism::NoKeywordsParameterNode then :no_keywords
1363
+ when Prism::ImplicitRestNode then :implicit_rest
1364
+ else kind
1365
+ end
1366
+ value = parameter.value if parameter.is_a?(Prism::OptionalParameterNode) || parameter.is_a?(Prism::OptionalKeywordParameterNode)
1367
+ if value.respond_to?(:heredoc?) && value.heredoc?
1368
+ @diagnostics << Model::V1::Diagnostic.new(code: "parameter.unsupported-default", severity: :warning,
1369
+ message: "Heredoc parameter defaults retain their header; a contiguous default expression is unavailable.", span: span(value.location))
1370
+ value = nil
1371
+ end
1372
+ Model::V1::Parameter.new(kind: kind,
1373
+ name: parameter.respond_to?(:name) ? parameter.name&.to_s : nil,
1374
+ source: parameter.location.slice, span: span(parameter.location),
1375
+ default: value&.location&.slice, default_span: value ? span(value.location) : nil)
1376
+ end
1377
+
1335
1378
  def parameter_names(parameters)
1336
1379
  return [] unless parameters
1337
1380
 
data/lib/zard/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zard
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
data/lib/zard.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "zard/version"
4
4
  require_relative "zard/model/v1"
5
5
  require_relative "zard/parsing/parser"
6
+ require_relative "zard/formatting/formatter"
6
7
 
7
8
  module Zard
8
9
  class Error < StandardError; end
@@ -11,5 +12,9 @@ module Zard
11
12
  Parsing::Parser.call(source, path: path)
12
13
  end
13
14
 
14
- private_constant :Parsing
15
+ def self.format(source, path:)
16
+ Formatting::Formatter.call(source, path: path)
17
+ end
18
+
19
+ private_constant :Parsing, :Formatting
15
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - USAMI Kenta
@@ -38,15 +38,20 @@ files:
38
38
  - docs/adr/0001-contract-and-documentation-channels.md
39
39
  - docs/adr/0002-em-dash-zard-documentation-marker.md
40
40
  - docs/adr/0003-package-boundaries-and-rigor-lens.md
41
+ - docs/adr/0004-documentation-reference-syntax.md
42
+ - docs/adr/0005-safe-annotation-formatting.md
43
+ - docs/adr/0006-hierarchical-documentation-output.md
41
44
  - docs/agents/domain.md
42
45
  - docs/agents/issue-tracker.md
43
46
  - docs/agents/triage-labels.md
47
+ - docs/json-export.md
44
48
  - docs/supported-ruby-syntax.md
45
49
  - docs/usage.md
46
50
  - examples/catalog/expected.md
47
51
  - examples/catalog/lib/catalog.rb
48
52
  - examples/catalog/lib/catalog/entry.rb
49
53
  - lib/zard.rb
54
+ - lib/zard/formatting/formatter.rb
50
55
  - lib/zard/model/v1.rb
51
56
  - lib/zard/parsing/parser.rb
52
57
  - lib/zard/version.rb
@@ -74,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
79
  - !ruby/object:Gem::Version
75
80
  version: '0'
76
81
  requirements: []
77
- rubygems_version: 4.0.20
82
+ rubygems_version: 4.0.16
78
83
  specification_version: 4
79
84
  summary: AI-friendly Ruby documentation built on RBS and Rigor extensions
80
85
  test_files: []