com.elestrago.unity.package-tools 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ Each namespace section follows this exact shape:
26
26
  {declaration line copied verbatim from source}
27
27
  ```
28
28
 
29
- **Source:** `{file}:{line}`
29
+ **Source:** `{file}:{line}` (md5: `{md5}`, lines: {lines})
30
30
  **Attributes:** `[Attr1]`, `[Attr2]` (omit line if none)
31
31
  **Base:** `{baseList}` (omit if empty)
32
32
 
@@ -34,9 +34,18 @@ Each namespace section follows this exact shape:
34
34
 
35
35
  **Members:**
36
36
 
37
- | Kind | Name | Signature | Summary |
38
- |------|------|-----------|---------|
39
- | method | Foo | `public void Foo()` | … |
37
+ | Kind | Name | Signature | Line | Summary |
38
+ |------|------|-----------|------|---------|
39
+ | method | Foo | `public void Foo()` | 88 | … |
40
+
41
+ The exact shape of the Source parenthetical — `(md5: \`<32-hex>\`, lines: <n>)` —
42
+ is the contract the Step 3.6 fast-update check greps for. Do not reflow it
43
+ (e.g. drop the backticks around the hex, swap the order, or put it on a new
44
+ line) — the check will silently stop skipping unchanged namespaces.
45
+
46
+ Members share the type's file; the **Line** column gives the line number
47
+ within that file. Anchors stay `{typename-lowercase}` regardless of the
48
+ file/line annotation.
40
49
 
41
50
  Omit types with no public members and no XML summary unless they carry a Unity attribute.
42
51
  Omit empty namespaces entirely.
@@ -15,6 +15,7 @@ parsing: good enough for well-formatted Unity editor code, not a full Roslyn par
15
15
  from __future__ import annotations
16
16
 
17
17
  import argparse
18
+ import hashlib
18
19
  import json
19
20
  import os
20
21
  import re
@@ -154,7 +155,7 @@ SUMMARY_LINE_RE = re.compile(r"^\s*///\s?(?P<text>.*)$")
154
155
 
155
156
  def parse_cs_file(path: Path, rel_path: str) -> dict:
156
157
  """Return {'types': [...], 'namespaces': set([...])} for one .cs file."""
157
- raw = path.read_text(encoding="utf-8", errors="replace")
158
+ raw = path.read_text(encoding="utf-8-sig", errors="replace")
158
159
  norm = normalize(raw)
159
160
  norm_lines = norm.split("\n")
160
161
  raw_lines = raw.split("\n")
@@ -353,7 +354,7 @@ def scan_asmdefs(package_dir: Path, repo_root: Path) -> list[dict]:
353
354
  results: list[dict] = []
354
355
  for p in package_dir.rglob("*.asmdef"):
355
356
  try:
356
- data = json.loads(p.read_text(encoding="utf-8"))
357
+ data = json.loads(p.read_text(encoding="utf-8-sig"))
357
358
  except json.JSONDecodeError:
358
359
  continue
359
360
  results.append(
@@ -383,7 +384,7 @@ def build_script_guid_map(repo_root: Path) -> dict[str, str]:
383
384
  return mapping
384
385
  for meta in assets.rglob("*.cs.meta"):
385
386
  try:
386
- text = meta.read_text(encoding="utf-8", errors="replace")
387
+ text = meta.read_text(encoding="utf-8-sig", errors="replace")
387
388
  except OSError:
388
389
  continue
389
390
  m = META_GUID_RE.search(text)
@@ -409,7 +410,7 @@ def scan_sample_dir(sample_dir: Path, repo_root: Path, package_types: set[str],
409
410
  suf = p.suffix.lower()
410
411
  if suf == ".cs":
411
412
  try:
412
- src = p.read_text(encoding="utf-8", errors="replace")
413
+ src = p.read_text(encoding="utf-8-sig", errors="replace")
413
414
  except OSError:
414
415
  continue
415
416
  norm = normalize(src)
@@ -417,10 +418,18 @@ def scan_sample_dir(sample_dir: Path, repo_root: Path, package_types: set[str],
417
418
  ns = ns_match.group("name") if ns_match else ""
418
419
  words = set(re.findall(r"\b([A-Z][A-Za-z0-9_]*)\b", norm))
419
420
  refs = sorted(words & package_types)
420
- results.append({"path": rel, "kind": "script", "namespace": ns, "packageTypesReferenced": refs})
421
+ md5, lines = _file_md5_and_lines(p)
422
+ results.append({
423
+ "path": rel,
424
+ "kind": "script",
425
+ "namespace": ns,
426
+ "packageTypesReferenced": refs,
427
+ "md5": md5,
428
+ "lines": lines,
429
+ })
421
430
  elif suf in (".unity", ".prefab"):
422
431
  try:
423
- text = p.read_text(encoding="utf-8", errors="replace")
432
+ text = p.read_text(encoding="utf-8-sig", errors="replace")
424
433
  except OSError:
425
434
  continue
426
435
  guids = sorted(set(SCRIPT_REF_RE.findall(text)))
@@ -429,7 +438,14 @@ def scan_sample_dir(sample_dir: Path, repo_root: Path, package_types: set[str],
429
438
  resolved = guid_map.get(g)
430
439
  if resolved and resolved in package_files:
431
440
  refs.append({"guid": g, "resolvedTo": resolved})
432
- results.append({"path": rel, "kind": "scene" if suf == ".unity" else "prefab", "scriptsReferenced": refs})
441
+ md5, lines = _file_md5_and_lines(p)
442
+ results.append({
443
+ "path": rel,
444
+ "kind": "scene" if suf == ".unity" else "prefab",
445
+ "scriptsReferenced": refs,
446
+ "md5": md5,
447
+ "lines": lines,
448
+ })
433
449
  return results
434
450
 
435
451
 
@@ -450,6 +466,28 @@ def iter_cs_files(root: Path) -> Iterable[Path]:
450
466
  yield p
451
467
 
452
468
 
469
+ def _file_md5_and_lines(path: Path) -> tuple[str, int]:
470
+ """Return (md5 hex digest, line count) for a file. MD5 is over raw bytes —
471
+ line endings are not normalized, so a CRLF↔LF swap counts as a change, which
472
+ is what we want for change detection."""
473
+ h = hashlib.md5()
474
+ lines = 0
475
+ with path.open("rb") as f:
476
+ while True:
477
+ chunk = f.read(65536)
478
+ if not chunk:
479
+ break
480
+ h.update(chunk)
481
+ lines += chunk.count(b"\n")
482
+ # If the file does not end with a newline, count its trailing line.
483
+ if path.stat().st_size > 0:
484
+ with path.open("rb") as f:
485
+ f.seek(-1, os.SEEK_END)
486
+ if f.read(1) != b"\n":
487
+ lines += 1
488
+ return h.hexdigest(), lines
489
+
490
+
453
491
  # ---------------------------------------------------------------------------
454
492
  # Main
455
493
  # ---------------------------------------------------------------------------
@@ -470,11 +508,20 @@ def main(argv: list[str]) -> int:
470
508
  all_types: list[dict] = []
471
509
  all_namespaces: set[str] = set()
472
510
  package_files: set[str] = set()
511
+ file_records: list[dict] = []
512
+ file_md5_by_path: dict[str, str] = {}
473
513
 
474
514
  for cs in iter_cs_files(package_dir):
475
515
  rel = _rel(cs, repo_root)
476
516
  package_files.add(rel)
517
+ md5, lines = _file_md5_and_lines(cs)
518
+ file_md5_by_path[rel] = md5
519
+ file_records.append({"path": rel, "md5": md5, "lines": lines})
477
520
  result = parse_cs_file(cs, rel)
521
+ # Stamp every type with the md5 of its containing file so consumers can
522
+ # render it inline without a second lookup.
523
+ for t in result["types"]:
524
+ t["md5"] = md5
478
525
  all_types.extend(result["types"])
479
526
  all_namespaces.update(result["namespaces"])
480
527
 
@@ -491,6 +538,7 @@ def main(argv: list[str]) -> int:
491
538
  "packageDir": _rel(package_dir, repo_root),
492
539
  "asmdefs": asmdefs,
493
540
  "namespaces": sorted(all_namespaces),
541
+ "files": sorted(file_records, key=lambda r: r["path"]),
494
542
  "types": all_types,
495
543
  "scriptGuidCount": len(guid_map),
496
544
  "samples": samples,
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "com.elestrago.unity.package-tools",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "displayName": "Package Tool",
5
5
  "description": "Tool for create unity packages",
6
6
  "category": "unity",
7
7
  "unity": "2021.3",
8
8
  "homepage": "https://gitlab.com/elestrago-pkg/package-tool",
9
- "documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.2/README.md",
10
- "changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.2/CHANGELOG.md",
11
- "licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.2.2/LICENSE",
9
+ "documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.3.0/README.md",
10
+ "changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.3.0/CHANGELOG.md",
11
+ "licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.3.0/LICENSE",
12
12
  "license": "MIT",
13
13
  "keywords": [
14
14
  "unity",
@@ -1,309 +0,0 @@
1
- ---
2
- name: unity-package-docs
3
- description: Create or update AI-first documentation for a Unity package at the repository root — README.md plus Documentation~/manual.md, api.md, samples.md, all structured for dense AI ingestion with explicit file paths and explicit mapping between the package source and the sample project. Auto-chunks large API/manual/samples by namespace/section to keep navigation intact. Use when the user wants to generate or refresh Unity package docs from source. Works in any Unity package repo with one package per repo (discovered via package.json, with a fallback to PackageManifestConfig.asset for package-tool-based projects).
4
- ---
5
-
6
- # unity-package-docs
7
-
8
- Generate a strict, AI-first documentation set for a Unity package at the **repository root** (cwd). The output is consumed primarily by AI agents (Claude, Copilot, Cursor) and secondarily by humans.
9
-
10
- ## Output contract (strict)
11
-
12
- Files created/updated at the repository root — **never** inside the package folder:
13
-
14
- ```
15
- <repo-root>/
16
- ├── README.md
17
- └── Documentation~/
18
- ├── manual.md
19
- ├── api.md
20
- └── samples.md
21
- ```
22
-
23
- The `Documentation~` folder name MUST include the trailing tilde (Unity convention — excludes it from asset import). Pre-existing files inside `Documentation~/` (e.g. screenshots) are left untouched; only the three `.md` files listed are managed.
24
-
25
- Every generated `.md` file begins with this exact marker on line 1:
26
-
27
- ```
28
- <!-- generated by unity-package-docs; safe to regenerate -->
29
- ```
30
-
31
- ## AI-first content rules (non-negotiable, apply to every generated file)
32
-
33
- 1. **Dense over decorative.** No emojis. No marketing prose. No horizontal rules (`---`) used as visual separators. Use heading hierarchy (H1 → H2 → H3) semantically, never as styling.
34
- 2. **Predictable structure.** Use the exact section names and order specified in each step below. Same anchor slugs every run so agents crawling multiple package docs can rely on the shape.
35
- 3. **Explicit paths everywhere.** Every reference to source code uses `path/to/File.cs` relative to the repo root, with `:line` when pointing at a specific declaration. Never write "see the inspector class" — write `see Assets/Package/.../Inspectors/Foo.cs:33`.
36
- 4. **Complete, tagged code blocks.** Every fenced block has a language tag (` ```csharp `, ` ```json `, ` ```yaml `, ` ```bash `). Snippets are paste-ready — no `// ...` elisions that lose context.
37
- 5. **Explicit package ↔ sample mapping.** In `samples.md`, every sample file is mapped to the exact package type(s) it instantiates or extends, with file paths on both sides. An agent reading the docs must be able to write equivalent code in a fresh project.
38
- 6. **No forward references to undocumented things.** If `manual.md` mentions a type, that type appears in `api.md` with a stable anchor. `samples.md` links into `api.md` rather than re-describing types.
39
-
40
- ## Skill args
41
-
42
- Parse from the user input (default values shown):
43
-
44
- - `force=false` — when `true`, overwrite files even if they lack the generator marker.
45
- - `dry-run=false` — when `true`, print the would-be output to the conversation without writing any files.
46
- - `package=<path>` — override package-root auto-discovery (for repos with multiple packages or a non-standard layout).
47
- - `chunk-threshold-lines=800` — when a rendered Documentation~ file's draft exceeds this many lines, the file is split into chunks (see Step 3.5). Below the threshold, output is identical to today's single-file form. Set very high (e.g. `100000`) to disable chunking entirely.
48
- - `chunk-threshold-api=<n>`, `chunk-threshold-manual=<n>`, `chunk-threshold-samples=<n>` — per-file overrides of `chunk-threshold-lines`. Each defaults to the global value.
49
-
50
- ## Pipeline (execute these steps in order)
51
-
52
- ### Step 1 — Locate the package
53
-
54
- From cwd, find `package.json` files via:
55
-
56
- ```bash
57
- find . -type f -name package.json \
58
- -not -path './Library/*' \
59
- -not -path './Temp/*' \
60
- -not -path './Logs/*' \
61
- -not -path './obj/*' \
62
- -not -path './Build/*' \
63
- -not -path './Builds/*' \
64
- -not -path './Packages/manifest.json' \
65
- -not -path './Packages/packages-lock.json'
66
- ```
67
-
68
- - **Exactly one match** → that file is the package manifest. Its containing directory is the **package root**. Read it.
69
- - **Zero matches but `Assets/**/PackageManifestConfig.asset` exists** (package-tool projects pre-first-export) → use the config asset as the metadata source. The `sourcePath` field is the package root. Note in `manual.md` that the `package.json` is generated to `<packageDestinationPath>/` on export.
70
- - **More than one match, or zero matches with no config** → abort. Print the matches found and ask the user to disambiguate via `package=<path>`.
71
-
72
- ### Step 2 — Read metadata
73
-
74
- From `package.json` (preferred) or `PackageManifestConfig.asset`, extract:
75
-
76
- | Field | `package.json` key | Config key |
77
- |-------|-------------------|------------|
78
- | Package name | `name` | `packageName` |
79
- | Display name | `displayName` | `displayName` |
80
- | Version | `version` | `packageVersion` |
81
- | Description | `description` | `description` |
82
- | Unity version | `unity` | `unityVersion` |
83
- | Author | `author` | `author.name` |
84
- | Dependencies | `dependencies` (object) | `dependencies` (array of `{packageName, packageVersion}`) |
85
- | Keywords | `keywords` | `keywords` |
86
- | Samples | `samples` (array of `{path, displayName, description}`) | `samples` (array of `{sourcePath, folderName, displayName, description}`) |
87
- | Registry | `publishConfig.registry` | (not in config; omit) |
88
-
89
- For the config fallback, the **sample source paths** on disk are taken from `samples[].sourcePath`. For `package.json` projects, the on-disk sample paths are inside the package dir at `<package-root>/Samples~/<folderName>` (only present if previously exported); if absent, scan `Assets/Samples/`, `Samples/`, and `Assets/Example/` as fallback candidates.
90
-
91
- ### Step 3 — Scan source
92
-
93
- Run the bundled Python scanner. Its absolute path is `${SKILL_DIR}/scripts/scan_package.py` where `${SKILL_DIR}` is the directory containing this SKILL.md. Resolve it relative to where this skill is installed; do not hardcode.
94
-
95
- ```bash
96
- python3 "${SKILL_DIR}/scripts/scan_package.py" \
97
- --package "<package-root>" \
98
- --samples "<sample-dir-1>" --samples "<sample-dir-2>" \
99
- --repo-root .
100
- ```
101
-
102
- The scanner emits JSON to stdout with this shape:
103
-
104
- ```json
105
- {
106
- "packageDir": "Assets/Package/PackageTool",
107
- "asmdefs": [{"path": "...", "name": "...", "rootNamespace": "...", "includePlatforms": [...], "excludePlatforms": [...], "references": [...], "precompiledReferences": [...], "defineConstraints": [...], "autoReferenced": true}],
108
- "namespaces": ["PackageTool", "PackageTool.Tools", ...],
109
- "types": [
110
- {
111
- "file": "Assets/.../Foo.cs",
112
- "line": 37,
113
- "namespace": "PackageTool",
114
- "name": "Foo",
115
- "kind": "class|struct|interface|enum",
116
- "modifiers": ["public", "sealed"],
117
- "baseList": ["ScriptableObject"],
118
- "attributes": ["CreateAssetMenu(...)", "Serializable"],
119
- "summary": "...",
120
- "containingType": null,
121
- "members": [
122
- {"line": 88, "kind": "method|property|field", "name": "Bar", "signature": "public string Bar()", "summary": "...", "attributes": ["MenuItem(\"Tools/...\")"]}
123
- ]
124
- }
125
- ],
126
- "samples": [
127
- {"path": "Assets/.../Foo.cs", "kind": "script", "namespace": "...", "packageTypesReferenced": ["Foo", "Bar"]},
128
- {"path": "Assets/.../Scene.unity", "kind": "scene|prefab", "scriptsReferenced": [{"guid": "...", "resolvedTo": "Assets/.../Foo.cs"}]}
129
- ]
130
- }
131
- ```
132
-
133
- Capture this JSON; the rest of the pipeline consumes it.
134
-
135
- ### Step 3.5 — Plan chunking
136
-
137
- For each Documentation~ file (`api.md`, `manual.md`, `samples.md`), decide whether to emit it as a single file or as a chunk index plus per-chunk files. Decision procedure:
138
-
139
- 1. **Render to an in-memory buffer first.** Run the full Step 4/5/6 rendering logic into a string. Count its lines (`\n`-separated, EOF-trimmed).
140
- 2. **Resolve the threshold** for this file: use the file-specific arg if supplied (`chunk-threshold-api`, `chunk-threshold-manual`, `chunk-threshold-samples`), else the global `chunk-threshold-lines` (default 800).
141
- 3. **Apply the structural floor.** Skip chunking even when the threshold is exceeded if the file is structurally unsplittable:
142
- - `api.md`: chunk only when `namespaces.length > 1`.
143
- - `samples.md`: chunk only when the sample mapping covers more than one file (i.e. `samples.length > 1`).
144
- - `manual.md`: chunk only when at least one H2 section in the draft individually exceeds the threshold. (Splitting a single oversized section yields no benefit.)
145
- 4. **Decide:** if `linesDrafted > threshold` AND the structural floor passes → **chunked**; else → **single-file** (write the draft as-is at Step 4/5/6's write step).
146
-
147
- For each file decided to chunk, plan the chunk set:
148
-
149
- - `api.md` → one chunk per namespace: `Documentation~/api/<Namespace>.md` (literal namespace, no slugification — e.g. `Documentation~/api/PackageTool.Tools.md`). Do NOT sub-split a single huge namespace.
150
- - `samples.md` → one chunk per top-level sample folder: `Documentation~/samples/<SampleFolderName>.md`. Sanitize `<SampleFolderName>` to `[A-Za-z0-9._-]` (replace any other character with `-`).
151
- - `manual.md` → one chunk per H2 section using this fixed slug set: `overview.md`, `architecture.md`, `assemblies.md`, `entry-points.md`, `data-model.md`, `editor-ui.md`, `extension-points.md` (these correspond 1:1 to the H2 sections in `manual.md.template`). Sections that render empty are omitted from the chunk set.
152
-
153
- Build a **`crossRefMap`**: `{ typeAnchor → "<destination-relative-to-Documentation~>#<anchor>" }`, populated during the api decision:
154
-
155
- - When api is single-file: every entry is `api.md#<anchor>` (identity with the legacy shape).
156
- - When api is chunked: each entry is `api/<Namespace>.md#<anchor>`, where `<Namespace>` is the namespace that contains the type (from `types[i].namespace`). Nested types use `containingType`'s namespace.
157
-
158
- `crossRefMap` is consumed by Steps 5 and 6 when emitting `[Type](…)` references. Renderers MUST NOT hard-code `api.md` in link destinations — always resolve via `crossRefMap`.
159
-
160
- Print one line per file decision, e.g. `chunk-plan: api.md → chunked (12 namespaces, draft=2143 lines, threshold=800)` or `chunk-plan: samples.md → single (draft=78 lines)`.
161
-
162
- ### Step 4 — Render `Documentation~/api.md` first
163
-
164
- Render this file first because `manual.md` and `samples.md` link into its anchors (via `crossRefMap` from Step 3.5) and you want those anchors to exist.
165
-
166
- Read `${SKILL_DIR}/assets/api.md.template`. Replace `{{displayName}}` with the discovered value. Replace `{{namespaceContentsList}}` with a bulleted index of namespaces, each linking to its anchor. Replace `{{perNamespaceSections}}` with one section per namespace following the structure documented in the template's HTML comment block.
167
-
168
- For each type entry:
169
-
170
- - Heading: `### {TypeName}` (stable; the anchor is `{typename-lowercase}` by default Markdown rules; for nested types use `### {ContainingType}.{TypeName}`).
171
- - ` ```csharp ` block with the declaration line copied verbatim from source (use the file + line in the manifest to extract the line; do not paraphrase).
172
- - `**Source:** ` + `path:line`.
173
- - `**Attributes:** ` + comma-separated list (omit the line if none).
174
- - `**Base:** ` + comma-separated `baseList` (omit if empty).
175
- - Summary verbatim if present.
176
- - Members table: only include public/internal/protected members; omit private. Strip XML tags from member summaries.
177
-
178
- Omit types with no public members and no XML summary unless they carry a Unity attribute (`[MenuItem]`, `[CustomEditor]`, etc.). Omit empty namespaces entirely.
179
-
180
- **Output (single-file branch, default):** if Step 3.5 decided api is single-file, write the rendered draft to `Documentation~/api.md` as-is.
181
-
182
- **Output (chunked branch):** if Step 3.5 decided api is chunked, split the draft by namespace and emit:
183
-
184
- 1. **One chunk per namespace** at `Documentation~/api/<Namespace>.md` using `${SKILL_DIR}/assets/api-chunk.md.template`. Each chunk contains the H1 (`# {{displayName}} — API Reference: \`{{namespace}}\``), the namespace's section body (same `### TypeName` blocks as the single-file form), and the generator marker on line 1. Anchors inside the chunk stay `{typename-lowercase}` — they do NOT include the namespace prefix, because cross-doc links resolve via `crossRefMap` (which carries the path-plus-anchor pair).
185
- 2. **One index** at `Documentation~/api.md` using `${SKILL_DIR}/assets/api-index.md.template`. The index keeps the generator marker, H1, a `## Contents` Markdown table linking each chunk (`| Namespace | Description |`), and a one-line `## Notes` paragraph: `This API reference is split by namespace; see files under \`api/\`.`. The one-line description per namespace is the same blurb you would have placed in the single-file `{{namespaceContentsList}}`.
186
- 3. **Stale chunks:** before writing, list `Documentation~/api/*.md`. For each existing file that starts with the generator marker and is NOT in the current chunk set, delete it and print `Removed stale: <path>`. (See Idempotency.)
187
-
188
- ### Step 5 — Render `Documentation~/manual.md`
189
-
190
- Read `${SKILL_DIR}/assets/manual.md.template`. Fill each placeholder:
191
-
192
- - `{{overviewProse}}` — 2–4 dense paragraphs synthesizing what the package does. Read `description` plus the XML summaries of the top 3–5 most-referenced types (highest member counts or carrying Unity attributes).
193
- - `{{namespaceTree}}` — Plain ASCII tree of namespaces, sorted, with no decoration.
194
- - `{{architectureProse}}` — One short paragraph per top-level namespace explaining its responsibility. Reference at least one representative file path per namespace.
195
- - `{{assembliesTableRows}}` — One Markdown table row per `.asmdef` (name, root namespace or `—`, platforms joined with `,` or `Any`, references joined with `,` or `—`).
196
- - `{{entryPointsList}}` — Bulleted list. One bullet per `[MenuItem]` (show menu path), one per `[CreateAssetMenu]` (show `menuName`), one per public static method on a public static class (CI/CLI candidates). Each bullet ends with `— path:line`. Include the type's XML summary if non-empty.
197
- - `{{dataModelList}}` — Bulleted list of every type with base `ScriptableObject` and every type with `[Serializable]`. For each, list its public fields as a sub-bulleted list with their types.
198
- - `{{editorUiProse}}` — Detect IMGUI vs UI Toolkit by checking for `.uxml`/`.uss` files under the package root and for types inheriting `EditorWindow` / `PropertyDrawer` / `UnityEditor.Editor`. State which approach is used and list representative file paths.
199
- - `{{extensionPointsList}}` — Public abstract types, public virtual methods, types ending in `Base`, or those marked `partial`. If none, write `None.`.
200
-
201
- **Resolving `[Type](…)` links.** When rendering any link to a package type, look it up in `crossRefMap` (built in Step 3.5). Use the value verbatim. Do NOT hard-code `api.md#anchor`. If the link is emitted from a manual chunk file (chunked branch), the value must be prefixed with `../` (because chunks live under `manual/`); the renderer prepends `../` to every `crossRefMap` value when emitting from a chunk file, and emits the bare value when emitting from the index-level `manual.md`. Same rule applies to samples in Step 6.
202
-
203
- **Output (single-file branch, default):** if Step 3.5 decided manual is single-file, write the rendered draft to `Documentation~/manual.md` as-is.
204
-
205
- **Output (chunked branch):** if Step 3.5 decided manual is chunked, split the draft along its H2 boundaries and emit:
206
-
207
- 1. **One chunk per non-empty H2 section** at `Documentation~/manual/<slug>.md`, using the fixed slug set from Step 3.5 (`overview.md`, `architecture.md`, `assemblies.md`, `entry-points.md`, `data-model.md`, `editor-ui.md`, `extension-points.md`). Each chunk contains the generator marker on line 1, an H1 echoing the section title (`# {{displayName}} — Manual: <Section Title>`), then the section body (everything that was below the H2 in the single-file draft, with the H2 line itself dropped). Within a chunk, all `[Type](…)` link values from `crossRefMap` are prefixed with `../`.
208
- 2. **One index** at `Documentation~/manual.md` containing the generator marker, the H1 (`# {{displayName}} — Manual`), a `## Contents` Markdown table (`| Section | Description |`) linking each chunk (one row per emitted slug; description = the first sentence of that section's body), and one-line `## Notes`: `This manual is split by section; see files under \`manual/\`.`
209
- 3. **Stale chunks:** before writing, list `Documentation~/manual/*.md`. For each existing file that starts with the generator marker and is NOT in the current chunk set, delete it and print `Removed stale: <path>`.
210
-
211
- ### Step 6 — Render `Documentation~/samples.md`
212
-
213
- Read `${SKILL_DIR}/assets/samples.md.template`. Fill placeholders:
214
-
215
- - `{{sampleTree}}` — ASCII tree of the discovered sample dir(s), one line per file.
216
- - `{{sampleMappingSections}}` — One `### path` subsection per sample file, following the structure documented in the template's HTML comment. For scripts, list referenced package types as links resolved through `crossRefMap` (Step 3.5). For scenes/prefabs, list resolved script GUIDs as links resolved through `crossRefMap`.
217
- - `{{reproductionProse}}` — One paragraph explaining how a downstream user reproduces the sample setup from scratch.
218
- - `{{reproductionSnippet}}` — Paste-ready `csharp` snippet using only public API documented in api.md (or its chunks). If the sample is empty, omit the snippet section entirely and write a stub (see edge cases).
219
- - `{{notesList}}` — Any sample-only conventions worth knowing.
220
-
221
- **Resolving `[Type](…)` links.** Same rule as Step 5: look up every type link in `crossRefMap`. From chunked output, prepend `../` (chunks live under `samples/`); from the index-level `samples.md`, emit the bare value.
222
-
223
- **Edge case — empty sample:** if no sample files exist (the dir is empty or absent), emit a single-file `Documentation~/samples.md` with the `<!-- generated -->` marker, the H1, and a single section: `## No sample content` containing one sentence: `No sample content present. Add files under \`<expected sample path>\` and re-run this skill.`. Do not fail and do not chunk.
224
-
225
- **Output (single-file branch, default):** if Step 3.5 decided samples is single-file, write the rendered draft to `Documentation~/samples.md` as-is.
226
-
227
- **Output (chunked branch):** if Step 3.5 decided samples is chunked, group sample files by their **top-level sample folder** (the first path segment under the sample root, e.g. `Assets/Example/Sample/Foo.cs` → folder `Sample`) and emit:
228
-
229
- 1. **One chunk per top-level sample folder** at `Documentation~/samples/<SampleFolderName>.md` (sanitized to `[A-Za-z0-9._-]`). Each chunk contains the generator marker on line 1, H1 `# {{displayName}} — Samples: <SampleFolderName>`, the per-sample `### path` subsections that belong to that folder, the per-folder `## Reproducing the Sample` prose + snippet, and `## Notes`. Inside chunks, `crossRefMap` link values are prefixed with `../`.
230
- 2. **One index** at `Documentation~/samples.md` containing the generator marker, H1, the global `## Sample Layout` tree, a `## Contents` Markdown table (`| Sample | Description |`) linking each chunk, and `## Notes`: `Samples are split by sample folder; see files under \`samples/\`.`
231
- 3. **Stale chunks:** before writing, list `Documentation~/samples/*.md`. For each existing file that starts with the generator marker and is NOT in the current chunk set, delete it and print `Removed stale: <path>`.
232
-
233
- ### Step 7 — Render `README.md`
234
-
235
- Read `${SKILL_DIR}/assets/README.md.template`. Fill placeholders:
236
-
237
- - `{{displayName}}`, `{{description}}` — from metadata.
238
- - `{{platformConstraintsBullets}}` — bullet per `.asmdef` with a non-empty `includePlatforms` or `excludePlatforms`. Omit the whole line if all asmdefs allow all platforms.
239
- - `{{dependenciesTable}}` — Markdown table (`| Package | Version |`) of dependencies. If none, write `None.`.
240
- - `{{installManifestSnippet}}` — JSON snippet showing the `manifest.json` edit. Include `scopedRegistries` with the URL from `publishConfig.registry` only if present in `package.json`; otherwise omit and add a one-line note in `{{scopedRegistryNote}}` reading `Replace the registry URL above with your scoped registry, or remove the scopedRegistries block when consuming a published package.` (when present, the note is empty).
241
- - `{{gettingStartedProse}}` — one paragraph. Identify the primary entry point:
242
- 1. The first type with `[CreateAssetMenu]` → describe `Assets > Create > <menuName>`.
243
- 2. Else the first class with a `[MenuItem]` method → describe the menu path.
244
- 3. Else the first public static method on a public type → describe a direct call.
245
- - `{{gettingStartedSnippet}}` — minimal `csharp` snippet exercising the primary entry point.
246
- - `{{licensePath}}` — path to the root `LICENSE` file (typically `LICENSE`).
247
-
248
- Write `README.md` at the repo root.
249
-
250
- ### Step 8 — Verify
251
-
252
- After all files (including any chunks) are written:
253
-
254
- 1. Grep `README.md` for `Documentation~/manual.md`, `Documentation~/api.md`, `Documentation~/samples.md`. Confirm each file exists. Abort with a clear error if any link is broken. (These three top-level paths are unchanged whether the file is single or chunked — when chunked the file is a Contents-table index, but the path still resolves.)
255
- 2. Walk every emitted `.md` under `Documentation~/` (index files and chunk files). For each `[Type](…)` link extract the destination path (relative to the link's own file) and anchor. Resolve the destination to an absolute path under `Documentation~/`; confirm the file exists and contains an `### {anchor}` heading (case-insensitive, hyphen-normalized per Markdown's auto-slug rule). Print warnings (not errors) for any broken anchors. Replaces the legacy "grep `api.md#`" check, which is no longer sufficient because anchors now live in either `api.md` or `api/<Namespace>.md`.
256
- 3. Confirm every generated file — including every chunk under `Documentation~/api/`, `Documentation~/manual/`, `Documentation~/samples/` — starts with the generator marker on line 1.
257
- 4. Confirm no chunk file outside the current chunk set survived (the stale-removal in Steps 4/5/6 should have deleted them; this is a safety re-check).
258
-
259
- Print a summary: which files were created vs updated, which were skipped (and why), and which chunks were removed as stale.
260
-
261
- ## Idempotency
262
-
263
- For each output file:
264
-
265
- 1. If the file does not exist → write it.
266
- 2. If the file exists and starts with the generator marker → overwrite it.
267
- 3. If the file exists and does NOT start with the marker → **do not overwrite** unless `force=true` was passed. Print: `Skipped <path>: marker absent (file is hand-edited or pre-existing). Re-run with force=true to overwrite.`
268
-
269
- `Documentation~/` pre-existing assets (PNG, JPG, etc.) are never touched regardless of `force`.
270
-
271
- ### Stale chunks
272
-
273
- Chunk files (under `Documentation~/api/`, `Documentation~/manual/`, `Documentation~/samples/`) follow the same marker-on-line-1 contract as top-level generated files. Additionally:
274
-
275
- 4. **Stale removal (marker-gated).** Before writing the chunk set for an area, list every `.md` file in that area's chunk directory. For each existing file that **starts with the generator marker** AND is **not in the current chunk set**, delete it and print `Removed stale: <path>`. Files without the marker are left in place and a warning is printed (matches rule 3 — never silently destroy hand-edited content).
276
- 5. **Mode flip (single ↔ chunked).** When a Documentation~ file flips from chunked to single-file between runs (e.g. the user removes namespaces until the threshold is no longer exceeded), every file in that area's chunk directory is treated as stale by rule 4: marker-bearing chunks are deleted, marker-absent files are warned about. The chunk directory itself is left in place even when empty (Unity treats empty directories as no-ops; do not `rmdir`).
277
- 6. **Force.** `force=true` extends rules 3 and 4 — marker-absent files are overwritten (rule 3) and marker-absent chunks are also deleted when stale (rule 4 extension), matching the existing semantics for top-level files.
278
-
279
- The pipeline as a whole is idempotent: re-running on an unchanged repo produces a no-op diff.
280
-
281
- ## Examples of correct output style
282
-
283
- Good (rule 3 — explicit paths):
284
-
285
- > The CI entry point is `CIUtils.Generate` at `Assets/Package/PackageTool/Editor/CIUtils.cs:65`. It is invoked in batch mode and reads command-line keys parsed by `CommandLineTools.GetKVPCommandLineArguments` at `Assets/Package/PackageTool/Editor/Tools/CommandLineTools.cs:43`.
286
-
287
- Bad (rule 3 violation):
288
-
289
- > The CI entry point is `CIUtils.Generate`. It reads command-line keys from a helper.
290
-
291
- Good (rule 4 — tagged, complete code blocks):
292
-
293
- ````
294
- ```csharp
295
- var config = ScriptableObject.CreateInstance<PackageManifestConfig>();
296
- config.packageName = "com.example.foo";
297
- FileTools.CreateOrUpdatePackageSource(config);
298
- ```
299
- ````
300
-
301
- Bad (rule 4 violation):
302
-
303
- ````
304
- ```
305
- var config = ...
306
- // ... fill in fields ...
307
- FileTools.CreateOrUpdatePackageSource(config);
308
- ```
309
- ````
@@ -1,42 +0,0 @@
1
- <!-- generated by unity-package-docs; safe to regenerate -->
2
-
3
- # {{displayName}}
4
-
5
- {{description}}
6
-
7
- ## Requirements
8
-
9
- - Unity {{unityVersion}} or newer.
10
- {{platformConstraintsBullets}}
11
-
12
- ## Dependencies
13
-
14
- {{dependenciesTable}}
15
-
16
- ## Installation
17
-
18
- Add the package to `Packages/manifest.json`:
19
-
20
- ```json
21
- {{installManifestSnippet}}
22
- ```
23
-
24
- {{scopedRegistryNote}}
25
-
26
- ## Getting Started
27
-
28
- {{gettingStartedProse}}
29
-
30
- ```csharp
31
- {{gettingStartedSnippet}}
32
- ```
33
-
34
- ## Documentation
35
-
36
- - [Manual](Documentation~/manual.md) — concepts and architecture.
37
- - [API Reference](Documentation~/api.md) — every public type and member.
38
- - [Samples](Documentation~/samples.md) — how the sample project in this repo uses the package.
39
-
40
- ## License
41
-
42
- See [{{licensePath}}]({{licensePath}}).
@@ -1,41 +0,0 @@
1
- <!-- generated by unity-package-docs; safe to regenerate -->
2
-
3
- # {{displayName}} — API Reference: `{{namespace}}`
4
-
5
- {{typeSections}}
6
-
7
- <!--
8
- Used only when api.md is chunked (Step 4 chunked branch).
9
- {{namespace}} is the literal namespace, matching the chunk filename
10
- (Documentation~/api/<Namespace>.md).
11
-
12
- {{typeSections}} is one section per type in this namespace, identical
13
- in shape to a per-type block in the single-file api.md template:
14
-
15
- ### {TypeName}
16
-
17
- ```csharp
18
- {declaration line copied verbatim from source}
19
- ```
20
-
21
- **Source:** `{file}:{line}`
22
- **Attributes:** `[Attr1]`, `[Attr2]` (omit line if none)
23
- **Base:** `{baseList}` (omit if empty)
24
-
25
- {summary}
26
-
27
- **Members:**
28
-
29
- | Kind | Name | Signature | Summary |
30
- |------|------|-----------|---------|
31
- | method | Foo | `public void Foo()` | … |
32
-
33
- Anchors are `{typename-lowercase}` (no namespace prefix). Cross-doc links
34
- from manual.md / samples.md resolve via crossRefMap, which carries the
35
- chunk-relative path-plus-anchor pair (e.g. `api/PackageTool.Tools.md#filetools`).
36
- For nested types use `### {ContainingType}.{TypeName}` and anchor
37
- `{containingtype}.{typename}`.
38
-
39
- Omit types with no public members and no XML summary unless they carry a
40
- Unity attribute (`[MenuItem]`, `[CustomEditor]`, etc.).
41
- -->
@@ -1,26 +0,0 @@
1
- <!-- generated by unity-package-docs; safe to regenerate -->
2
-
3
- # {{displayName}} — API Reference
4
-
5
- This API reference is split by namespace because the rendered single-file draft exceeded the chunk threshold (see `chunk-threshold-api` / `chunk-threshold-lines` in the unity-package-docs skill).
6
-
7
- ## Contents
8
-
9
- | Namespace | Description |
10
- |-----------|-------------|
11
- {{namespaceTableRows}}
12
-
13
- ## Notes
14
-
15
- This API reference is split by namespace; see files under `api/`. Cross-doc links from `manual.md` and `samples.md` point directly into the relevant chunk file (e.g. `api/PackageTool.Tools.md#filetools`); this index is intentionally thin and stable so external links to `Documentation~/api.md` keep resolving.
16
-
17
- <!--
18
- Used only when api.md is chunked (Step 4 chunked branch).
19
-
20
- {{namespaceTableRows}} is one Markdown table row per namespace:
21
-
22
- | [`PackageTool`](api/PackageTool.md) | Root namespace; holds the configuration ScriptableObject. |
23
-
24
- The description is the same one-line blurb that would have appeared in
25
- the single-file template's {{namespaceContentsList}}.
26
- -->