feat(link): 連結一律寫成 [文字](絕對網址),寫入前先驗證連得到

取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與
「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上
看起來像普通文字或死連結,巡不到也修不了。

連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API,
不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把
好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁
整批判死。
This commit is contained in:
2026-09-02 14:27:18 +08:00
parent fc4e4a00e2
commit f4e489ceb4
17 changed files with 214 additions and 56 deletions
+23 -7
View File
@@ -9,7 +9,7 @@ Goal: complete the analysis page's todos one by one; **update the wiki status im
`jsc-gitea/tools/hash-id` prints the **full 40-character uppercase SHA-1** of its input — no truncation to 8 characters, no prefix rewrite. Every `{HASH}` this stage builds carries that full length: the page names, the worktree path and the work ticket of step 6 alike. Never shorten one by hand.
**Content pages and directory pages live in different wiki repos.** `ANALYZE_{HASH}` sits in the repo `jsc-gitea/tools/gitea.sh wiki-repo ANALYZE` resolves and `DELIVER_{HASH}` in the one `gitea.sh wiki-repo DELIVER` resolves, while the directory pages `ANALYZE_CONTENTS`, `DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` all sit in the repo `gitea.sh wiki-repo CONTENTS` resolves: `JSC_WIKI_REPO_CONTENTS` first, `JSC_WIKI_REPO` second, exit 3 when neither is set; it **never** falls back to the page type's own variable. Because directory and content pages sit in different wikis, every directory row links its content page by the absolute URL from `gitea.sh wiki-url {content repo} {page}`; `[[...]]` resolves only inside one wiki and would dead-link from the directory.
**Content pages and directory pages live in different wiki repos.** `ANALYZE_{HASH}` sits in the repo `jsc-gitea/tools/gitea.sh wiki-repo ANALYZE` resolves and `DELIVER_{HASH}` in the one `gitea.sh wiki-repo DELIVER` resolves, while the directory pages `ANALYZE_CONTENTS`, `DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` all sit in the repo `gitea.sh wiki-repo CONTENTS` resolves: `JSC_WIKI_REPO_CONTENTS` first, `JSC_WIKI_REPO` second, exit 3 when neither is set; it **never** falls back to the page type's own variable. Every directory row links its content page by the absolute URL from `gitea.sh wiki-url {content repo} {page}`, written as `[{text}]({url})` — one link syntax, whichever wiki the two pages sit in. The syntax and the check that runs before every write: "Every link is checked before it reaches a page" below.
All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or write stops this stage**: report which page and which operation failed, never carry on against a page you could not read, and never report a page as saved when the write failed. Step 13 still runs after such a stop.
@@ -64,7 +64,7 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
3. **Start both audits together and let them run side by side** — they read the same diff and neither one's verdict changes the other's input, so the closing wait costs one audit, not two. Completion condition: both audits have returned, and both cleared — a pass from `code-review`, and either a pass or a reported skip from the API document audit.
11. **One work package finished → commit, push, PR back to the source branch, then hold on that PR until it merges**:
1. Call `jsc-git:pr` from inside the worktree, **passing `{source-branch}` as the base branch**. One package, one PR; the branch ladder and how the base is derived are in `references/branch.md`.
2. Write the PR URL and number into that work package's PR column on the analysis page and save it back to the wiki, so the next run of this skill can find it (step 2).
2. Write the PR URL and number into that work package's PR column on the analysis page as `[{text}]({url})`, **checked with `jsc-gitea/tools/link-check.sh` before the save** — see "Every link is checked before it reaches a page" below — and save the page back to the wiki, so the next run of this skill can find it (step 2).
3. Run `jsc-sdlc/tools/wp-gate.sh lock {owner}/{repo} {index} --wp {wp-number}`. The lock is what makes step 2's gate hold across work sessions — a new session starts blocked until that PR merges — and `--wp` is what hangs this PR on the claim from step 4.5, so `owns` can keep other sessions off it. Leave `--wp` out and every session is free to touch this PR. Branch on the exit code: exit 0 (`status=locked`) → proceed. Exit 2 (`status=usage`) → fix the arguments and run it again. Exit 3 (`status=missing-dep`) → the lock could not be recorded; report it and stop, because without the lock the next session starts unblocked on a PR that has not merged. Completion condition: the script printed `status=locked` and named the work package.
4. **The work package is finished the moment its PR is open — call `jsc-log:worklog` now**, under the Rules section's one-task-one-entry rule, reusing the PLAN and ANALYZE wiki repositories and page URLs resolved for this package in step 2.10. Completion condition: the entry is saved on `LOG_{HASH}` before the wait starts.
5. **Wait for the merge with `jsc-gitea/tools/pr-watch.sh {owner}/{repo} {index}`** — it polls every 60 seconds (`JSC_PR_WATCH_INTERVAL` overrides), never times out, and never repeats a comment it already reported. Branch on its exit code:
@@ -77,7 +77,7 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
12. **Deliver the document, then register for maintenance** — one closing pass over the two questions this stage owes the user. A finished work package is a delivery, so **always ask before producing it; never pick a format silently and never skip either question**:
1. Ask per `jsc-ask:ask` rules which delivery format to produce. The options are fixed: **a `DELIVER_{HASH}` wiki page** or **a Gitea issue comment**. State the impact scope on each (the wiki page lives beside the plan and analysis pages; the issue comment reaches whoever follows that issue).
2. Both formats use the same structure — `templates/deliver-page.md`, in Traditional Chinese. Only the destination differs. Sample values and personal-data handling: `references/deliver-formats.md`.
3. Wiki page: write `DELIVER_{HASH}` into the DELIVER wiki repo through `jsc-gitea:wiki`, where `{HASH}` comes from `jsc-gitea/tools/hash-id` over `{owner}/{repo}` plus the work package number (for example `plugins/sdlc#WP-01`), so each work package gets its own page instead of overwriting the previous one. That input is what keeps the pages apart, and the full 40 characters `hash-id` prints are what the page is named — hashing the repository alone, or shortening the result, puts two work packages on one page again. Then upsert this work package's row with `jsc-gitea/tools/wiki-contents.sh upsert DELIVER 6 {HASH} {row file} templates/deliver-contents.md`: the row follows `templates/deliver-contents.md` and the key is the HASH column, column 6, written exactly as the row file writes it. Its 交付頁 cell holds the absolute URL from `gitea.sh wiki-url {DELIVER repo} DELIVER_{HASH}` — run that **after** the delivery page is saved and branch on its exit code, the same branching `jsc-log:worklog` runs over the same call:
3. Wiki page: write `DELIVER_{HASH}` into the DELIVER wiki repo through `jsc-gitea:wiki` — **every link that page carries is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the write**, per "Every link is checked before it reaches a page" below — where `{HASH}` comes from `jsc-gitea/tools/hash-id` over `{owner}/{repo}` plus the work package number (for example `plugins/sdlc#WP-01`), so each work package gets its own page instead of overwriting the previous one. That input is what keeps the pages apart, and the full 40 characters `hash-id` prints are what the page is named — hashing the repository alone, or shortening the result, puts two work packages on one page again. Then upsert this work package's row with `jsc-gitea/tools/wiki-contents.sh upsert DELIVER 6 {HASH} {row file} templates/deliver-contents.md`: the row follows `templates/deliver-contents.md` and the key is the HASH column, column 6, written exactly as the row file writes it. Its 交付頁 cell holds the absolute URL from `gitea.sh wiki-url {DELIVER repo} DELIVER_{HASH}`, written as `[{text}]({url})` — run that **after** the delivery page is saved and branch on its exit code, the same branching `jsc-log:worklog` runs over the same call:
| Exit | What this step does |
| --- | --- |
@@ -87,10 +87,10 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
| 7 | the key is invalid or lacks permission (HTTP 401/403) — stop and report the key problem. Never read this as exit 4: the delivery page is alive, and treating it as absent records a delivered work package as one that was never delivered |
| 8 | any other API failure — stop and report that status, and do not retry the same call unchanged |
Never let an empty string stand in for the URL: a row whose 交付頁 cell is empty names a delivery nobody can open, and the next run replaces that row as if it were correct. Branch on the upsert's own exit code per "Contents pages are appended, never overwritten" below, and never hand-edit the directory page.
4. Issue comment: confirm the issue number with the user (propose the one referenced by the work package or the PR; never guess), then post via `jsc-gitea/tools/gitea.sh api POST /repos/{owner}/{repo}/issues/{n}/comments` with the body passed in as a UTF-8 file — real newlines, never a literal `\n`.
5. With the delivery produced, ask per `jsc-ask:ask` rules whether to register this project for maintenance: upsert this repository's row with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {row file} templates/maintain-contents.md`, the row built from `templates/maintain-contents.md` and the key column 1, the repository name. Required: repository `{owner}/{repo}`, maintenance method, start date. Optional: end date (NULL = maintain forever), last-maintained time. `MAINTAIN` has no content page — this directory page is the whole record — so branch on the exit code per "Contents pages are appended, never overwritten" below and never hand-edit it.
6. Completion condition: the chosen delivery format has actually been produced and you have reported where it landed (wiki page name, or the comment URL); on the wiki-page format the `wiki-url` call returned 0 and its URL is the one in the 交付頁 cell, with any non-zero code branched on as sub-step 12.3 says; **and** the user has answered the maintenance question with a chosen registration saved on the wiki.
Never let an empty string stand in for the URL: a row whose 交付頁 cell is empty names a delivery nobody can open, and the next run replaces that row as if it were correct. **Check that URL with `jsc-gitea/tools/link-check.sh` and build the row only on exit 0** — a link that does not answer never goes into a directory everyone else reads. Branch on the upsert's own exit code per "Contents pages are appended, never overwritten" below, and never hand-edit the directory page.
4. Issue comment: confirm the issue number with the user (propose the one referenced by the work package or the PR; never guess), then post via `jsc-gitea/tools/gitea.sh api POST /repos/{owner}/{repo}/issues/{n}/comments` with the body passed in as a UTF-8 file — real newlines, never a literal `\n`. Every link in that body is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the post: a comment is as hard to correct as a page once other people have read it.
5. With the delivery produced, ask per `jsc-ask:ask` rules whether to register this project for maintenance — any link that row carries is written as `[{text}]({url})` and passes `jsc-gitea/tools/link-check.sh` before the upsert: upsert this repository's row with `jsc-gitea/tools/wiki-contents.sh upsert MAINTAIN 1 {owner}/{repo} {row file} templates/maintain-contents.md`, the row built from `templates/maintain-contents.md` and the key column 1, the repository name. Required: repository `{owner}/{repo}`, maintenance method, start date. Optional: end date (NULL = maintain forever), last-maintained time. `MAINTAIN` has no content page — this directory page is the whole record — so branch on the exit code per "Contents pages are appended, never overwritten" below and never hand-edit it.
6. Completion condition: the chosen delivery format has actually been produced and you have reported where it landed (wiki page name, or the comment URL); on the wiki-page format the `wiki-url` call returned 0 and its URL is the one in the 交付頁 cell, with any non-zero code branched on as sub-step 12.3 says; every link written by this step was cleared by a `link-check.sh` run that exited 0; **and** the user has answered the maintenance question with a chosen registration saved on the wiki.
13. **Stage report — the last thing this stage does, including every early stop** (the work package gate blocked, no work package was selectable, the source branch was missing from the remote, a wiki read or write failed). Run `tools/stage-report.sh implement` with:
- one `--page TYPE:{page}` per wiki page this run wrote — `--page ANALYZE:ANALYZE_{HASH}`, `--page DELIVER:DELIVER_{HASH}`, `--page CONTENTS:DELIVER_CONTENTS`, `--page CONTENTS:MAINTAIN_CONTENTS`. **Every directory page takes the `CONTENTS` type**: the script resolves each page's repo from the TYPE you pass, and a directory page passed under its old type resolves the wrong repo and prints no URL;
- `--worklog` and `--worklog-heading` pointing at the entry step 11.4 wrote — following the Rules section's one-task-one-entry rule, a stage that finished anything already has one. `--pending-file {file} --log-hash {HASH}` is the fallback for a stage that stopped before any task finished: it holds the content for the next `jsc-log:worklog` run, and held content is not a written log;
@@ -98,6 +98,22 @@ All wiki reads and writes go through `jsc-gitea:wiki`. **A failed wiki read or w
Rules and exit codes: `references/stage-report.md`. Exit 1 is a warning, never a block. Completion condition: the script's output is reported to the user verbatim, and it names the worktree, all three branches and every wiki page this run wrote.
## Every link is checked before it reaches a page
**One syntax.** Every link this stage writes — on `DELIVER_{HASH}`, in the analysis page's PR column, in the `DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` rows, in an issue comment, in the stage report — is written as `[{text}]({url})`. The `{url}` is the absolute URL `gitea.sh wiki-url {repo} {page}` printed, used verbatim: never assemble a wiki path by hand, and never write a link as `[[頁名]]` or `[[顯示文字|頁名]]`. That form resolves only inside the wiki it sits in, and it fails without an error — the reader sees plain text or a dead link, so a wrong link is neither noticed nor fixable.
**Checked before it is written.** Collect every link the page, the row or the comment is about to carry, hand them all to `jsc-gitea/tools/link-check.sh` in one run — `link-check.sh {網址}...`, or the same URLs on stdin, one per line — and write only when that run exits 0. It prints one `{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}` line per URL. The check goes through the API, never a web status code: a private repository's web URL answers 404 to a request carrying no key, so a status-code check marks live pages dead.
| Exit | What this step does |
| --- | --- |
| 0 | every link answers — write the page, upsert the row, post the comment |
| 1 | at least one link is dead — **write nothing**, and report the `DEAD` lines to the user |
| 2 | usage error: not one URL was passed — pass the links and run it again |
| 3 | the list holds a Gitea URL but `GITEA_HOST` is unset — report it as a setting to fix and run it again, and never skip the check instead |
| 7 | Gitea authentication failed (401/403) — stop and report the key problem. Never read this as exit 1: an expired key makes live pages look absent, and a page rewritten on that reading loses the links that were fine |
Completion condition: every page, row and comment this stage wrote was cleared by a `link-check.sh` run that exited 0, and every non-zero code was branched on as this table says.
## Contents pages are appended, never overwritten
`DELIVER_CONTENTS` and `MAINTAIN_CONTENTS` are shared directories in the CONTENTS wiki repo: every row on them belongs to somebody's work package or repository, and this run reads none of those rows from anywhere else. So every write to them is an upsert of one row — never a whole-page overwrite, and never a row this run does not own. `jsc-gitea/tools/wiki-contents.sh upsert` is the one way this stage does it: it resolves the CONTENTS repo, reads the whole page, replaces the row whose key column matches and appends when none matches, then writes the page back.