mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
docs: a documentation standard with named owners, held by repolint
Documents on the studio line had no owner, no status and no shape. Of
120 Markdown files, 544 paragraphs and list items ran past 320
characters. Chinese copies had drifted from the English ones (SPEC.md
1301 lines, SPEC.zh-CN.md 492). CODEOWNERS still named five paths that
no longer exist. The Studio release doc claimed a required approval and a
main-v2-only signing policy, neither of which is true any more.
- docs/DOCS_STANDARD.md states the rules as ID tables: ownership,
document classes, header, writing width, section shape per type,
language, and lifecycle.
- .github/CODEOWNERS gives every area a primary and a backup, in that
order. Every rule still lists both maintainers, so review requests do
not change.
- docs/STUDIO_RELEASE.md becomes a runbook: owners, preconditions,
steps, verification, recovery, release-note format, reference. Each
fact was checked against release-studio.yml, the environment's branch
policy and the 2.17.0 release.
- repolint gains five rules:
- codeowners: a rule must match a file and name two people.
- doc-owner: a document header's owner and backup must equal the
matching CODEOWNERS rule.
- doc-prose: a block must stay within 320 display columns, with a CJK
character counting as two.
- doc-language: only three onboarding pages keep a Chinese copy.
- release-note: fixed group headings, one referenced short line per
change.
Document classes are declared by path. Landing pages, model-read files,
GitHub templates and release notes carry no header, because each is
rendered or consumed verbatim; CODEOWNERS owns them.
Baseline: -allow-widen raises only the five new rules, from nothing to
the debt that exists today (doc-owner 91, doc-prose 2755 weighted,
doc-language 33, release-note 661, codeowners 0). No existing rule
widened. Deleting stale documents and the untranslated copies follows
in its own change, after the list is reviewed.
This commit is contained in:
1 parent
44150b0aa4
commit
6b6a41d367
8 files changed
+1312
-130
No files matched your search
+30
-15
@@ -1,16 +1,31 @@
|
||||
# Every pull request requests review from the maintainers below.
|
||||
# Either maintainer can review and merge any change.
|
||||
* @SivanCola @esengine
|
||||
# Ownership for the studio line. Standard: docs/DOCS_STANDARD.md.
|
||||
#
|
||||
# Each rule lists exactly two owners: primary first, backup second.
|
||||
# GitHub requests review from both. The last matching rule wins.
|
||||
# repolint (codeowners, doc-owner) fails a rule for a missing path, a rule
|
||||
# without a backup, and a document header that names different owners.
|
||||
|
||||
# Keep the provider-side signing policy and its GitHub control plane under the
|
||||
# same explicit review boundary as the machine-readable contract.
|
||||
/.signpath/ @SivanCola @esengine
|
||||
/.github/workflows/release-stable.yml @SivanCola @esengine
|
||||
/.github/workflows/release-stable-trigger.yml @SivanCola @esengine
|
||||
/.github/workflows/release-desktop.yml @SivanCola @esengine
|
||||
/.github/workflows/release-studio.yml @SivanCola @esengine
|
||||
/scripts/complete-signpath-request.ps1 @SivanCola @esengine
|
||||
/scripts/package-windows-desktop.sh @SivanCola @esengine
|
||||
/scripts/verify-windows-authenticode.ps1 @SivanCola @esengine
|
||||
/scripts/studio-build.sh @SivanCola @esengine
|
||||
/cmd/signpath-contract/ @SivanCola @esengine
|
||||
# Default: kernel, Studio shell, tooling, standing instructions.
|
||||
* @esengine @SivanCola
|
||||
|
||||
# User-facing guides and references.
|
||||
/docs/ @SivanCola @esengine
|
||||
/sdk/ @SivanCola @esengine
|
||||
/README.md @SivanCola @esengine
|
||||
/README.zh-CN.md @SivanCola @esengine
|
||||
/SECURITY.md @SivanCola @esengine
|
||||
/CHANGELOG.md @SivanCola @esengine
|
||||
|
||||
# Contracts and processes the Studio line runs on.
|
||||
/docs/DOCS_STANDARD.md @esengine @SivanCola
|
||||
/docs/STUDIO_*.md @esengine @SivanCola
|
||||
/docs/SESSION_MEMORY_RETRIEVAL.md @esengine @SivanCola
|
||||
/docs/research/ @esengine @SivanCola
|
||||
/release-notes/ @esengine @SivanCola
|
||||
|
||||
# Release signing: provider policy, its contract and the workflow that uses it.
|
||||
/.signpath/ @esengine @SivanCola
|
||||
/.github/workflows/release-studio.yml @esengine @SivanCola
|
||||
/cmd/signpath-contract/ @esengine @SivanCola
|
||||
/scripts/complete-signpath-request.ps1 @esengine @SivanCola
|
||||
/scripts/verify-windows-authenticode.ps1 @esengine @SivanCola
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
owner: @esengine
|
||||
backup: @SivanCola
|
||||
status: active
|
||||
reviewed: 2026-09-17
|
||||
---
|
||||
|
||||
# Documentation standard
|
||||
|
||||
Scope: every Markdown file on the `studio` branch outside `benchmarks/`.
|
||||
Enforcement: `make check` and CI run `repolint`; rule IDs below name its findings.
|
||||
|
||||
## 1. Ownership
|
||||
|
||||
| ID | Rule |
|
||||
| --- | --- |
|
||||
| O1 | Every path MUST match a `.github/CODEOWNERS` rule listing exactly a primary and a backup. |
|
||||
| O2 | The primary MUST review and merge changes to the path; the backup acts when the primary is unavailable. |
|
||||
| O3 | A CODEOWNERS rule MUST match at least one file. Delete the rule with the file. |
|
||||
| O4 | Owners MUST have write access to the repository. |
|
||||
|
||||
Current areas:
|
||||
|
||||
| Area | Paths | Primary | Backup |
|
||||
| --- | --- | --- | --- |
|
||||
| Kernel, Studio shell, tooling, standing instructions | `*` | @esengine | @SivanCola |
|
||||
| User guides and references | `docs/`, `sdk/`, `README*`, `SECURITY.md`, `CHANGELOG.md` | @SivanCola | @esengine |
|
||||
| Studio contracts and processes | `docs/STUDIO_*.md`, `docs/DOCS_STANDARD.md`, `docs/research/` | @esengine | @SivanCola |
|
||||
| Release notes and release signing | `release-notes/`, `.signpath/`, `release-studio.yml` | @esengine | @SivanCola |
|
||||
|
||||
## 2. Document classes
|
||||
|
||||
| Class | Paths | Header | Prose width |
|
||||
| --- | --- | --- | --- |
|
||||
| Document | everything not listed below | required | 320 |
|
||||
| Landing | `README.md`, `README.zh-CN.md` | none (CODEOWNERS) | 320 |
|
||||
| Machine-read | `REASONIX.md`, `AGENTS.md`, `CLAUDE.md`, `.github/`, `.reasonix/`, skill and guardian prompts | none (CODEOWNERS) | 320 |
|
||||
| Release note | `release-notes/` | none (published verbatim) | 200 per item |
|
||||
|
||||
The class table lives in `tools/repolint/docs.go`. Change both together.
|
||||
|
||||
## 3. Header (`doc-owner`)
|
||||
|
||||
A document MUST start with this block:
|
||||
|
||||
```yaml
|
||||
---
|
||||
owner: @github-handle # CODEOWNERS primary for this path
|
||||
backup: @github-handle # CODEOWNERS backup for this path
|
||||
status: active # active | deprecated
|
||||
reviewed: YYYY-MM-DD # last time the owner confirmed it matches the code
|
||||
---
|
||||
```
|
||||
|
||||
| ID | Rule |
|
||||
| --- | --- |
|
||||
| H1 | `owner` and `backup` MUST equal the first and second owner of the matching CODEOWNERS rule. |
|
||||
| H2 | `status: deprecated` means scheduled for deletion; link the replacement in the first line. |
|
||||
| H3 | The owner SHOULD update `reviewed` whenever the described behavior changes. |
|
||||
|
||||
## 4. Writing (`doc-prose`)
|
||||
|
||||
| ID | Rule |
|
||||
| --- | --- |
|
||||
| W1 | State content as numbered rules, ordered steps, or tables. |
|
||||
| W2 | A paragraph or list item MUST stay within 320 display columns (a CJK character counts as two). |
|
||||
| W3 | Use MUST, SHOULD and MAY for requirements. Anything else is information. |
|
||||
| W4 | Describe the current behavior. History and rationale belong in commit messages. |
|
||||
| W5 | Put commands in fenced code blocks so they can be copied as-is. |
|
||||
|
||||
## 5. Types and required sections
|
||||
|
||||
| Type | File pattern | Required sections, in order |
|
||||
| --- | --- | --- |
|
||||
| Contract | `*_CONTRACT.md`, `SPEC.md` | Scope, Rules (ID table), Enforcement |
|
||||
| Runbook | `*_RELEASE.md`, `*_RUNBOOK.md` | Owners, Preconditions, Steps, Verification, Recovery |
|
||||
| Guide | everything else under `docs/` | Purpose, Steps or Reference tables |
|
||||
|
||||
Sections are reviewed by the owner. The gate checks header, width and language only.
|
||||
|
||||
## 6. Language (`doc-language`)
|
||||
|
||||
| ID | Rule |
|
||||
| --- | --- |
|
||||
| L1 | English is the maintained language of every document. |
|
||||
| L2 | Only `README.zh-CN.md`, `docs/GUIDE.zh-CN.md` and `docs/CLI.zh-CN.md` MAY keep a Chinese copy. |
|
||||
| L3 | Release notes are written in Chinese, the language Studio users read. |
|
||||
|
||||
## 7. Lifecycle
|
||||
|
||||
| ID | Rule |
|
||||
| --- | --- |
|
||||
| C1 | A document describing removed behavior MUST be deleted in the change that removes it. |
|
||||
| C2 | Git history is the archive. Do not add `archive/` directories. |
|
||||
| C3 | Existing violations are recorded in `tools/repolint/baseline.json` and may only go down. |
|
||||
+101
-115
@@ -1,132 +1,118 @@
|
||||
# Releasing Reasonix Studio
|
||||
---
|
||||
owner: @esengine
|
||||
backup: @SivanCola
|
||||
status: active
|
||||
reviewed: 2026-09-17
|
||||
---
|
||||
|
||||
Studio ships from the `studio` branch and publishes as a prerelease: it does not
|
||||
enter the website's download page, does not claim the repository-wide "latest"
|
||||
release, and does not touch the desktop line's rollback catalog.
|
||||
# Studio release runbook
|
||||
|
||||
## Its own line
|
||||
Workflow: `.github/workflows/release-studio.yml`. Trigger: push of a `studio-vX.Y.Z` tag.
|
||||
|
||||
`release-studio.yml` is the only release workflow this branch carries. It has its
|
||||
own tag namespace (`studio-v*`), its own environment gate and its own rollback
|
||||
catalog, so nothing it publishes can reach the desktop line's users.
|
||||
## 1. Owners
|
||||
|
||||
Windows signing is the one boundary that still bites. The production SignPath
|
||||
policy allows exactly one origin (protected `main-v2`), which is what lets it
|
||||
avoid trusting wildcard tag-like branch names. A build from `studio` cannot
|
||||
request it, so **Windows artifacts are unsigned** and users see a SmartScreen
|
||||
warning. macOS is not affected: one Developer ID signs any bundle identifier its
|
||||
team owns, so Studio is signed and notarized exactly as the desktop line is.
|
||||
| Role | Person | Responsibility |
|
||||
| --- | --- | --- |
|
||||
| Release owner | @esengine | Decides the version, writes the notes, pushes the tag, verifies, recovers. |
|
||||
| Backup | @SivanCola | Runs this runbook when the release owner is unavailable. |
|
||||
|
||||
## One-time setup
|
||||
## 2. Preconditions
|
||||
|
||||
`release-studio.yml` gates publication on a GitHub environment named
|
||||
`studio-release`. Create it with the owner as a required reviewer:
|
||||
| ID | Check | Command |
|
||||
| --- | --- | --- |
|
||||
| P1 | The release commit is the fetched head of `origin/studio`. | `git fetch origin studio && git rev-parse origin/studio` |
|
||||
| P2 | Every job of `CI` and `Studio` succeeded on that commit. A flaky job is rerun, not ignored. | `gh run list --branch studio --commit <sha> --json name,conclusion` |
|
||||
| P3 | `release-notes/studio/X.Y.Z.md` is in that commit and passes `make check`. | `git cat-file -e <sha>:release-notes/studio/X.Y.Z.md` |
|
||||
| P4 | The version is valid semver and above the latest tag. | `git tag -l 'studio-v*' --sort=-v:refname \| head -1` |
|
||||
| P5 | The Windows signing mode is known. `true` signs through SignPath; `false` ships unsigned and the body says so. | `gh variable list \| grep STUDIO_SIGNING_ENABLED` |
|
||||
|
||||
- Settings → Environments → New environment → `studio-release`
|
||||
- Required reviewers: the owner
|
||||
## 3. Steps
|
||||
|
||||
It reuses these existing repository secrets — none are Studio-specific:
|
||||
1. Write `release-notes/studio/X.Y.Z.md` (format in section 6), commit, push to `studio`.
|
||||
2. Wait until P2 holds for the commit that contains the notes.
|
||||
3. Tag the verified commit and push the tag:
|
||||
|
||||
| Secret | Used for |
|
||||
```bash
|
||||
git fetch origin studio
|
||||
SHA=$(git rev-parse origin/studio)
|
||||
git tag studio-vX.Y.Z "$SHA"
|
||||
git fetch origin studio && test "$(git rev-parse origin/studio)" = "$SHA"
|
||||
git push origin studio-vX.Y.Z
|
||||
```
|
||||
|
||||
4. Watch the run until it finishes:
|
||||
|
||||
```bash
|
||||
RUN=$(gh run list --workflow release-studio.yml --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
gh run watch "$RUN" --exit-status
|
||||
```
|
||||
|
||||
Jobs in the run:
|
||||
|
||||
| Job | Does | Gate |
|
||||
| --- | --- | --- |
|
||||
| `resolve` | Validates the tag shape and that the commit is on `studio`. | fails on any other ref |
|
||||
| `signing-contract` | Validates `.signpath/` locally and prints its fingerprint. | does not call SignPath |
|
||||
| `build` | Builds windows/amd64, darwin/amd64, darwin/arm64, linux/amd64; signs macOS and, if enabled, Windows. | Apple secrets are required |
|
||||
| `cli` | Builds `reasonix` archives for six OS/arch targets plus `SHA256SUMS`. | fails on a missing archive |
|
||||
| `publish` | Minisigns, writes `latest.json`, creates the GitHub prerelease, mirrors to R2. | environment `studio-release` |
|
||||
|
||||
The `studio-release` environment allows the `studio-v*` tag and the `studio` branch. It has no required reviewer.
|
||||
|
||||
## 4. Verification
|
||||
|
||||
| ID | Expected | Command |
|
||||
| --- | --- | --- |
|
||||
| V1 | Prerelease exists with 22 assets (per-platform packages, `.minisig` files, CLI archives, `latest.json`, `SHA256SUMS`). | `gh release view studio-vX.Y.Z --json isPrerelease,assets --jq '.isPrerelease, (.assets \| length)'` |
|
||||
| V2 | The catalog lists the new version first. | `curl -s https://dl.reasonix.io/studio/versions.json \| jq -r '.versions[0].tag'` |
|
||||
| V3 | The manifest is served. | `curl -sI https://dl.reasonix.io/studio-vX.Y.Z/latest.json \| head -1` |
|
||||
| V4 | The body contains the version notes and the standing install text. | `gh release view studio-vX.Y.Z --json body --jq .body` |
|
||||
|
||||
## 5. Recovery
|
||||
|
||||
| Symptom | Cause | Action |
|
||||
| --- | --- | --- |
|
||||
| No run appears, or a rerun ends in `startup_failure` with no jobs. | GitHub Actions runner outage. | Wait for queued runs to drain, then `gh workflow run release-studio.yml --ref studio -f tag=studio-vX.Y.Z`. |
|
||||
| A build or publish step failed. | Workflow or runner fault. | Fix on `studio` if needed, then dispatch as above. The dispatch rebuilds from the tag's commit with the workflow from `studio`. |
|
||||
| Windows signing fails with `Invalid request to SignPath API.` | SignPath rejected the request; the action hides the status. | `gh run rerun <run> --failed --debug` to see it. On quota exhaustion set `STUDIO_SIGNING_ENABLED=false` and dispatch. |
|
||||
| Signing is restored after an unsigned release. | Artifacts were published unsigned. | Set `STUDIO_SIGNING_ENABLED=true` and dispatch the same tag; `publish` replaces the assets. |
|
||||
| The body is missing or wrong. | Notes are read from the tag's commit, not the branch. | `gh release edit studio-vX.Y.Z --notes-file <file>`; append the standing text from the previous body. |
|
||||
| The tag points at the wrong commit and `publish` has not run. | Tagging error. | `git push origin :refs/tags/studio-vX.Y.Z`, delete the local tag, restart at step 3. |
|
||||
| The tag points at the wrong commit and `publish` has run. | Tagging error after release. | Do not move the tag. Release the next patch version. |
|
||||
|
||||
## 6. Release note format
|
||||
|
||||
File: `release-notes/studio/X.Y.Z.md`, without the tag's `v`. Language: Chinese. Enforced by `release-note` and `doc-prose`.
|
||||
|
||||
| ID | Rule |
|
||||
| --- | --- |
|
||||
| `MINISIGN_PRIVATE_KEY`, `MINISIGN_PASSWORD` | detached signatures |
|
||||
| `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ACCOUNT_ID`, `R2_BUCKET` | catalog + artifact mirror |
|
||||
| `APPLE_CERT_P12`, `APPLE_CERT_PASSWORD`, `APPLE_API_KEY_P8`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER_ID` | Developer ID signing and notarization |
|
||||
| R1 | The first line is a one-sentence summary. No other paragraph. |
|
||||
| R2 | Group headings are `## 新增`, `## 变更`, `## 修复`, `## 移除`, `## 升级须知`, in that order, omitting empty ones. No deeper headings. |
|
||||
| R3 | One change is one list item within 200 display columns. |
|
||||
| R4 | Every item names its issue (`#123`), pull request or commit. |
|
||||
| R5 | Describe what the user observes. The explanation belongs in the linked commit. |
|
||||
|
||||
The minisign key is deliberately the desktop line's: the public key that verifies
|
||||
it is compiled into the client (`update.PublicKey`), so a separate key would not
|
||||
verify. The five `APPLE_*` secrets are required rather than optional — the macOS
|
||||
job fails closed without them, because an unsigned build that reached users would
|
||||
break the next self-update rather than the first launch. If the R2 secrets are
|
||||
absent the GitHub release still publishes, but the catalog is not updated and
|
||||
running builds will not see the new version.
|
||||
```markdown
|
||||
本版修复读图模型误报看不到图,并让被 ACL 残留阻塞的 Windows 安装恢复启动。
|
||||
|
||||
## Cutting a release
|
||||
## 修复
|
||||
|
||||
Write the version's notes first. `release-studio.yml` reads
|
||||
`release-notes/studio/<semver>.md` — named without the tag's v — and appends
|
||||
the standing text about signing,
|
||||
installation and self-update; without that file it publishes the standing text
|
||||
alone and warns.
|
||||
|
||||
```bash
|
||||
$EDITOR release-notes/studio/2.0.0.md
|
||||
git checkout studio
|
||||
git tag studio-v2.0.0
|
||||
git push origin studio-v2.0.0
|
||||
- 读图模型不再声称看不到已附加的图片 (44150b0aa)
|
||||
- Windows 安装目录带 AppContainer 包 SID 授权时窗口可以正常打开 #10435
|
||||
```
|
||||
|
||||
The tag triggers `release-studio.yml`, which:
|
||||
## 7. Reference
|
||||
|
||||
1. validates the tag shape (`studio-vMAJOR.MINOR.PATCH[-PRERELEASE]`) and that
|
||||
it sits on `studio` history;
|
||||
2. builds Windows x64, macOS universal and Linux x64 with electron-builder,
|
||||
from `desktop/electron/electron-builder.yml`. Windows and macOS package in
|
||||
two steps because both need something done to the bundle in between — an
|
||||
Authenticode signature there, notarization and stapling here;
|
||||
3. waits for the `studio-release` approval;
|
||||
4. minisign-signs every artifact and verifies each signature;
|
||||
5. generates `latest.json` with `desktop/cmd/studio-manifest`;
|
||||
6. publishes a GitHub **prerelease** (never `latest`);
|
||||
7. mirrors artifacts to `dl.reasonix.io/<tag>/` and updates
|
||||
`dl.reasonix.io/studio/versions.json`.
|
||||
| Name | Kind | Used by |
|
||||
| --- | --- | --- |
|
||||
| `APPLE_CERT_P12`, `APPLE_CERT_PASSWORD`, `APPLE_API_KEY_P8`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER_ID` | secret | macOS signing and notarization |
|
||||
| `SIGNPATH_API_TOKEN`, `SIGNPATH_ORGANIZATION_ID` | secret | Windows Authenticode signing |
|
||||
| `STUDIO_SIGNING_ENABLED` | variable | Windows signing switch and the body's Windows sentence |
|
||||
| `MINISIGN_PRIVATE_KEY`, `MINISIGN_PASSWORD` | secret | detached signatures verified by the updater |
|
||||
| `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ACCOUNT_ID`, `R2_BUCKET` | secret | artifact mirror and catalog |
|
||||
|
||||
`workflow_dispatch` with an existing tag recovers a failed run.
|
||||
|
||||
## R2 layout
|
||||
|
||||
```
|
||||
dl.reasonix.io/
|
||||
versions.json # desktop line — release-studio.yml never writes this
|
||||
studio/versions.json # Studio's catalog; studioCatalog in desktop/next
|
||||
studio-v2.0.0/… # Studio artifacts, latest.json, signatures
|
||||
desktop-v1.26.0/… # desktop artifacts
|
||||
```
|
||||
|
||||
Tag prefixes cannot collide because the tag names differ, so Studio artifacts sit
|
||||
beside desktop ones without a separate bucket.
|
||||
|
||||
## Self-update
|
||||
|
||||
`latest.json` carries three maps and the updater reads each by role:
|
||||
|
||||
- `downloads` is what a person is offered, so it lists only what installs itself:
|
||||
the `.dmg`, the Windows installer, the `.deb`.
|
||||
- `platforms` is what the updater resolves — the Windows installer, which it runs
|
||||
to upgrade in place, and the macOS universal `.zip`, whose bundle it swaps. A
|
||||
portable archive is never listed: resolving one would hand the updater an
|
||||
artifact it cannot install.
|
||||
- `nativePackages` carries the `.deb`, which is how Linux upgrades, because dpkg
|
||||
replaces the binary and the SPA tree together.
|
||||
|
||||
`desktop/cmd/studio-manifest` derives all three from the artifact names, and its
|
||||
tests assert the manifest never claims a platform it cannot install.
|
||||
|
||||
The Windows installer is started as the user who asked for the update, not
|
||||
elevated. It requests nothing in its manifest and raises consent itself only if
|
||||
that user chooses an all-users install; starting it with `runas` would put it in
|
||||
the consenting account, where its per-user default writes that profile and
|
||||
leaves the updating user on the build they had. `update.StudioLine` states which
|
||||
of the two it ships, and a line that states neither is refused rather than
|
||||
guessed for.
|
||||
|
||||
That installer also takes over a Wails-era install rather than sitting beside
|
||||
it. The old one is per-machine under Program Files with its own uninstall key,
|
||||
this one is per-user under a key derived from the appId, so nothing about
|
||||
installing it would have replaced the old one — both would have stayed in
|
||||
Add/Remove Programs and only one would ever have been updated again.
|
||||
`desktop/electron/assets/installer.nsh` runs the old uninstaller from
|
||||
`customInstall`; a declined consent prompt leaves both installed rather than
|
||||
failing the install.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- No `versioninfo` stamp and **no windows/arm64 build**: `desktop/next` builds
|
||||
with plain `go build` rather than the Wails CLI, and Windows x64 takes its icon
|
||||
from the committed `rsrc_windows_amd64.syso` — arm64 has no `.syso`.
|
||||
- Giving `desktop/next` its own `wails.json` fixes the version stamp and arm64
|
||||
together. When doing it, keep Studio's install directory and uninstall registry
|
||||
key distinct from `reasonix-desktop` — sharing them would let installing Studio
|
||||
overwrite an existing desktop install.
|
||||
- Installs of the desktop line (`desktop-v*`) read the root catalog, so they are
|
||||
never offered Studio. Migrating them is not wired up.
|
||||
| R2 path | Owner | Content |
|
||||
| --- | --- | --- |
|
||||
| `studio/versions.json` | this workflow | Studio catalog, newest first |
|
||||
| `studio-vX.Y.Z/` | this workflow | artifacts, signatures, `latest.json` |
|
||||
| `versions.json` | desktop line | never written by this workflow |
|
||||
@@ -3,8 +3,12 @@
|
||||
"banner": 0,
|
||||
"build-artifact": 0,
|
||||
"claude-dialect": 1,
|
||||
"codeowners": 0,
|
||||
"commented-code": 0,
|
||||
"complexity": 1495,
|
||||
"doc-language": 33,
|
||||
"doc-owner": 91,
|
||||
"doc-prose": 2755,
|
||||
"error-text": 9,
|
||||
"essay": 3207,
|
||||
"file-size": 31285,
|
||||
@@ -16,11 +20,36 @@
|
||||
"narrative": 0,
|
||||
"orphan": 80,
|
||||
"refusal-path": 0,
|
||||
"release-note": 661,
|
||||
"spec-parity": 0,
|
||||
"struct-state": 32,
|
||||
"wire-parity": 0
|
||||
},
|
||||
"files": {
|
||||
".github/pull_request_template.md": {
|
||||
"doc-prose": 1
|
||||
},
|
||||
"CHANGELOG.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 94
|
||||
},
|
||||
"CONTRIBUTING.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"README.md": {
|
||||
"doc-prose": 1
|
||||
},
|
||||
"README.zh-CN.md": {
|
||||
"doc-prose": 1
|
||||
},
|
||||
"REASONIX.md": {
|
||||
"doc-prose": 92
|
||||
},
|
||||
"SECURITY.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"cmd/e2ebench/main.go": {
|
||||
"essay": 1
|
||||
},
|
||||
@@ -30,6 +59,10 @@
|
||||
"cmd/reasonix-plugin-example/main.go": {
|
||||
"essay": 19
|
||||
},
|
||||
"desktop/README.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"desktop/cmd/sign/main.go": {
|
||||
"essay": 14
|
||||
},
|
||||
@@ -47,9 +80,346 @@
|
||||
"desktop/cmd/update-helper/versioned_windows.go": {
|
||||
"essay": 10
|
||||
},
|
||||
"desktop/frontend-next/perf/README.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 21
|
||||
},
|
||||
"desktop/frontend-next/src/styles/LAYOUT.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 128
|
||||
},
|
||||
"desktop/frontend-next/src/styles/TOKENS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 22
|
||||
},
|
||||
"desktop/internal/winuninstall/registry_windows.go": {
|
||||
"essay": 1
|
||||
},
|
||||
"docs/ACP.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 13
|
||||
},
|
||||
"docs/ACP.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/BILLING.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/BILLING.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/BOT_GUIDE.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 27
|
||||
},
|
||||
"docs/BOT_GUIDE.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 11
|
||||
},
|
||||
"docs/CAPABILITY_DIAGNOSTICS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 5
|
||||
},
|
||||
"docs/CAPABILITY_DIAGNOSTICS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/CHECKPOINTS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 4
|
||||
},
|
||||
"docs/CHECKPOINTS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"docs/CLI.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 50
|
||||
},
|
||||
"docs/CLI.zh-CN.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 17
|
||||
},
|
||||
"docs/COLLABORATION_MODES.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/CONFIG_PATHS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 30
|
||||
},
|
||||
"docs/CONFIG_PATHS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 12
|
||||
},
|
||||
"docs/DESKTOP_CRASH_DIAGNOSTICS_RUNBOOK.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 11
|
||||
},
|
||||
"docs/DESKTOP_CRASH_DIAGNOSTICS_RUNBOOK.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/DESKTOP_HOOKS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 10
|
||||
},
|
||||
"docs/EXTENSIONS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 16
|
||||
},
|
||||
"docs/EXTENSIONS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 4
|
||||
},
|
||||
"docs/EXTENSION_PROTOCOL.generated.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/EXTENSION_PROTOCOL.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 20
|
||||
},
|
||||
"docs/EXTENSION_PROTOCOL.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 6
|
||||
},
|
||||
"docs/EXTENSION_RUNTIME_V2.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 13
|
||||
},
|
||||
"docs/EXTENSION_RUNTIME_V2.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 6
|
||||
},
|
||||
"docs/EXTENSION_RUNTIME_V2_PERF.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 9
|
||||
},
|
||||
"docs/EXTENSION_RUNTIME_V2_PERF.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/GOAL_ENFORCEMENT.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 8
|
||||
},
|
||||
"docs/GUIDE.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 372
|
||||
},
|
||||
"docs/GUIDE.zh-CN.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 178
|
||||
},
|
||||
"docs/HISTORY_SEARCH_CATALOG.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 8
|
||||
},
|
||||
"docs/HISTORY_SEARCH_CATALOG.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/MIGRATING.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 44
|
||||
},
|
||||
"docs/MIGRATING.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/PLUGIN_PACKAGES.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 103
|
||||
},
|
||||
"docs/PLUGIN_PACKAGES.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 69
|
||||
},
|
||||
"docs/REASONING_LANGUAGE.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/REASONING_LANGUAGE.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/REASONING_PROVIDERS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 23
|
||||
},
|
||||
"docs/REASONING_PROVIDERS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 11
|
||||
},
|
||||
"docs/RECOVERY.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/RECOVERY.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/RELEASING.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 7
|
||||
},
|
||||
"docs/SESSION_MEMORY_RETRIEVAL.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 15
|
||||
},
|
||||
"docs/SESSION_MEMORY_RETRIEVAL.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/SESSION_REFERENCE_ARCHITECTURE.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/SIGNPATH_WINDOWS_ADMIN_SOP.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/SPEC.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 341
|
||||
},
|
||||
"docs/SPEC.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 78
|
||||
},
|
||||
"docs/STUDIO_SHELL_BOUNDARIES.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/SUBAGENT_PROFILES.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 4
|
||||
},
|
||||
"docs/SUBAGENT_PROFILES.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/SUBAGENT_PROGRESS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 5
|
||||
},
|
||||
"docs/SUBAGENT_PROGRESS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"docs/TASK_CONTRACT.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"docs/TASK_CONTRACT.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/THEME_ASSETS.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/THEME_ASSETS.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/THEME_PACK.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 5
|
||||
},
|
||||
"docs/THEME_PACK.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"docs/TOOL_APPROVAL_MODES.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 10
|
||||
},
|
||||
"docs/TOOL_APPROVAL_MODES.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"docs/TOOL_CONTRACT.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 41
|
||||
},
|
||||
"docs/TOOL_CONTRACT.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 20
|
||||
},
|
||||
"docs/USAGE_CATALOG.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/USAGE_CATALOG.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/WINDOWS_LTSC_2019_CRASH_RUNBOOK.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/WINDOWS_LTSC_2019_CRASH_RUNBOOK.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/production_checklist.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/production_checklist.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/research/cache-aware-compaction-design.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/research/goal-verification-authority.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/research/subagent-handoff-baseline.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"docs/superpowers/audits/2026-06-30-autoresearch-runtime-verification.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/superpowers/plans/2026-06-29-autoresearch-runtime-implementation.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"docs/superpowers/plans/billing-currency-acceptance.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"docs/superpowers/specs/2026-06-29-autoresearch-runtime-design.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 2
|
||||
},
|
||||
"docs/superpowers/specs/2026-08-07-spatiotemporal-composability-v2.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 3
|
||||
},
|
||||
"internal/acp/clientio.go": {
|
||||
"essay": 4
|
||||
},
|
||||
@@ -1039,6 +1409,9 @@
|
||||
"internal/guardian/guardian.go": {
|
||||
"essay": 25
|
||||
},
|
||||
"internal/guardian/guardian_policy.md": {
|
||||
"doc-prose": 1
|
||||
},
|
||||
"internal/history/indexed.go": {
|
||||
"orphan": 1
|
||||
},
|
||||
@@ -1586,9 +1959,94 @@
|
||||
"internal/update/manifest.go": {
|
||||
"essay": 1
|
||||
},
|
||||
"release-notes/studio/2.0.0.md": {
|
||||
"release-note": 29
|
||||
},
|
||||
"release-notes/studio/2.1.0.md": {
|
||||
"doc-prose": 1,
|
||||
"release-note": 25
|
||||
},
|
||||
"release-notes/studio/2.10.0.md": {
|
||||
"doc-prose": 28,
|
||||
"release-note": 46
|
||||
},
|
||||
"release-notes/studio/2.11.0.md": {
|
||||
"doc-prose": 206,
|
||||
"release-note": 75
|
||||
},
|
||||
"release-notes/studio/2.12.0.md": {
|
||||
"doc-prose": 16,
|
||||
"release-note": 35
|
||||
},
|
||||
"release-notes/studio/2.13.0.md": {
|
||||
"release-note": 23
|
||||
},
|
||||
"release-notes/studio/2.14.0.md": {
|
||||
"doc-prose": 28,
|
||||
"release-note": 53
|
||||
},
|
||||
"release-notes/studio/2.14.1.md": {
|
||||
"doc-prose": 16,
|
||||
"release-note": 7
|
||||
},
|
||||
"release-notes/studio/2.15.0.md": {
|
||||
"doc-prose": 110,
|
||||
"release-note": 43
|
||||
},
|
||||
"release-notes/studio/2.16.0.md": {
|
||||
"doc-prose": 107,
|
||||
"release-note": 47
|
||||
},
|
||||
"release-notes/studio/2.17.0.md": {
|
||||
"doc-prose": 43,
|
||||
"release-note": 26
|
||||
},
|
||||
"release-notes/studio/2.2.0.md": {
|
||||
"doc-prose": 1,
|
||||
"release-note": 26
|
||||
},
|
||||
"release-notes/studio/2.3.0.md": {
|
||||
"doc-prose": 2,
|
||||
"release-note": 30
|
||||
},
|
||||
"release-notes/studio/2.4.0.md": {
|
||||
"doc-prose": 1,
|
||||
"release-note": 45
|
||||
},
|
||||
"release-notes/studio/2.5.0.md": {
|
||||
"doc-prose": 10,
|
||||
"release-note": 29
|
||||
},
|
||||
"release-notes/studio/2.6.0.md": {
|
||||
"doc-prose": 8,
|
||||
"release-note": 31
|
||||
},
|
||||
"release-notes/studio/2.7.0.md": {
|
||||
"doc-prose": 44,
|
||||
"release-note": 26
|
||||
},
|
||||
"release-notes/studio/2.8.0.md": {
|
||||
"doc-prose": 58,
|
||||
"release-note": 37
|
||||
},
|
||||
"release-notes/studio/2.9.0.md": {
|
||||
"doc-prose": 32,
|
||||
"release-note": 28
|
||||
},
|
||||
"sdk/go/README.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 11
|
||||
},
|
||||
"sdk/go/examples/fullsidecar/main.go": {
|
||||
"essay": 25
|
||||
},
|
||||
"sdk/go/examples/starterextension/README.md": {
|
||||
"doc-owner": 1
|
||||
},
|
||||
"sdk/go/examples/starterextension/README.zh-CN.md": {
|
||||
"doc-language": 1,
|
||||
"doc-owner": 1
|
||||
},
|
||||
"sdk/go/sdk.go": {
|
||||
"essay": 27,
|
||||
"file-size": 367
|
||||
@@ -1602,6 +2060,18 @@
|
||||
"tools/repolint/orphan.go": {
|
||||
"essay": 2
|
||||
},
|
||||
"tools/trajectory/README.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 11
|
||||
},
|
||||
"tools/workgroupassay/README.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 1
|
||||
},
|
||||
"workers/accounts/README.md": {
|
||||
"doc-owner": 1,
|
||||
"doc-prose": 4
|
||||
},
|
||||
"workers/crash-report/src/index.ts": {
|
||||
"file-size": 686
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// codeowners.go — who answers for a path, read from .github/CODEOWNERS.
|
||||
// The first owner on a line is the primary and the second the backup; GitHub
|
||||
// requests review from both, and the documentation gate reads the order.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const codeownersPath = ".github/CODEOWNERS"
|
||||
|
||||
type ownerRule struct {
|
||||
line int
|
||||
pattern string
|
||||
owners []string
|
||||
match *regexp.Regexp
|
||||
}
|
||||
|
||||
func loadOwnerRules(root string) ([]ownerRule, error) {
|
||||
data, err := os.ReadFile(filepath.Join(root, codeownersPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rules []ownerRule
|
||||
for i, raw := range strings.Split(string(data), "\n") {
|
||||
fields := strings.Fields(raw)
|
||||
if len(fields) == 0 || strings.HasPrefix(fields[0], "#") {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, ownerRule{line: i + 1, pattern: fields[0], owners: fields[1:], match: ownerPattern(fields[0])})
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// ownersOf is the last matching rule, which is the one GitHub applies.
|
||||
func ownersOf(rules []ownerRule, rel string) (ownerRule, bool) {
|
||||
for _, r := range slices.Backward(rules) {
|
||||
if r.match.MatchString(rel) {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
return ownerRule{}, false
|
||||
}
|
||||
|
||||
// ownerPattern translates the subset of CODEOWNERS syntax this repository
|
||||
// uses: a leading slash anchors, a trailing slash names a directory, * stays
|
||||
// within a segment and ** crosses them.
|
||||
func ownerPattern(pattern string) *regexp.Regexp {
|
||||
anchored := strings.HasPrefix(pattern, "/") || strings.Contains(strings.Trim(pattern, "/"), "/")
|
||||
dir := strings.HasSuffix(pattern, "/")
|
||||
body := strings.Trim(pattern, "/")
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(body); i++ {
|
||||
switch {
|
||||
case strings.HasPrefix(body[i:], "**"):
|
||||
b.WriteString(".*")
|
||||
i++
|
||||
case body[i] == '*':
|
||||
b.WriteString("[^/]*")
|
||||
case body[i] == '?':
|
||||
b.WriteString("[^/]")
|
||||
default:
|
||||
b.WriteString(regexp.QuoteMeta(body[i : i+1]))
|
||||
}
|
||||
}
|
||||
prefix := "^"
|
||||
if !anchored {
|
||||
prefix = "^(?:.*/)?"
|
||||
}
|
||||
suffix := "(?:/.*)?$"
|
||||
if dir {
|
||||
suffix = "/.*$"
|
||||
}
|
||||
return regexp.MustCompile(prefix + b.String() + suffix)
|
||||
}
|
||||
|
||||
// checkCodeOwners holds every rule to a path that exists and to a primary and
|
||||
// a backup who are two different people. A rule for a deleted file routes
|
||||
// nothing, and reading it tells a newcomer the file is still owned.
|
||||
func checkCodeOwners(root string) []Finding {
|
||||
rules, err := loadOwnerRules(root)
|
||||
if err != nil {
|
||||
return []Finding{{codeownersPath, 1, ruleCodeOwners, "no CODEOWNERS: nothing names who answers for a path", 1}}
|
||||
}
|
||||
paths, err := repoFiles(root)
|
||||
var out []Finding
|
||||
for _, r := range rules {
|
||||
if len(r.owners) < 2 || r.owners[0] == r.owners[1] {
|
||||
out = append(out, Finding{codeownersPath, r.line, ruleCodeOwners,
|
||||
fmt.Sprintf("%s names %v; a rule needs a primary and a different backup", r.pattern, r.owners), 1})
|
||||
}
|
||||
if err == nil && !anyMatch(r.match, paths) {
|
||||
out = append(out, Finding{codeownersPath, r.line, ruleCodeOwners,
|
||||
fmt.Sprintf("%s matches no file in the tree; remove the rule with the file it owned", r.pattern), 1})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func anyMatch(re *regexp.Regexp, paths []string) bool {
|
||||
return slices.ContainsFunc(paths, re.MatchString)
|
||||
}
|
||||
|
||||
// repoFiles is the tree a reviewer would see: tracked files plus new ones git
|
||||
// does not ignore, minus anything deleted from the working tree. A walk would
|
||||
// also read local worktrees and caches nobody commits.
|
||||
func repoFiles(root string) ([]string, error) {
|
||||
listed, err := gitPaths(root, "ls-files", "--cached", "--others", "--exclude-standard")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := listed[:0]
|
||||
for _, rel := range listed {
|
||||
if _, err := os.Stat(filepath.Join(root, rel)); err == nil {
|
||||
out = append(out, rel)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// docs.go — a document names who answers for it and states its content as
|
||||
// rules, steps and tables. docs/DOCS_STANDARD.md is the contract; this file is
|
||||
// the part of it a machine can hold. Classes are declared by path, never
|
||||
// inferred from what a file happens to contain.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type docClass int
|
||||
|
||||
const (
|
||||
// classDoc carries a metadata header and is held to the prose width.
|
||||
classDoc docClass = iota
|
||||
// classLanding renders as the repository's front page, where a header
|
||||
// table would be the first thing a visitor reads; CODEOWNERS owns it.
|
||||
classLanding
|
||||
// classMachine is read verbatim by a model, GitHub or a release body, so a
|
||||
// header would travel with it; CODEOWNERS owns it.
|
||||
classMachine
|
||||
classReleaseNote
|
||||
)
|
||||
|
||||
const (
|
||||
docProseWidth = 320
|
||||
releaseNoteItemWidth = 200
|
||||
proseWeightUnit = 80
|
||||
docMetadataDelimiter = "---"
|
||||
)
|
||||
|
||||
var docClassByPrefix = []struct {
|
||||
prefix string
|
||||
class docClass
|
||||
}{
|
||||
{"release-notes/", classReleaseNote},
|
||||
{"README.md", classLanding},
|
||||
{"README.zh-CN.md", classLanding},
|
||||
{"REASONIX.md", classMachine},
|
||||
{"AGENTS.md", classMachine},
|
||||
{"CLAUDE.md", classMachine},
|
||||
{".github/", classMachine},
|
||||
{".reasonix/", classMachine},
|
||||
{"internal/skill/builtincontent/", classMachine},
|
||||
{"internal/guardian/", classMachine},
|
||||
}
|
||||
|
||||
// docSkippedPrefixes are corpora and fixtures: text a benchmark or a test
|
||||
// reads as data, which no reader is meant to follow.
|
||||
var docSkippedPrefixes = []string{"benchmarks/"}
|
||||
|
||||
// translatedDocs are the onboarding pages kept in Chinese beside the English
|
||||
// original. Every other document has one language, English.
|
||||
var translatedDocs = []string{
|
||||
"README.zh-CN.md",
|
||||
"docs/GUIDE.zh-CN.md",
|
||||
"docs/CLI.zh-CN.md",
|
||||
}
|
||||
|
||||
var (
|
||||
docStatuses = []string{"active", "deprecated"}
|
||||
releaseNoteGroups = []string{"新增", "变更", "修复", "移除", "升级须知"}
|
||||
isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
||||
listItem = regexp.MustCompile(`^\s*(?:[-*+]|\d+\.)\s+`)
|
||||
changeReference = regexp.MustCompile(`#\d+|\b[0-9a-f]{7,40}\b|https?://`)
|
||||
)
|
||||
|
||||
func classifyDoc(rel string) (docClass, bool) {
|
||||
if !strings.HasSuffix(rel, ".md") {
|
||||
return 0, false
|
||||
}
|
||||
for _, p := range docSkippedPrefixes {
|
||||
if strings.HasPrefix(rel, p) {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
for seg := range strings.SplitSeq(rel, "/") {
|
||||
if skipDirs[seg] {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
for _, c := range docClassByPrefix {
|
||||
if rel == c.prefix || (strings.HasSuffix(c.prefix, "/") && strings.HasPrefix(rel, c.prefix)) {
|
||||
return c.class, true
|
||||
}
|
||||
}
|
||||
return classDoc, true
|
||||
}
|
||||
|
||||
func checkDocs(root string) []Finding {
|
||||
files, err := repoFiles(root)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
rules, rulesErr := loadOwnerRules(root)
|
||||
var out []Finding
|
||||
for _, rel := range files {
|
||||
class, ok := classifyDoc(rel)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(root, rel))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
|
||||
if strings.HasSuffix(rel, ".zh-CN.md") && !slices.Contains(translatedDocs, rel) {
|
||||
out = append(out, Finding{rel, 1, ruleDocLanguage,
|
||||
"only onboarding pages keep a Chinese copy; the English document is the one maintained", 1})
|
||||
}
|
||||
switch class {
|
||||
case classDoc:
|
||||
if rulesErr == nil {
|
||||
out = append(out, checkDocMetadata(rel, lines, rules)...)
|
||||
}
|
||||
out = append(out, checkProse(rel, lines, docProseWidth)...)
|
||||
case classReleaseNote:
|
||||
out = append(out, checkReleaseNote(rel, lines)...)
|
||||
default:
|
||||
out = append(out, checkProse(rel, lines, docProseWidth)...)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkDocMetadata reads the header and holds its owners to CODEOWNERS, so the
|
||||
// name printed on the page is the name GitHub asks for review.
|
||||
func checkDocMetadata(rel string, lines []string, rules []ownerRule) []Finding {
|
||||
meta, ok := docMetadata(lines)
|
||||
if !ok {
|
||||
return []Finding{{rel, 1, ruleDocOwner, "no metadata header: start with ---, owner, backup, status, reviewed, ---", 1}}
|
||||
}
|
||||
var problems []string
|
||||
rule, owned := ownersOf(rules, rel)
|
||||
switch {
|
||||
case !owned || len(rule.owners) < 2:
|
||||
problems = append(problems, "no CODEOWNERS rule names a primary and a backup for this path")
|
||||
case meta["owner"] != rule.owners[0] || meta["backup"] != rule.owners[1]:
|
||||
problems = append(problems, fmt.Sprintf("owner/backup %s/%s differ from CODEOWNERS line %d (%s %s)",
|
||||
meta["owner"], meta["backup"], rule.line, rule.owners[0], rule.owners[1]))
|
||||
}
|
||||
if !slices.Contains(docStatuses, meta["status"]) {
|
||||
problems = append(problems, fmt.Sprintf("status %q is not one of %v", meta["status"], docStatuses))
|
||||
}
|
||||
if !isoDate.MatchString(meta["reviewed"]) {
|
||||
problems = append(problems, fmt.Sprintf("reviewed %q is not YYYY-MM-DD", meta["reviewed"]))
|
||||
}
|
||||
if len(problems) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []Finding{{rel, 1, ruleDocOwner, strings.Join(problems, "; "), len(problems)}}
|
||||
}
|
||||
|
||||
func docMetadata(lines []string) (map[string]string, bool) {
|
||||
if len(lines) == 0 || strings.TrimSpace(lines[0]) != docMetadataDelimiter {
|
||||
return nil, false
|
||||
}
|
||||
meta := map[string]string{}
|
||||
for _, line := range lines[1:] {
|
||||
if strings.TrimSpace(line) == docMetadataDelimiter {
|
||||
return meta, true
|
||||
}
|
||||
if key, value, ok := strings.Cut(line, ":"); ok {
|
||||
meta[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// proseBlock is one paragraph or one list item with its continuation lines.
|
||||
type proseBlock struct {
|
||||
line int
|
||||
item bool
|
||||
width int
|
||||
text string
|
||||
}
|
||||
|
||||
func proseBlocks(lines []string) []proseBlock {
|
||||
var out []proseBlock
|
||||
var cur *proseBlock
|
||||
flush := func() {
|
||||
if cur != nil {
|
||||
out = append(out, *cur)
|
||||
cur = nil
|
||||
}
|
||||
}
|
||||
inFence, inMeta := false, false
|
||||
for i, raw := range lines {
|
||||
t := strings.TrimSpace(raw)
|
||||
switch {
|
||||
case i == 0 && t == docMetadataDelimiter:
|
||||
inMeta = true
|
||||
continue
|
||||
case inMeta:
|
||||
inMeta = t != docMetadataDelimiter
|
||||
continue
|
||||
case strings.HasPrefix(t, "```"):
|
||||
inFence = !inFence
|
||||
flush()
|
||||
continue
|
||||
case inFence:
|
||||
continue
|
||||
case t == "" || strings.HasPrefix(t, "#") || strings.HasPrefix(t, "|") || strings.HasPrefix(t, "<"):
|
||||
flush()
|
||||
continue
|
||||
}
|
||||
if loc := listItem.FindStringIndex(raw); loc != nil {
|
||||
flush()
|
||||
cur = &proseBlock{line: i + 1, item: true, text: raw[loc[1]:]}
|
||||
continue
|
||||
}
|
||||
if cur == nil {
|
||||
cur = &proseBlock{line: i + 1}
|
||||
}
|
||||
cur.text += " " + strings.TrimLeft(t, "> ")
|
||||
}
|
||||
flush()
|
||||
for i := range out {
|
||||
out[i].text = strings.TrimSpace(out[i].text)
|
||||
out[i].width = displayWidth(out[i].text)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// displayWidth counts a CJK character as two columns: the same budget in runes
|
||||
// would let a Chinese paragraph run twice as long as an English one.
|
||||
func displayWidth(s string) int {
|
||||
n := 0
|
||||
for _, r := range s {
|
||||
if unicode.In(r, unicode.Han, unicode.Hiragana, unicode.Katakana, unicode.Hangul) || (r >= 0x3000 && r <= 0x303f) || (r >= 0xff00 && r <= 0xffef) {
|
||||
n += 2
|
||||
} else {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func checkProse(rel string, lines []string, limit int) []Finding {
|
||||
var out []Finding
|
||||
for _, b := range proseBlocks(lines) {
|
||||
if b.width <= limit {
|
||||
continue
|
||||
}
|
||||
out = append(out, Finding{rel, b.line, ruleDocProse,
|
||||
fmt.Sprintf("%s is %d columns, over %d: split it into rules, steps or a table", blockKind(b), b.width, limit),
|
||||
(b.width - limit + proseWeightUnit - 1) / proseWeightUnit})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func blockKind(b proseBlock) string {
|
||||
if b.item {
|
||||
return "list item"
|
||||
}
|
||||
return "paragraph"
|
||||
}
|
||||
|
||||
// checkReleaseNote holds a version's notes to the published shape: one summary
|
||||
// line, changes grouped under the fixed headings, one short referenced line per
|
||||
// change. The reasoning behind a change lives in its commit.
|
||||
func checkReleaseNote(rel string, lines []string) []Finding {
|
||||
var out []Finding
|
||||
paragraphs := 0
|
||||
for i, raw := range lines {
|
||||
t := strings.TrimSpace(raw)
|
||||
switch {
|
||||
case strings.HasPrefix(t, "## "):
|
||||
if !slices.Contains(releaseNoteGroups, strings.TrimSpace(t[3:])) {
|
||||
out = append(out, Finding{rel, i + 1, ruleReleaseNote,
|
||||
fmt.Sprintf("heading %q is not one of %v", t, releaseNoteGroups), 1})
|
||||
}
|
||||
case strings.HasPrefix(t, "#"):
|
||||
out = append(out, Finding{rel, i + 1, ruleReleaseNote, "only ## group headings; a change is one list item, not a section", 1})
|
||||
}
|
||||
}
|
||||
for _, b := range proseBlocks(lines) {
|
||||
switch {
|
||||
case !b.item:
|
||||
paragraphs++
|
||||
if paragraphs > 1 {
|
||||
out = append(out, Finding{rel, b.line, ruleReleaseNote, "only the first line may be a summary paragraph; state changes as list items", 1})
|
||||
}
|
||||
case !changeReference.MatchString(b.text):
|
||||
out = append(out, Finding{rel, b.line, ruleReleaseNote, "a change names its issue, pull request or commit", 1})
|
||||
}
|
||||
limit := docProseWidth
|
||||
if b.item {
|
||||
limit = releaseNoteItemWidth
|
||||
}
|
||||
if b.width > limit {
|
||||
out = append(out, Finding{rel, b.line, ruleDocProse,
|
||||
fmt.Sprintf("%s is %d columns, over %d", blockKind(b), b.width, limit),
|
||||
(b.width - limit + proseWeightUnit - 1) / proseWeightUnit})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// docRepo is a git repository holding files, uncommitted: repoFiles lists what
|
||||
// git does not ignore, so no commit is needed for the gate to see them.
|
||||
func docRepo(t *testing.T, files map[string]string) string {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git is not on PATH")
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "repolint-docs")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
for rel, body := range files {
|
||||
path := filepath.Join(dir, filepath.FromSlash(rel))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
git(t, dir, "init", "--quiet")
|
||||
return dir
|
||||
}
|
||||
|
||||
func ruleFindings(findings []Finding, rule string) []Finding {
|
||||
var out []Finding
|
||||
for _, f := range findings {
|
||||
if f.Rule == rule {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestOwnerPatternFollowsCodeOwnersAnchoring(t *testing.T) {
|
||||
cases := []struct {
|
||||
pattern, path string
|
||||
want bool
|
||||
}{
|
||||
{"*", "docs/GUIDE.md", true},
|
||||
{"/docs/", "docs/GUIDE.md", true},
|
||||
{"/docs/", "sub/docs/GUIDE.md", false},
|
||||
{"docs/", "docs/GUIDE.md", true},
|
||||
{"/docs/STUDIO_*.md", "docs/STUDIO_RELEASE.md", true},
|
||||
{"/docs/STUDIO_*.md", "docs/nested/STUDIO_RELEASE.md", false},
|
||||
{"*.md", "a/b/c.md", true},
|
||||
{"/release-notes/**/*.md", "release-notes/studio/2.0.0.md", true},
|
||||
{"/SECURITY.md", "SECURITY.md", true},
|
||||
{"/SECURITY.md", "docs/SECURITY.md", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ownerPattern(c.pattern).MatchString(c.path); got != c.want {
|
||||
t.Errorf("%s vs %s = %v, want %v", c.pattern, c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeOwnersNeedsALivePathAndTwoPeople(t *testing.T) {
|
||||
root := docRepo(t, map[string]string{
|
||||
".github/CODEOWNERS": "* @a @b\n/docs/ @a\n/gone.yml @a @b\n/kept.md @a @a\n",
|
||||
"docs/x.md": "x",
|
||||
"kept.md": "x",
|
||||
})
|
||||
got := ruleFindings(checkCodeOwners(root), ruleCodeOwners)
|
||||
lines := map[int]bool{}
|
||||
for _, f := range got {
|
||||
lines[f.Line] = true
|
||||
}
|
||||
if len(got) != 3 || !lines[2] || !lines[3] || !lines[4] {
|
||||
t.Fatalf("findings = %+v, want the single owner (2), the dead path (3) and the repeated owner (4)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestADocumentNamesItsOwnersAsCodeOwnersDoes(t *testing.T) {
|
||||
header := func(owner, backup string) string {
|
||||
return "---\nowner: " + owner + "\nbackup: " + backup + "\nstatus: active\nreviewed: 2026-09-17\n---\n# T\n"
|
||||
}
|
||||
root := docRepo(t, map[string]string{
|
||||
".github/CODEOWNERS": "* @a @b\n/docs/ @b @a\n",
|
||||
"docs/good.md": header("@b", "@a"),
|
||||
"docs/swapped.md": header("@a", "@b"),
|
||||
"docs/bare.md": "# No header\n",
|
||||
"top.md": header("@a", "@b"),
|
||||
"README.md": "# Landing page, owned through CODEOWNERS only\n",
|
||||
"REASONIX.md": "# Read by a model verbatim\n",
|
||||
})
|
||||
got := ruleFindings(checkDocs(root), ruleDocOwner)
|
||||
flagged := map[string]string{}
|
||||
for _, f := range got {
|
||||
flagged[f.File] = f.Msg
|
||||
}
|
||||
if len(flagged) != 2 || flagged["docs/swapped.md"] == "" || flagged["docs/bare.md"] == "" {
|
||||
t.Fatalf("flagged = %v, want exactly the swapped owners and the missing header", flagged)
|
||||
}
|
||||
if !strings.Contains(flagged["docs/swapped.md"], "CODEOWNERS line 2") {
|
||||
t.Fatalf("the finding must point at the rule it disagrees with: %s", flagged["docs/swapped.md"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusAndReviewDateAreChecked(t *testing.T) {
|
||||
rules := []ownerRule{{line: 1, pattern: "*", owners: []string{"@a", "@b"}, match: ownerPattern("*")}}
|
||||
lines := strings.Split("---\nowner: @a\nbackup: @b\nstatus: draft\nreviewed: last week\n---\n", "\n")
|
||||
got := checkDocMetadata("x.md", lines, rules)
|
||||
if len(got) != 1 || got[0].Weight != 2 {
|
||||
t.Fatalf("findings = %+v, want one finding weighing both problems", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProseWidthCountsChineseAsTwoColumns(t *testing.T) {
|
||||
if got := displayWidth("ab中文,"); got != 8 {
|
||||
t.Fatalf("width = %d, want 8", got)
|
||||
}
|
||||
long := strings.Repeat("字", 170)
|
||||
lines := []string{"---", "owner: @a", "---", "# Title", "", long, "", "- short item", "```", long, "```", "| " + long + " |"}
|
||||
got := checkProse("x.md", lines, docProseWidth)
|
||||
if len(got) != 1 || got[0].Line != 6 {
|
||||
t.Fatalf("findings = %+v, want only the paragraph: fences, tables and the header are not prose", got)
|
||||
}
|
||||
if got[0].Weight != 1 {
|
||||
t.Fatalf("weight = %d, want the 20-column excess rounded up to one unit", got[0].Weight)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWrappedParagraphIsOneBlock(t *testing.T) {
|
||||
line := strings.Repeat("word ", 20)
|
||||
blocks := proseBlocks([]string{line, line, line, line, "", "- item", " continued " + line})
|
||||
if len(blocks) != 2 || blocks[0].width != 399 || !blocks[1].item || !strings.Contains(blocks[1].text, "continued") {
|
||||
t.Fatalf("blocks = %+v, want one wrapped paragraph and one item with its continuation", blocks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyOnboardingPagesKeepAChineseCopy(t *testing.T) {
|
||||
root := docRepo(t, map[string]string{
|
||||
".github/CODEOWNERS": "* @a @b\n",
|
||||
"README.zh-CN.md": "# 入门\n",
|
||||
"docs/GUIDE.zh-CN.md": "---\nowner: @a\nbackup: @b\nstatus: active\nreviewed: 2026-09-17\n---\n",
|
||||
"docs/SPEC.zh-CN.md": "---\nowner: @a\nbackup: @b\nstatus: active\nreviewed: 2026-09-17\n---\n",
|
||||
"benchmarks/x.zh-CN.md": "corpus",
|
||||
})
|
||||
got := ruleFindings(checkDocs(root), ruleDocLanguage)
|
||||
if len(got) != 1 || got[0].File != "docs/SPEC.zh-CN.md" {
|
||||
t.Fatalf("findings = %+v, want only the non-onboarding translation", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAReleaseNoteIsGroupedReferencedAndShort(t *testing.T) {
|
||||
note := strings.Join([]string{
|
||||
"一句话概括本版。",
|
||||
"",
|
||||
"## 修复",
|
||||
"- 读图模型不再说看不到图 (44150b0aa)",
|
||||
"- 没有引用的改动",
|
||||
"- " + strings.Repeat("长", 110) + " #10435",
|
||||
"",
|
||||
"### 细节",
|
||||
"",
|
||||
"第二段散文。",
|
||||
"",
|
||||
"## 亮点",
|
||||
}, "\n")
|
||||
got := checkReleaseNote("release-notes/studio/9.9.9.md", strings.Split(note, "\n"))
|
||||
byRule := map[string]int{}
|
||||
for _, f := range got {
|
||||
byRule[f.Rule]++
|
||||
}
|
||||
if byRule[ruleReleaseNote] != 4 || byRule[ruleDocProse] != 1 {
|
||||
t.Fatalf("findings = %+v, want sub-heading, unknown group, unreferenced item, second paragraph, and one long item", got)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,11 @@ const (
|
||||
ruleFrontendParity = "frontend-parity"
|
||||
ruleFlatView = "flat-view"
|
||||
ruleBuildArtifact = "build-artifact"
|
||||
ruleCodeOwners = "codeowners"
|
||||
ruleDocOwner = "doc-owner"
|
||||
ruleDocProse = "doc-prose"
|
||||
ruleDocLanguage = "doc-language"
|
||||
ruleReleaseNote = "release-note"
|
||||
)
|
||||
|
||||
var allRules = []string{
|
||||
@@ -50,6 +55,7 @@ var allRules = []string{
|
||||
ruleFuncSize, ruleComplexity, ruleStructState, ruleRefusalPath, ruleErrorText,
|
||||
ruleClaudeDialect, ruleSpecParity, ruleWireParity, ruleOrphan,
|
||||
ruleFrontendParity, ruleFlatView, ruleBuildArtifact,
|
||||
ruleCodeOwners, ruleDocOwner, ruleDocProse, ruleDocLanguage, ruleReleaseNote,
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -193,6 +199,8 @@ func run(root string) ([]Finding, error) {
|
||||
findings = append(findings, checkLayering(imports)...)
|
||||
findings = append(findings, checkSpecParity(root)...)
|
||||
findings = append(findings, checkBuildArtifacts(root)...)
|
||||
findings = append(findings, checkCodeOwners(root)...)
|
||||
findings = append(findings, checkDocs(root)...)
|
||||
findings = append(findings, wires.findings(root)...)
|
||||
findings = append(findings, checkTransientTagParity(root)...)
|
||||
findings = append(findings, checkInboxRefusalParity(root)...)
|
||||
|
||||
Reference in new issue
Block a user