@akinet/akidevrule 3.0.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.
- package/CHANGELOG.md +835 -0
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/claude/CLAUDE.md +40 -0
- package/claude/agents/aki-challenger.md +38 -0
- package/claude/agents/aki-conduct.md +54 -0
- package/claude/agents/aki-hands.md +59 -0
- package/claude/agents/aki-judge.md +37 -0
- package/claude/agents/aki-maker.md +36 -0
- package/claude/fragments/settings.akidoc.fragment.json +15 -0
- package/claude/hooks/aki-update-check.mjs +160 -0
- package/claude/hooks/aki_version_check.mjs +83 -0
- package/docs/ref/macos-codesign-tcc.md +59 -0
- package/install.mjs +1067 -0
- package/install.ps1 +11 -0
- package/install.sh +12 -0
- package/package.json +52 -0
- package/payload/GEMINI.md +147 -0
- package/payload/METHOD-audit-flow.md +147 -0
- package/payload/METHOD-audit-subtraction.md +67 -0
- package/payload/METHOD-audit-zero-trust.md +49 -0
- package/payload/METHOD-deep-think.md +172 -0
- package/payload/METHOD-proportionality.md +62 -0
- package/payload/METHOD-ux-psych.md +60 -0
- package/payload/RULE-agent-behavior.md +138 -0
- package/payload/RULE-biz.md +51 -0
- package/payload/RULE-coding.md +130 -0
- package/payload/RULE-content-write.md +54 -0
- package/payload/RULE-db-design.md +26 -0
- package/payload/RULE-docs.md +144 -0
- package/payload/RULE-pattern-core.md +80 -0
- package/payload/RULE-release.md +215 -0
- package/payload/RULE-seo.md +173 -0
- package/payload/RULE-stack-akiNuxtCf.md +179 -0
- package/payload/RULE-stack-tauri.md +59 -0
- package/payload/RULE-ui-pattern.md +167 -0
- package/payload/index.md +91 -0
- package/skills/aki-article-writer/SKILL.md +50 -0
- package/skills/aki-article-writer/references/article-workflow.md +377 -0
- package/skills/akidevsync-notes/SKILL.md +48 -0
- package/skills/akidevsync-notes/scripts/notes_cli.py +212 -0
- package/skills/akiflow/SKILL.md +221 -0
- package/skills/akiflow/references/harness-facts.md +215 -0
- package/skills/akiflow/scripts/council-cost.sh +4 -0
- package/skills/akiflow/scripts/council-open.sh +4 -0
- package/skills/akiflow/scripts/council-read.sh +4 -0
- package/skills/akiflow/scripts/council-verify.sh +4 -0
- package/skills/akiflow/scripts/council_cost.py +149 -0
- package/skills/akiflow/scripts/council_open.py +323 -0
- package/skills/akiflow/scripts/council_read.py +148 -0
- package/skills/akiflow/scripts/council_verify.py +315 -0
- package/skills/akiflow/scripts/scythe.py +307 -0
- package/skills/akiflow/scripts/scythe.sh +4 -0
- package/skills/akigitcommit/SKILL.md +85 -0
- package/skills/akihelp/SKILL.md +47 -0
- package/skills/akihtmlreport/SKILL.md +59 -0
- package/skills/akilint/SKILL.md +29 -0
- package/skills/akirule/SKILL.md +155 -0
- package/skills/akiship/SKILL.md +55 -0
- package/skills/akithink/SKILL.md +59 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Release & Versioning Rule
|
|
2
|
+
|
|
3
|
+
<!-- Address map: release.A1-5 · release.B1-9 · release.C1-4 (⟨Aki⟩) -->
|
|
4
|
+
|
|
5
|
+
## A. Versioning core
|
|
6
|
+
|
|
7
|
+
### A1. Scope — when this applies, CHANGELOG mandatory
|
|
8
|
+
Every Aki project, any stack (Nuxt web — see `RULE-stack-akiNuxtCf.md` — Tauri v2, CLI, …). `CHANGELOG.md` is **mandatory from project creation**: the commit workflow and versioning discipline both anchor to it, so a repo without one is broken, not exempt — create it as the first fix. If a change is user-visible or dev-relevant, the release artifacts below must move with the code in the same task — never edit code and leave them stale.
|
|
9
|
+
|
|
10
|
+
**Release vs deploy — two different events.** A *release* defines a version of the app (CHANGELOG, releases.json, GitHub Release) and applies to every project type. A *deploy* puts a web build live and is web-only — see `RULE-stack-akiNuxtCf.md` § Deploy verification. This rule owns releases only.
|
|
11
|
+
|
|
12
|
+
### A2. Versioning — semver `major.minor.patch`
|
|
13
|
+
- **patch** — bug fixes, internal-only changes
|
|
14
|
+
- **minor** — new backward-compatible features
|
|
15
|
+
- **major** — breaking changes
|
|
16
|
+
- One release = one version; bundle the session's changes under it. Bump deliberately — do not bump on every tiny edit, and do not skip a bump when something shipped.
|
|
17
|
+
|
|
18
|
+
### A3. Version string format (ABSOLUTE — never violate)
|
|
19
|
+
The version *attribute itself* is always bare semver, **never prefixed with `v`**: `package.json`/`Cargo.toml`/equivalent manifest `"version"` field, and every git tag, are `1.10.1` — not `v1.10.1`. This is a real bug class, not a style nit: an inconsistent prefix across tags silently breaks semver comparisons and diffing tools (`git describe`, `hasUpdate()`-style JS comparisons against a fetched tag name), and produces doubled-up UI text when display code does `` `v${version}` `` against a value that already contains `v` (rendered as `vv1.10.1`).
|
|
20
|
+
- **Tag priority is stack-dependent.** Distributed-artifact apps (Tauri/Desktop/CLI) already cut a tag on every release build (A5's atomic bump+tag+build) — treat tagging as standard practice there, not optional, and give it real weight. Web (continuously-deployed) apps already have CHANGELOG.md/releases.json/GitHub Release as the authoritative record, so a tag there is a lighter-weight, optional checkpoint: mention it only lightly for an ordinary patch release, but actively recommend cutting one once a release bundles a major/minor bump or several substantial changes — that is exactly when a rollback/bisect anchor earns its keep.
|
|
21
|
+
- Tag only if the project already tags, or the user accepts a suggestion above: `git tag -l` empty and no acceptance → skip tag creation; CHANGELOG/releases.json/GitHub Release stay authoritative. Creating and pushing a tag is visible to others once pushed — propose it, never create or push one unasked ([[RULE-agent-behavior]] B3). Exception — an authorized full-release run (B8): the invocation is the acceptance, so an existing tag convention is followed mechanically (create the bare tag, no ask); an empty tag list still means skip, mentioned once. Pushing the tag stays bound to the run's push authorization.
|
|
22
|
+
- Create tags bare: `git tag 1.10.1`, never `git tag v1.10.1`.
|
|
23
|
+
- Before cutting any release, check the existing tag convention with `git tag -l | sort -V | tail -5` — if a project's history has drifted to `v`-prefixed tags partway through, treat that drift as the bug being corrected (go back to bare), not as the precedent to keep following.
|
|
24
|
+
- Human-facing display **may** prepend `v` at render time only — a GitHub Release title (`v{version}: …`, see below), a UI badge ("Update Available — v1.10.1"). That is a presentation concern, separate from and does not violate this rule. The forbidden thing is `v` baked into the stored/compared value itself.
|
|
25
|
+
- When resolving the last release's boundary commit, prefer the bare-tag form: `git rev-parse "<last-version>"`, falling back to `git rev-parse "v<last-version>"` only to read a legacy/already-existing `v`-prefixed tag — never as the form to create going forward.
|
|
26
|
+
|
|
27
|
+
### A4. Bump level — driven by content severity, not by step-count
|
|
28
|
+
Classify every accumulated change found in the git log:
|
|
29
|
+
- breaking / not backward-compatible → major
|
|
30
|
+
- new capability, backward-compatible → minor
|
|
31
|
+
- fix / internal-only → patch
|
|
32
|
+
|
|
33
|
+
**New version = the last version recorded in CHANGELOG (the Pre-bump baseline from the state table in B1) + exactly one step at the HIGHEST severity found across the full accumulation.** Do not add steps per session or per commit.
|
|
34
|
+
|
|
35
|
+
Unsure between two levels → choose the smaller, state the reason.
|
|
36
|
+
|
|
37
|
+
A jump like `1.4.2 → 2.0.0` is a correct single major step if the accumulation contains a breaking change. A jump like `1.4.2 → 1.6.0` remains invalid because it skips the minor version `1.5.0` (minor must only increment by 1).
|
|
38
|
+
|
|
39
|
+
### A5. A version is minted at the release event, never at work-completion (ABSOLUTE)
|
|
40
|
+
|
|
41
|
+
Finishing a piece of work does not earn a version number. Only shipping does — a production deploy, a published tag, a distributed build.
|
|
42
|
+
|
|
43
|
+
- **Continuously-deployed Web / Service Apps**: Between releases, the accumulation lives under `## [Unreleased]` at the top of `CHANGELOG.md` with **no version number and no manifest bump**. When the release actually happens, rename that `[Unreleased]` heading to the new version and bump the manifest once. Local version == production version at all times.
|
|
44
|
+
- **Distributed Artifact Apps (Tauri Desktop App, CLI Binaries, compiled packages)**:
|
|
45
|
+
- Atomic bump + tag + build in the same release task is **PERMITTED** (because the version string is baked into the compiled binary artifact at build time).
|
|
46
|
+
- **Mandatory Pre-Bump Guard (ABSOLUTE)**: This guards minting the **next** version — it does **not** block finishing the current one. Being at a manifest version with no tag yet is the *normal* mid-release state here: the manifest is bumped when work on the version starts, and the tag is cut only at build. The guard fires only when you would stack a **second** unshipped version on top of the first. Before advancing the manifest to a new, higher number, verify the current manifest version already has a matching tag/release build (`git tag -l "<current_manifest_version>"`). If it does not, **finish the current version (cut its tag/build) first** — do not open another version on top of an unshipped one (state: **Drifted**, see B1). Completing the current version (tag + build at its own number) is never blocked.
|
|
47
|
+
|
|
48
|
+
- **Never bump the manifest `version` field in the same task as a routine code change (Web apps)**. The bump belongs to the release task alone. A task that ends with "bumped to 0.2.0" but nothing deployed is the bug for web apps.
|
|
49
|
+
- **Many sessions collapse into one version, not one version per session.** Three or four rounds of local improvement on top of a `0.1.0` production release ship as `0.2.0` (or `0.1.1`, or `1.0.0` — whatever A4's severity rule gives), **never** as `0.3.4`.
|
|
50
|
+
- **Materiality test before minting.** A version the user sees must be worth seeing. If the whole accumulation is one or two trivial internal lines, do not mint it — leave it in the bucket and let it ride with the next real change. A release with three versions of two bullets each is the symptom this rule exists to prevent; those should have been one version.
|
|
51
|
+
- **Recovery when drift already happened.** Versions that were never published to production/tagged are not public, so they are **not protected by B3's "never renumber public versions"**. Squash them: collapse every unpublished version's entries into one `[Unreleased]` section, reset the manifest to the last *actually released* version, then mint one version. Only versions with a real deploy/tag/build behind them are frozen. If it is unclear whether a version ever shipped, treat it as shipped and ask the user.
|
|
52
|
+
- **Date = confirmed live, not drafted.** The version's date (CHANGELOG heading, `releases.json`) is the day C5's live-verification passes, never the day the entry was drafted or committed. Readers read the date as "when I got this," not "when the dev wrote the note." Same-day push makes the two dates match by default; they diverge on a late-night commit or a build that fails and gets fixed the next day — verified-live wins.
|
|
53
|
+
|
|
54
|
+
## B. Identify & audit
|
|
55
|
+
|
|
56
|
+
### B1. Identify the current version — cold-start, not session-memory
|
|
57
|
+
|
|
58
|
+
Run this check **each time a problem is closed and about to be recorded** — not once at the end of a session. It answers "does this entry go into a new version or the one already open?" Never rely on remembering a prior session: every time this step runs — 5 minutes or 5 months since the last run — it must re-derive the correct state from the repo alone.
|
|
59
|
+
|
|
60
|
+
1. Read `package.json` (or equivalent) for the recorded version.
|
|
61
|
+
2. Read `CHANGELOG.md` to identify the last documented version (`<last-version>`).
|
|
62
|
+
3. Determine the release state **before** deciding any bump. Web app: before concluding *Pre-bump* or *Mid-release*, confirm the top CHANGELOG version actually reached production (step 4b's remote check) — a manifest and CHANGELOG agreeing on a version production never received are consistent-looking drift, not Pre-bump. *Unreleased open* needs no remote check.
|
|
63
|
+
|
|
64
|
+
| State | Condition | Action |
|
|
65
|
+
|-------|-----------|--------|
|
|
66
|
+
| **Unreleased open** | `CHANGELOG.md` has an `## [Unreleased]` section on top | Normal working state (A5) — append the entry there, do NOT bump anything |
|
|
67
|
+
| **Pre-bump** | `package.json` == CHANGELOG top version | Nothing pending. Recording a change → open a new `## [Unreleased]`; releasing now → reconstruct the accumulation (steps 4–5) and mint exactly once |
|
|
68
|
+
| **Mid-release** | `package.json` > CHANGELOG top version | Already bumped, version still open — append to it, do NOT bump again |
|
|
69
|
+
| **Drifted** | unshipped versions sit above the last deployed/tagged version — the threshold follows A5's two shipping models. **Web / continuously-deployed: ≥1** — a single CHANGELOG version entry or manifest bump with no production deploy behind it already breaks A5's "local == production"; web has no normal one-ahead state. **Distributed-artifact (Tauri/CLI): ≥2** — one untagged version matching the manifest is the normal mid-release state (A5's Pre-Bump Guard); resolve via *Unreleased open* / *Pre-bump* above, do not block | Apply A5's recovery (squash unpublished entries back into `[Unreleased]`, reset the manifest to the last actually-released version, mint once) before doing anything else. Recovery always runs **backward**: never legitimize an orphan forward by backfilling it as a tag/GitHub Release — it was never public, so it has no history to protect (B3 shields published versions only) |
|
|
70
|
+
| **Mismatch** | any other disagreement | Warn the user, do not auto-fix |
|
|
71
|
+
|
|
72
|
+
4. Find the boundary commit for `<last-version>` using this sequence:
|
|
73
|
+
a. The commit that wrote the CHANGELOG entry — the strongest anchor, since the entry itself marks the release boundary: `git log -1 --format=%H -S "[<last-version>]" -- CHANGELOG.md`
|
|
74
|
+
b. Production baseline verification (stack-specific, strictly remote):
|
|
75
|
+
- **App (Tauri/Desktop/CLI):** Check remote git tags (`git ls-remote --tags origin`) and GitHub Releases.
|
|
76
|
+
- **Web (Nuxt Cloudflare / AkiNuxtCf):** Check remote GitHub state (published `app/data/releases.json` / remote GitHub tags).
|
|
77
|
+
c. A release commit message — use fixed strings (`.` is a regex wildcard): `git log --fixed-strings --grep="<last-version>" -n 1 --format="%H"` (A later commit that merely *mentions* the version, e.g. "fix regression from 1.4.2", is NOT the boundary — inspect the hit before trusting it.)
|
|
78
|
+
d. If no boundary is found, **do not scan the entire history**. Fall back to `git log --oneline -20`, analyze manually, and ask the user to confirm the boundary if there is any ambiguity.
|
|
79
|
+
5. Run `git log <boundary-commit>..HEAD --oneline` to get the complete, unbounded list of accumulated changes since the last release.
|
|
80
|
+
6. Fresh repo: fewer than ~5 commits, or no version recorded anywhere yet → treat the entire history as the current accumulation.
|
|
81
|
+
|
|
82
|
+
### B2. The real anti-skip invariant
|
|
83
|
+
|
|
84
|
+
A version jump is only actually wrong when there is evidence that a release boundary was already completed and left unrecorded. Concretely:
|
|
85
|
+
- Every git tag matching a version pattern (if tags are used) MUST have exactly one matching CHANGELOG entry.
|
|
86
|
+
- Every entry in `app/data/releases.json` (web stacks) MUST have exactly one matching CHANGELOG entry, and vice versa.
|
|
87
|
+
- CHANGELOG versions must increase monotonically with no gaps or duplicates.
|
|
88
|
+
|
|
89
|
+
If a tag or milestone exists without a matching entry, write the missing entry retroactively. Do not just warn and move on.
|
|
90
|
+
|
|
91
|
+
### B3. Audit mode — for legacy or imported projects
|
|
92
|
+
|
|
93
|
+
Run once when `CHANGELOG.md` was not produced under this rule from project inception:
|
|
94
|
+
1. Verify monotonic order of all versions in `CHANGELOG.md`.
|
|
95
|
+
2. Cross-check against all version-pattern git tags.
|
|
96
|
+
3. Cross-check against `app/data/releases.json` (if it exists).
|
|
97
|
+
4. Report mismatches and propose retroactive entries for any gaps. Never renumber or delete public versions.
|
|
98
|
+
5. If a gap's historical content cannot be determined (a tag exists but nobody knows what it contained), the retroactive entry must say so explicitly ("historical content unknown") — never invent or infer changes that cannot be verified.
|
|
99
|
+
|
|
100
|
+
### B4. GitHub Release — create it, do not just describe it
|
|
101
|
+
|
|
102
|
+
A pushed git tag is **not** a release: A1 lists the GitHub Release as a required artifact alongside CHANGELOG/releases.json. Stopping at `git push --tags` and reporting "released" is the exact gap this item closes — the Releases page still shows the previous version while the tag and code have moved on.
|
|
103
|
+
|
|
104
|
+
After updating CHANGELOG and the version bump, produce the GitHub Release without waiting to be asked:
|
|
105
|
+
- **Repo publishes GitHub Releases and `gh` is available** (`gh release list` succeeds) → create it directly: `gh release create <tag> --title "…" --notes-file <file> --latest`. An already-pushed tag is reused, not duplicated. Verify with `gh release list` that the new version now reads `Latest`.
|
|
106
|
+
- **Otherwise** (no `gh`, or the user will publish manually) → output the copy-ready block below instead.
|
|
107
|
+
- Before minting, cross-check tags against Releases (`gh release list` vs `git tag`) and offer to backfill any tag that has no matching Release, so the Releases page has no gaps.
|
|
108
|
+
|
|
109
|
+
**Title:** `v{version}: {2-5 word specific impact}` — no generic words ("patch fixes", "bug fixes", "improvements")
|
|
110
|
+
- Good: `v1.5.1: fix production icons blank, caret, grid gap`
|
|
111
|
+
- Bad: `v1.5.1: patch fixes`, `v1.5.1: various improvements`
|
|
112
|
+
|
|
113
|
+
**Body:** same `#### Fixed` / `#### Changed` / `#### Added` sections as CHANGELOG, but each bullet trimmed to one short sentence — symptom first, no file paths, no internal jargon.
|
|
114
|
+
|
|
115
|
+
**Compare link (GitHub-hosted repos — mandatory footer):** the notes end with `**Full Changelog**: <repo-url>/compare/<prev-tag>...<new-tag>` — or use `gh release create --generate-notes`, which inserts it automatically. The Release page renders notes only, never a diff, and the tag itself points at a single commit (usually the version-mint commit, a tiny diff) — without this line there is no one-click view of the commits accumulated since the previous release. First release with no prior tag: link `<repo-url>/commits/<new-tag>` instead.
|
|
116
|
+
|
|
117
|
+
**`--generate-notes` alone is not a substitute for Title/Body above.** It derives content from merged PRs only; a repo that commits straight to trunk (no PR history) gets a near-empty body — footer line only. Pair it with `--notes-file` for real content, or, when release creation is CI-automated rather than run interactively, have the workflow itself extract the tagged version's CHANGELOG section into the notes file — the content requirement above still applies even though no one is typing the `gh release create` command by hand.
|
|
118
|
+
|
|
119
|
+
### B5. Migration/infra completeness gate — a schema or infra change is not "released" until it ran
|
|
120
|
+
|
|
121
|
+
A CHANGELOG or `releases.json` entry that describes a database schema change or any other infra-dependent change (migration, remote config, env var, cron/schedule registration — see [[RULE-coding]] B3) is a claim that the change is live. That claim is only true once two things both hold, not one:
|
|
122
|
+
|
|
123
|
+
1. **The migration/infra action actually succeeded against the real target** — for a DB change, the migration ran against the production database (not just local/dev), and its own stated postconditions were checked (row counts, expected columns/tables), not assumed.
|
|
124
|
+
2. **The script is marked complete in the repo's own convention** — e.g. moved out of a pending location (`scripts/`) into its done location (`scripts/done/`), or whatever equivalent completion marker the project uses. A migration file still sitting in the pending location is itself the signal that step 1 has not been confirmed, regardless of what the CHANGELOG says.
|
|
125
|
+
|
|
126
|
+
Do not report a plan, task, or release/deploy as complete when a migration/infra step it depends on has not cleared **both** conditions. A written migration script plus a "Added" changelog line with the actual execution still outstanding is exactly the failure this gate exists to catch — the code shipped, the database did not, and nothing else in the release checklist would have noticed.
|
|
127
|
+
|
|
128
|
+
### B6. Content discipline
|
|
129
|
+
- Release note copy: no em/en dash (`—` `–`); short user-facing sentences, benefit first. See [[RULE-content-write]].
|
|
130
|
+
- Keep terminology stable across versions (e.g. always "Release Notes", not mixed synonyms). See [[RULE-content-write]] semantic stability.
|
|
131
|
+
- Doc/version moves are part of the change, not an afterthought. See [[RULE-docs]].
|
|
132
|
+
|
|
133
|
+
### B7. Pre-ship gate — work finished, nothing pushed yet
|
|
134
|
+
|
|
135
|
+
The last moment a mistake is still cheap: the work is done, the tree is clean, and nothing is public. This gate **composes rules that already exist** rather than adding new ones — its value is being one entry point instead of five scattered ones. It is a **pass/fail check, not a report**: every failure is fixed before shipping, and it produces no audit doc (contrast `docs.C`, which records findings precisely because its baseline is already published).
|
|
136
|
+
|
|
137
|
+
Run in order; each step names the rule that owns it.
|
|
138
|
+
|
|
139
|
+
0. **Leftover triage** — a tree that is not uniformly finished is classified first: finished / mid-edit / abandoned / accidental (the `/akigitcommit` step-0 taxonomy, under [[RULE-agent-behavior]] B5's read-only floor). Mid-edit vs abandoned is undecidable from the tree alone — that is an escalation (B8), never a guess.
|
|
140
|
+
1. **Release state** — derive it cold from the repo per B1, never from session memory. `Drifted` blocks everything until A5's recovery has run.
|
|
141
|
+
2. **Hygiene sweep — scoped to the accumulation, never the whole repo.** On the files touched since the boundary commit (B1.5): scythe `[WRAP]`/`[YAP]` lint ([[RULE-agent-behavior]] §0), dead code / redundant guards / duplication the accumulation itself introduced (`pattern.A8`; subtract-class detectors at diff scope), and doc references in touched comments still resolving ([[RULE-docs]] B3). A repo-wide subtraction or zero-trust sweep is a separately scheduled audit, never a per-release cost — diff scope is what keeps this gate affordable at many releases per day. Unlike an audit, findings here are fixed in place: this is a gate, not a report.
|
|
142
|
+
3. **External-action completeness** — every change whose "done" depends on something outside the repo actually happened: migrations ran against the real target and their postconditions were checked, remote config/env vars/cron registrations are live, and each script sits in its completion location (B5, [[RULE-coding]] B3). A green build proves nothing about the database.
|
|
143
|
+
4. **Record truthfulness** — every closed problem has its `CHANGELOG.md` entry, and no entry claims something step 3 has not cleared (B2). Web stacks additionally need `releases.json` parity (C3).
|
|
144
|
+
5. **Doc sync — every record surface the accumulation touched, not only `docs/`.** Enumerate, then check each against the diff: plans whose work shipped moved to `docs/plan/done/`; `arch`/`feat` docs match what is about to ship ([[RULE-docs]] B1, B3); `README.md` wherever the accumulation changed setup, commands, layout, or a documented behavior; the project's task-note file when one exists (`.akidevsync/notes.json`, edited only through the `akidevsync-notes` skill — a note whose fix is in this accumulation is marked done with the matching CHANGELOG line, an unmatched or unverified one stays open and is named in the report); and any external standards doc the project `CLAUDE.md` binds the project to, updated in place when the accumulation changed a convention that doc owns. A surface skipped because it was not in `docs/` is the same drift finding as a stale doc.
|
|
145
|
+
6. **Verification honesty** — anything only checkable at runtime is reported as unverified rather than assumed ([[RULE-coding]] B3). "Untested but I expect it works" is a valid gate output; a silent "Done" is not.
|
|
146
|
+
7. **Version decision** — mint or defer per A4/A5's materiality test. Do not mint a version to mark that a session ended.
|
|
147
|
+
|
|
148
|
+
Deploy verification is deliberately **not** in this gate — it happens after the push, against the live target, and is owned by the stack rule.
|
|
149
|
+
|
|
150
|
+
### B8. Autonomous full-release run — only an explicit `/akiship` invocation is the authorization
|
|
151
|
+
|
|
152
|
+
The B7 gate plus its surrounding ritual (fix findings → sync docs → CHANGELOG/`releases.json` → grouped commits → mint → artifacts) is routinely run as one unattended pass. **Activation is owned by the `/akiship` skill and is literal — this section is not a trigger.** The contract below has force only inside a user turn carrying the exact token `/akiship` that asks for the run to be performed; that skill's activation gate is the single source of truth for what counts (`pattern.A1`). Reading this section grants nothing — not its completion-intensity list, and not the fact that a keyword routed this file into context. Outside a valid invocation those words are ordinary vocabulary, and a turn without the token is answered, never executed (`agent.A3`: a question is not a request). This rule predates the skill and stays for the release-domain signals and narrow release context the skill does not carry; on *whether the run may start*, the skill's gate wins. Nothing here weakens [[RULE-agent-behavior]] B3 elsewhere; it exercises B3's "durably authorized" clause, scoped to the enumerated steps of one explicit invocation.
|
|
153
|
+
|
|
154
|
+
- **A valid `/akiship` invocation = standing authorization for every enumerated step.** Explicitly invoking the full run authorizes: fixing gate findings, CHANGELOG/`releases.json` edits, grouped commits (the akigitcommit confirm step is pre-answered — "commit luôn" semantics), the version mint per A4/A5, and tag/GitHub Release strictly per the repo's existing convention (A3, B4). Push and deploy are included only when the invocation names them **or carries a completion-intensity signal** (the same trigger set as the next bullet) — a plain `/akiship` with no intensity marker stays local-only; deploy still additionally requires the stack to auto-deploy on push (owned by the stack rule, not this contract).
|
|
155
|
+
- **Front-load the asks.** Derive B1 state and run B7 step 0 first; every escalation found is reported once, as one batch, and the run stops there. A clean front check means the run completes with zero mid-run questions — an automation that stalls on a question halfway through has failed this rule.
|
|
156
|
+
- **Escalation floor (canonical — `/akiship` references this list, never restates it, `pattern.A1`) — stop only for:** (1) public-history ambiguity — cannot determine whether a version actually shipped, or a `Mismatch`/`Drifted` state whose recovery would rewrite published versions (A5, B1); (2) work the tree cannot classify — mid-edit vs abandoned (B7 step 0); (3) contradiction with documented design, or scope beyond what the invocation named ([[RULE-agent-behavior]] B3).
|
|
157
|
+
- **Completion-intensity phrasing collapses condition (2) and unlocks push/deploy/GitHub-Release, never conditions (1) or (3).** The canonical phrase list — every other file (the `/akiship` skill, `README.md`) points here (`pattern.A1`): "trọn vẹn", "hoàn thành"/"hoàn thiện", "làm/xong hết", "tất cả"/"toàn bộ", or equivalent sentiment insisting the run finish everything, end to end — read **only inside a valid invocation**, where it modifies a run already authorized to start and never creates that authorization, does two things: resolves B7 step 0's mid-edit-vs-abandoned ambiguity toward **mid-edit by default** (finish and integrate the leftover instead of stopping to ask), and satisfies the previous bullet's push/deploy naming requirement, so the run pushes commits and tags, creates the GitHub Release, and runs post-push deploy verification (C5) without a separate mid-run confirmation. Conditions (1) and (3) gate on irreversibility (a published-version rewrite) and correctness (a documented-design contradiction), not on effort, so no phrasing intensity waives them — a "nghiêm trọng"/major-contradiction hit still stops the run.
|
|
158
|
+
- **A question the repo already answers is a violation.** Anything determined by the repo, its docs, these rules, or the invocation itself — bump level (A4), tag or no tag (existing convention), changelog channel and tone (C1) — is self-answered, never asked — and every remaining candidate question runs through `agent.A3`'s kill-tests first. Over-asking inside an authorized run is the same failure as acting unasked (`agent.A3`, `think.B5`).
|
|
159
|
+
- **This licence covers facts the repo determines, never what the owner meant.** A criterion stated in the owner's own words — what "trọn vẹn" must include, which leftovers count as debt and which are future plan — is his to define, and deciding it for him is not self-answering but overwriting the anchor. Report the open items and let him rule on them; when it is the owner's own wording that is ambiguous, that is the one question worth the interrupt.
|
|
160
|
+
|
|
161
|
+
### B9. Registry-published package (npm, crates.io, PyPI, …) — the registry version is the release
|
|
162
|
+
|
|
163
|
+
A package installed from a registry is a distributed artifact (A5): users get what the registry serves, so a tag plus GitHub Release with no registry version leaves `npx`/`pip install` on the old one. Released = tag + GitHub Release + `npm view <pkg>@<version> version` (or the registry's equivalent) returning the new version (`coding.B3`).
|
|
164
|
+
- **The publish mechanism is derived, never designed.** Read the existing convention first: project `CLAUDE.md`, `.github/workflows/`, and sibling packages the same account already publishes (`npm access list packages`) — a working sibling is the template (`coding.B5` rung 2). A CI publish job with a registry token adds a secret and automation: `agent.B3` territory, never the default.
|
|
165
|
+
- **Account facts are probed, not inferred** (`coding.B5` rung 5): `npm whoami` (session), `npm org ls <scope>` (scope ownership — a 404 on the package name means the name is unpublished, never that the scope is unowned), `npm profile get` (2FA mode).
|
|
166
|
+
- **2FA `auth-and-writes` makes `npm publish` the run's single hand-off** (rung 6: the OTP is human-held). Everything else is agent work — push, tag, GitHub Release, tarball verification — so the owner receives one command and the `npm view` check that proves it landed, never a list of prerequisites.
|
|
167
|
+
- **A published version number is burned forever** (`npm unpublish` is time-limited and a number is never reusable), so verify the tarball before publishing: `npm pack --dry-run` against the `files` allowlist, manifest version == CHANGELOG top == tag (A3), and the `bin` executed from the packed tarball installed in the scratchpad. A `bin` that writes to `$HOME` takes the override on its own command — `printf y | HOME="$SANDBOX" bin`, never `HOME="$SANDBOX" printf y | bin`, which scopes the variable to `printf` and runs against the real home.
|
|
168
|
+
|
|
169
|
+
## C. ⟨Aki⟩ Web release artifacts
|
|
170
|
+
|
|
171
|
+
### C1. Two separate channels — do not merge them
|
|
172
|
+
| File | Audience | Language | Tone |
|
|
173
|
+
|------|----------|----------|------|
|
|
174
|
+
| `CHANGELOG.md` | developer / technical | English only | Precise, may name files/symbols. Keep a Changelog format (`Added` / `Changed` / `Fixed` / `Removed`) |
|
|
175
|
+
| `app/data/releases.json` | public / end user | Bilingual EN + VI if the site is multilingual (default EN); EN-only if single-language | Popular, user-friendly, benefit-first. No jargon, no file paths |
|
|
176
|
+
|
|
177
|
+
The changelog explains *what changed and why* for maintainers. The release note tells users *what they get*. Write them separately; do not paste changelog lines into the release note.
|
|
178
|
+
|
|
179
|
+
`releases.json` exists **only where a public web page renders it** (the Nuxt stack's release-notes page). Tauri, CLI, and other non-web projects keep `CHANGELOG.md` only — a release-notes file nothing renders is dead data; do not create one. Where `releases.json` does not exist, every rule below that mentions it simply does not apply.
|
|
180
|
+
|
|
181
|
+
### C2. releases.json schema
|
|
182
|
+
- Single-language site: `{ version, date, title, changes: [{ type, text }] }`
|
|
183
|
+
- Multilingual site: localize the human text — `title: { en, vi }`, `changes: [{ type, text: { en, vi } }]`. Keep `version`, `date`, `type` locale-neutral. Default/fallback language is English.
|
|
184
|
+
- `type` is one of `new` | `improved` | `fixed` | `internal` (stable badge keys).
|
|
185
|
+
|
|
186
|
+
### C3. No version gaps, and no content gaps, in releases.json
|
|
187
|
+
Every version that appears in `CHANGELOG.md` MUST also appear in `releases.json` (no missing version), and every `Added`/`Changed`/`Fixed`/`Removed` section in that version's CHANGELOG entry must be represented by at least one `changes[]` line in `releases.json` (no missing content) — skipping a version, or silently dropping a whole category of its work, because it reads as "internal" or "technical" is not allowed. This page is the one place both a human visitor and a crawling/LLM bot judge whether the product is actively maintained; a version that reads as empty is worse than one that reads as unglamorous.
|
|
188
|
+
|
|
189
|
+
**A version whose work is entirely internal (scripts, refactors, build tooling, admin-only changes) still gets an entry — tagged `"type": "internal"`, never disguised as `improved`/`fixed`.** Correcting existing entries follows this same rule (`release.B3` "never renumber or delete" protects *versions*, not a wrong `type` field within one). Keep the wording honest and generic — describe the capability gained, never a file, table, or route name:
|
|
190
|
+
- `"type": "internal"` — "Under-the-hood improvements for stability and performance, no visible change for users"
|
|
191
|
+
- `"type": "internal"` — "Build and SEO tooling updates (no visible change for users)"
|
|
192
|
+
- `"type": "fixed"` stays for a fix a user would actually notice (URL or display fixes); once the fix is invisible to the user (an internal build warning, an admin-only tool), it is `"type": "internal"` too, not `"fixed"`.
|
|
193
|
+
- A CHANGELOG `Removed` section has no badge of its own: `"type": "improved"` when the user notices the removal (a retired page, a dropped option), `"type": "internal"` when they cannot.
|
|
194
|
+
|
|
195
|
+
Never leave a gap like `1.0.5 → 1.0.7` or `0.1.0 → 0.1.3` in releases.json. A one-line entry is better than a missing version.
|
|
196
|
+
|
|
197
|
+
### C4. Sync check — required before closing a task
|
|
198
|
+
After editing `CHANGELOG.md` or `releases.json`, run:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
grep '"version"' app/data/releases.json
|
|
202
|
+
grep -E '^## \[' CHANGELOG.md
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Confirm every CHANGELOG version has a matching entry in releases.json and the order (newest-first in releases.json, newest-first in CHANGELOG) is consistent. Fix any gap before the task is done.
|
|
206
|
+
|
|
207
|
+
### C5. Live production verification
|
|
208
|
+
Never trust a deployment CLI's success status alone. A web deploy is only verified when the live production URL explicitly returns the new version data.
|
|
209
|
+
Wait ~3 minutes after a successful push/build, then fetch the rendered `/releases/` page and grep the version string — do NOT assume a `releases.json` static endpoint exists (it usually doesn't; `releases.json` is typically bundled into client JS, not served at a public path). The version-number CSS class differs per site (`rl-version`, `release-version`, or none at all), so match the semver text itself, not the class:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
curl -s https://<production-domain>/releases/ | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Confirm the printed version matches what was just released. Verified across the AkiNet ecosystem (8 sites, differing CSS class names, one with no version class at all) — this pattern works regardless of markup since every site renders the version as literal `v{{version}}` text.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# SEO Rule — Nuxt + Cloudflare Stack
|
|
2
|
+
|
|
3
|
+
<!-- Address map: seo.A1-5 · seo.B1-4 · seo.C1-3 (⟨Aki⟩) -->
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
Cross-project rules for all Nuxt 4 + Cloudflare Pages sites. For project-specific keyword strategy and schema values, see the project's own `docs/ref/seo.md` or equivalent.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## A. Meta & structure
|
|
11
|
+
|
|
12
|
+
### A1. Meta title & description
|
|
13
|
+
|
|
14
|
+
- **Title**: ≤ 60 chars total (including the ` | site.name` appended by `@nuxtjs/seo`). Source string must stay short enough to fit within the limit after the suffix is added. Exception: article/post/knowledge slug pages may use ≤ 80 chars when the title is inherently long.
|
|
15
|
+
- **Description**: ≤ 155 chars. Start with an action verb + keyword + benefit
|
|
16
|
+
- **No em/en dashes** (`—` or `–`) inside title or description — causes encoding issues in some SERPs. Use `|` or `-` instead
|
|
17
|
+
- **Single source of truth**: define `const title = '...'` once, pass to `usePageSeo`, OG, Twitter card, and JSON-LD — never repeat the string literal
|
|
18
|
+
|
|
19
|
+
### A2. Schema.org — page type matrix
|
|
20
|
+
|
|
21
|
+
| Page type | Required schemas | Optional |
|
|
22
|
+
|-----------|-----------------|----------|
|
|
23
|
+
| Homepage | `Organization` + `WebSite` + `WebPage` | `FAQPage` |
|
|
24
|
+
| Service / feature page | `Service` + `Organization` | `FAQPage` |
|
|
25
|
+
| Blog post / article | `BlogPosting` + `Person` + `Organization` + `BreadcrumbList` | `FAQPage` |
|
|
26
|
+
| Knowledge / glossary | `Article` + `DefinedTerm` + `DefinedTermSet` | `FAQPage` |
|
|
27
|
+
| Collection / listing | `CollectionPage` + `ItemList` | `BreadcrumbList` |
|
|
28
|
+
| Product | `Product` + `Offer` + `BreadcrumbList` | |
|
|
29
|
+
| About | `AboutPage` + `Organization` + `Person` | |
|
|
30
|
+
| Contact | `ContactPage` + `Organization` | |
|
|
31
|
+
|
|
32
|
+
> ⚠️ **`FAQPage` is inert schema as of 2026 — keep it, but never count it as an SEO deliverable.** Google retired FAQ rich results entirely (display stopped 7 May 2026; Search Console report and Rich Results Test support removed June 2026; API data removed August 2026). Outside Google, **no consumer is confirmed**: Bing's own docs can't be verified for FAQPage, OpenAI's official community thread is contradictory and inconclusive, Perplexity's "structured output" docs describe an API feature and say nothing about crawler parsing, and Claude/Gemini have made no statement. Google's own AI-features guidance says plainly there is *no special schema.org structured data you need to add*.
|
|
33
|
+
>
|
|
34
|
+
> **Rule:** leave existing `FAQPage` markup in place (it is not deprecated at the schema.org level, Google still parses it, and unused structured data causes no harm). But do **not** add an FAQ block to a page merely to emit schema, and do **not** list FAQPage as an SEO task in any plan.
|
|
35
|
+
>
|
|
36
|
+
> What actually earns citations is the **question format in the rendered HTML headings**, not the JSON-LD: passages cited by AI answers are roughly twice as likely to contain a question mark, and 78% of those question marks sit in headings. Write question-shaped `<h2>`s; the schema is a side effect.
|
|
37
|
+
|
|
38
|
+
**Organization — required fields across all projects**
|
|
39
|
+
```ts
|
|
40
|
+
defineOrganization({
|
|
41
|
+
name: 'BrandName',
|
|
42
|
+
alternateName: ['Brand Name', 'brandname', 'BRANDNAME', 'brandname.com'],
|
|
43
|
+
url: 'https://domain.com',
|
|
44
|
+
logo: 'https://domain.com/favicon/icon-192.png',
|
|
45
|
+
sameAs: [/* social profiles, parent org, ecosystem siblings */],
|
|
46
|
+
knowsAbout: [/* domain topics this brand covers */],
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### A3. Trailing slash — SEO-critical for Cloudflare Pages
|
|
51
|
+
|
|
52
|
+
All internal links, canonical tags, `og:url`, sitemap entries, and JSON-LD `url` fields **must end with `/`**. Configured in `nuxt.config.ts`:
|
|
53
|
+
```ts
|
|
54
|
+
site: { trailingSlash: true }
|
|
55
|
+
```
|
|
56
|
+
See `RULE-stack-akiNuxtCf.md` for full config context.
|
|
57
|
+
|
|
58
|
+
### A4. Robots & sitemap
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// nuxt.config.ts
|
|
62
|
+
routeRules: {
|
|
63
|
+
'/admin/**': { robots: false, prerender: false, sitemap: false }
|
|
64
|
+
}
|
|
65
|
+
sitemap: {
|
|
66
|
+
exclude: ['/admin', '/admin/**']
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `/login` defaults to **public and indexable** unless the project's own policy says otherwise
|
|
71
|
+
- Private/admin routes: `robots: false` + excluded from sitemap
|
|
72
|
+
- Public pages: `index, follow, max-image-preview:large, max-snippet:-1` (set in `usePageSeo` if the composable supports it)
|
|
73
|
+
|
|
74
|
+
### A5. OG image
|
|
75
|
+
|
|
76
|
+
- Manual approach — no `nuxt-og-image` (too heavy for static projects)
|
|
77
|
+
- **Size**: 1200×630px
|
|
78
|
+
- **Location**: `public/ogimage/[slug].jpg` or `.png`
|
|
79
|
+
- **Fallback**: if page-level image is absent, the composable falls back to the site-wide default OG image
|
|
80
|
+
- Never reference an OG image path that doesn't exist in `public/`
|
|
81
|
+
|
|
82
|
+
## B. AI visibility & entity
|
|
83
|
+
|
|
84
|
+
### B1. AI / LLM visibility
|
|
85
|
+
|
|
86
|
+
These rules help content appear in AI-generated answers (Perplexity, ChatGPT, Gemini AI Overviews):
|
|
87
|
+
|
|
88
|
+
- **FAQ first sentence**: answer directly (subject + verb + predicate). No "Đây là...", "According to...", "In this article..." preambles
|
|
89
|
+
- **FAQ length**: < 150 words per answer — short enough for AI to extract verbatim
|
|
90
|
+
- **DefinedTerm**: use for specialized domain terminology pages (glossary, knowledge bases)
|
|
91
|
+
- **alternateName in Organization/WebSite schema**: include all brand spelling variants (accented + unaccented + lowercase + domain form) so AI can resolve them to a single entity
|
|
92
|
+
- **knowsAbout**: list the topics the brand covers — helps AI cite the site as a relevant source
|
|
93
|
+
|
|
94
|
+
> ⚠️ **`llms.txt` is not an AI-visibility strategy (2026).** A log study across 137,000 domains found **97% of `llms.txt` files received zero requests over a full month**; no major LLM vendor has committed to reading the format, and Google's John Mueller has compared it to the meta keywords tag — a standard proposed by publishers that no consumer agreed to honour. Keep the file if it already exists (it costs nothing and is genuinely useful for *internal* agents reading the site), but never list it as an SEO/GEO deliverable and never let it substitute for the thing that does work: getting the content into the server-rendered HTML (B4).
|
|
95
|
+
|
|
96
|
+
### B2. Entity & ecosystem linking
|
|
97
|
+
|
|
98
|
+
For sites that belong to a multi-site ecosystem or brand family:
|
|
99
|
+
- `parentOrganization`: link subsidiary sites back to the parent organization's site
|
|
100
|
+
- `sameAs` in `Organization`: include parent org, sibling products, social profiles, and knowledge graph anchors (Wikidata, LinkedIn, etc.)
|
|
101
|
+
- Footer cross-links to ecosystem sibling sites reinforce entity co-occurrence for crawlers
|
|
102
|
+
- `Person` (Founder): link `worksFor` and `sameAs` to founder profiles and project pages
|
|
103
|
+
|
|
104
|
+
Keep the concrete domain list (parent org URL, sibling sites) in the project's own docs — one source of truth per ecosystem, not hardcoded in shared rules.
|
|
105
|
+
|
|
106
|
+
### B3. Vietnamese keyword handling (vi locale)
|
|
107
|
+
|
|
108
|
+
Google treats accented and unaccented Vietnamese as different queries (`vst là gì` ≠ `vst la gi`). To cover both without degrading UX:
|
|
109
|
+
|
|
110
|
+
- **Embed the unaccented form in parentheses** in the first mention of a term in body copy or FAQ: *"...VST (vst la gi)..."*
|
|
111
|
+
- **Or include it in** `keywords` meta or `alternateName` in schema
|
|
112
|
+
- **Never** put unaccented forms in H1, H2, visible headings, or the FAQ question text — it looks unprofessional
|
|
113
|
+
- **Meta title and description**: use correctly accented Vietnamese; unaccented coverage comes from schema + body copy
|
|
114
|
+
|
|
115
|
+
### B4. Prerendering & SSR
|
|
116
|
+
|
|
117
|
+
- SEO-critical content must be in the HTML at crawl time — not injected by client-side JS. **This is the single highest-evidence rule in this file for AI visibility**: roughly 69% of AI crawlers (GPTBot, OAI-SearchBot, ClaudeBot, Claude-SearchBot, PerplexityBot) do **not** execute JavaScript, so a client-only SPA is simply invisible to them. Googlebot and Gemini do render; ChatGPT, Claude and Perplexity do not. Prerendering beats every schema tweak combined.
|
|
118
|
+
- Public pages: prerender/SSG preferred
|
|
119
|
+
- Dynamic SEO content (live prices, user-specific data): SSR, never defer to client
|
|
120
|
+
- `zeroRuntime: true` in sitemap config for static deployments
|
|
121
|
+
|
|
122
|
+
## C. ⟨Aki⟩ API & tooling stack
|
|
123
|
+
|
|
124
|
+
### C1. usePageSeo — standard API
|
|
125
|
+
|
|
126
|
+
Every public page must call `usePageSeo()`. Canonical URL is derived automatically from `route.path` inside the composable — do not pass it manually unless the composable API requires it.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
usePageSeo({
|
|
130
|
+
title: 'Page Topic', // Max 60 chars total — NO brand suffix (see @nuxtjs/seo note below)
|
|
131
|
+
description: 'Action-oriented…', // Max 155 chars, unique per page
|
|
132
|
+
ogImage: 'https://domain.com/ogimage/slug.jpg', // optional
|
|
133
|
+
ogImageAlt: 'Description of image', // optional
|
|
134
|
+
noindex: true, // optional, for admin/private pages
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### C2. @nuxtjs/seo — titleTemplate behavior (CRITICAL)
|
|
139
|
+
|
|
140
|
+
`@nuxtjs/seo` automatically appends ` | site.name` to every title via `titleTemplate`.
|
|
141
|
+
|
|
142
|
+
**Rule: source title must NOT include the brand/site name.**
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// ✅ Correct — module adds " | AkiTao" automatically
|
|
146
|
+
usePageSeo({ title: 'Knowledge Base' })
|
|
147
|
+
// → <title>Knowledge Base | AkiTao</title>
|
|
148
|
+
|
|
149
|
+
// ❌ Wrong — results in double suffix
|
|
150
|
+
usePageSeo({ title: 'Knowledge Base | AkiTao' })
|
|
151
|
+
// → <title>Knowledge Base | AkiTao | AkiTao</title>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This applies to `usePageSeo()`, `useHead({ title })`, and `useSeoMeta({ title })` — all go through `titleTemplate`.
|
|
155
|
+
|
|
156
|
+
### C3. Post-build validation checklist
|
|
157
|
+
|
|
158
|
+
Run `scripts/validate-seo.js` (or equivalent) after every build. At minimum it should verify:
|
|
159
|
+
|
|
160
|
+
- [ ] All page titles ≤ 60 chars (article/post/knowledge slug pages: ≤ 80 chars)
|
|
161
|
+
- [ ] All descriptions ≤ 155 chars
|
|
162
|
+
- [ ] No em dash (`—`) or en dash (`–`) in title or description
|
|
163
|
+
- [ ] All canonical URLs end with `/`
|
|
164
|
+
- [ ] Homepage `Organization` schema has `alternateName` and `sameAs`
|
|
165
|
+
- [ ] `/admin/**` pages absent from sitemap output
|
|
166
|
+
- [ ] Skip redirect stub files (`http-equiv="refresh"`) — they have no SEO content to validate
|
|
167
|
+
|
|
168
|
+
**Validator implementation notes:**
|
|
169
|
+
- Use `>` not `>=` for length checks — exactly 60/155 chars is valid
|
|
170
|
+
- Decode HTML entities before measuring length (`&` = 1 char, not 5)
|
|
171
|
+
- Use `isArticlePage(relPath)` to apply the 80-char limit on article/post/knowledge slug pages
|
|
172
|
+
|
|
173
|
+
If the project doesn't have this script yet, port the validator from an existing project on the same stack as a baseline instead of writing one from scratch.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Stack Rule — Nuxt 4 + Cloudflare
|
|
2
|
+
|
|
3
|
+
<!-- Address map: stack.A1-3 · stack.B1-5 · stack.C1-8 (⟨Aki⟩) -->
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
Nuxt 4 · Vue 3 · Tailwind v4 · @nuxtjs/i18n · @nuxtjs/seo · SweetAlert2 · FontAwesome 7.2 · aki-info-detect
|
|
7
|
+
|
|
8
|
+
## A. Cloudflare & TypeScript foundation
|
|
9
|
+
|
|
10
|
+
### A1. Build & TypeScript
|
|
11
|
+
- Pin `packageManager` (e.g. `npm@10.9.2`) and `engines.node` in `package.json` to match the Cloudflare Pages build image version. Always use `npx npm@<pinned_version> install` to regenerate lockfiles before committing to avoid version drift (especially with optional peer dependencies).
|
|
12
|
+
- TypeScript `strict` mode
|
|
13
|
+
- Vue 3 `<script setup>` + Composition API only — Options API is forbidden
|
|
14
|
+
- Inside `server/`: use relative imports, never `~/server/...`
|
|
15
|
+
- Keep build logs clean — every remaining warning must be either traced to its root cause or filtered on purpose (`nitro.rollupConfig.onwarn`); never let warnings drift unexamined
|
|
16
|
+
- If the build spews sourcemap warnings, two Vite copies are likely loaded — pin one version via package.json `overrides` + clean reinstall
|
|
17
|
+
- Build-date stamp (e.g. a `__BUILD_DATE__` footer global): compute UTC in `nuxt.config.ts`, inject via `vite.define` — never via a shell env var (`VITE_BUILD_DATE=$(date -u ...) nuxt build` in `package.json` is dead code). Display side reads the raw global, formats with local `get*()` (never `getUTC*()`), wrapped in `<ClientOnly>` to avoid an SSR/client hydration mismatch.
|
|
18
|
+
|
|
19
|
+
### A2. Cloudflare Worker runtime constraints
|
|
20
|
+
- No `fs`, `child_process`, or `path` in Worker runtime
|
|
21
|
+
- Use `fetch()` for outbound requests
|
|
22
|
+
- Use `crypto.subtle`, not Node native crypto
|
|
23
|
+
- No `Buffer` here, so everything base64/UTF-8 is hand-rolled — this is exactly where the Unicode pitfalls in RULE-coding (Unicode / UTF-8 safety) bite in production. Concretely on this stack:
|
|
24
|
+
- Decode Firebase/JWT/base64 payloads via `TextDecoder`, never `atob()`+`JSON.parse`, or unicode claims (e.g. a Firebase token's `name`) reach D1 already mojibaked — the DB stores the corrupt bytes faithfully, so this is an app-layer bug, not a DB one.
|
|
25
|
+
- Non-ASCII in a response or `Set-Cookie` header value must be `encodeURIComponent`-ed — HTTP header values are Latin1.
|
|
26
|
+
- Response size / `Content-Length` and any KV/D1 size check are counted in bytes (`new TextEncoder().encode(str).length`), not `str.length`.
|
|
27
|
+
- `crypto.subtle` operates on bytes — feed it `new TextEncoder().encode(str)`, not the string.
|
|
28
|
+
- Do not enable `nodejs_compat` in `wrangler.toml` unless upstream issues are confirmed fixed
|
|
29
|
+
- Trailing slash: `trailingSlash: true` everywhere (routing, canonical, og:url, sitemap, schema.org) — canonical config lives in the i18n section below
|
|
30
|
+
- Never call h3's `readBody()` in a DELETE handler. On workerd, reading a body the runtime never actually attached to a DELETE request hangs the promise instead of rejecting it — the platform then kills the request as a bare 500 with no stack trace. This reproduces on real Cloudflare Pages/Workers but NOT under `nuxt dev` (Node), so it survives local testing and only surfaces in production. Pass the id (or any DELETE payload) via query string on both client and server instead — never body.
|
|
31
|
+
- After the response, keep the isolate alive with `event.context.cloudflare.context.waitUntil` (the path live notify modules use on tachnhac / vstshop / tuvi / kinhdich). Nitro 2.13 also wraps the same CF context as `event.waitUntil`. Do not copy Nitro v3 docs onto this stack: v3 puts the platform on `event.req.runtime.cloudflare`, and `event.req.waitUntil` does not exist on 2.13, so the isolate dies. `nuxt dev` cannot prove isolate lifetime. A 200 that returns before the background work is not evidence the work ran.
|
|
32
|
+
|
|
33
|
+
### A3. Preset and output
|
|
34
|
+
- Use `cloudflare_pages`, not `cloudflare_module`
|
|
35
|
+
- Output directory is `dist/`
|
|
36
|
+
- `wrangler.toml` must keep `pages_build_output_dir = "dist"`
|
|
37
|
+
|
|
38
|
+
## B. Render · i18n · Vue patterns
|
|
39
|
+
|
|
40
|
+
### B1. Rendering split
|
|
41
|
+
- Public pages: prerender/SSG when suitable
|
|
42
|
+
- Dynamic content: SSR
|
|
43
|
+
- Private admin routes: SPA/no-index when suitable
|
|
44
|
+
- NEVER index `/admin` or `/admin/**` in robots/sitemap
|
|
45
|
+
|
|
46
|
+
### B2. Vue/Nuxt patterns
|
|
47
|
+
- SSR guards belong at entry points only — no redundant client guards inside flows that are already client-only
|
|
48
|
+
- Prefer framework composables and runtime APIs over manual plumbing
|
|
49
|
+
- `v-for` must use stable `:key`
|
|
50
|
+
- `v-if` for conditional rendering; `v-show` for frequent toggles
|
|
51
|
+
- Internal links: `NuxtLink`
|
|
52
|
+
- External links: `<a target="_blank" rel="noopener noreferrer">`
|
|
53
|
+
- Dialogs: ONLY use `useSwal()`. Strictly forbidden to use `window.alert()` or `window.confirm()`.
|
|
54
|
+
- Template attribute order: `id` → `v-for :key` → `v-if/show` → `v-model` → `@events` → `:bindings` → `class/static`
|
|
55
|
+
|
|
56
|
+
### B3. i18n
|
|
57
|
+
- Follow the current project's existing locale key convention before introducing a new one
|
|
58
|
+
- For new locale keys, prefer one short, stable convention and keep it consistent within the same project
|
|
59
|
+
- Repeated/shared UI strings: i18n keys
|
|
60
|
+
- Page-specific text belongs co-located with the page — in the `.vue` file's own `<i18n>` block, or in a `.ts` file next to the page. The site-wide `en.json`/`vi.json` files hold ONLY strings shared across the whole site — never stuff page-specific copy into them.
|
|
61
|
+
- Strategy: `prefix_except_default`
|
|
62
|
+
- **`trailingSlash: true` must be set inside the `i18n` block** in addition to `router.options` and `site`. Without it, `localePath()` strips trailing slash from generated URLs, causing canonical mismatch warnings on every page:
|
|
63
|
+
```ts
|
|
64
|
+
i18n: {
|
|
65
|
+
strategy: 'prefix_except_default',
|
|
66
|
+
trailingSlash: true, // ← required; prevents localePath() from stripping slash
|
|
67
|
+
// ...
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### B4. State
|
|
72
|
+
- Prefer Nuxt `useState` first; reach for Pinia only when the state's shape genuinely needs it (cross-page store with actions/getters, not just shared reactive data)
|
|
73
|
+
- Sync any `localStorage`-backed persistence inside `onMounted`, never at setup-time top level — avoids SSR/hydration mismatch
|
|
74
|
+
|
|
75
|
+
### B5. UI baseline
|
|
76
|
+
- Desktop-first, but responsive across narrow to wide screens
|
|
77
|
+
- Scale spacing by breakpoint instead of hardcoding large values
|
|
78
|
+
- Use a scientific z-index system (`--z-index` variables) and standard border-radius dimensions (`radius-sm`, `md`, `lg`, `xl`, `pill`)
|
|
79
|
+
- Use FontAwesome Free. DO NOT write anything for FontAwesome in `.npmrc` (the free version does not need config).
|
|
80
|
+
- Add `aria-label` to icon-only controls
|
|
81
|
+
- Use focus trap for modals when needed
|
|
82
|
+
- Favicon: keep `favicon.ico` at the project's `public/` root
|
|
83
|
+
- Web manifest: full `name`/`short_name`/`icons` (192 + 512 maskable)/`apple-touch-icon`/`theme_color`, linked via `<link rel="manifest">` in `<head>`
|
|
84
|
+
|
|
85
|
+
## C. ⟨Aki⟩ Ecosystem conventions
|
|
86
|
+
|
|
87
|
+
### C1. Canonical component names
|
|
88
|
+
Fixed names for these roles — do not invent new names for them. Each site assembles from this set as needed (top nav is the minimum required; sidebar/rail/dock added when the site needs them). Rename on drift whenever you touch one of these files.
|
|
89
|
+
|
|
90
|
+
| Role | Canonical name |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Footer | `AppFooter.vue` |
|
|
93
|
+
| Top nav | `AppTopNav.vue` |
|
|
94
|
+
| Sidebar (one side) | `AppSidebar.vue` |
|
|
95
|
+
| Sidebar (two sides) | `AppSidebarLeft.vue` / `AppSidebarRight.vue` |
|
|
96
|
+
| Rail / dock (optional) | `AppRail.vue` / `AppDock.vue` |
|
|
97
|
+
| Admin sidebar (admin layout only) | `AdminSidebar.vue` |
|
|
98
|
+
| Breadcrumb | `Breadcrumb.vue` |
|
|
99
|
+
| Auth boundary util | `server/utils/auth.ts` |
|
|
100
|
+
|
|
101
|
+
### C2. Layout chrome — breadcrumb · back-to-home · scroll-to-top
|
|
102
|
+
ONE mechanism for every site on this stack. Do not reinvent it per page; any drift here breaks cross-project consistency.
|
|
103
|
+
|
|
104
|
+
**Breadcrumb — single source of truth**
|
|
105
|
+
- Exactly ONE `<Breadcrumb>`, rendered in `default.vue` inside `<main>` before `<slot/>`. No page renders its own breadcrumb nav.
|
|
106
|
+
- It owns the VISUAL trail only. Derive the trail from `route.path`: strip the non-default locale prefix, map known segments to i18n labels via a lookup table, humanize the rest. Hide it on home (`crumbs.length <= 1`).
|
|
107
|
+
- Dynamic leaf: a detail page supplies the real last-crumb label via `useBreadcrumb(() => label)` (path-keyed `useState`). Render the leaf through `<ClientOnly>` with the humanized segment as the SSR fallback. NEVER read the page-set leaf during the layout's synchronous SSR render — it is not set yet (hydration-mismatch trap).
|
|
108
|
+
- Crumb links point ONLY to real prerendered routes. An intermediate segment with no page of its own renders as plain text, never a link — a dead link makes the Nitro `no-error-response` prerender check fail with a 404.
|
|
109
|
+
- Translated-slug sites (locale-specific slugs for the same page, e.g. `bai-viet`↔`articles`): build links by reconstructing the real path and re-applying the locale prefix (`/en${acc}/`), NOT `localePath(acc)` — `localePath` cannot round-trip an already-localized slug.
|
|
110
|
+
|
|
111
|
+
**BreadcrumbList JSON-LD — owned by the page, never the layout**
|
|
112
|
+
- The `<Breadcrumb>` component emits NO structured data. The `BreadcrumbList` JSON-LD is the page's responsibility (in-page `useHead` / `useSeoSchemas().breadcrumb`, or a page-scoped SEO composable).
|
|
113
|
+
- Exactly ONE `BreadcrumbList` per page. If a SEO composable already emits it, do not add a second copy in the page.
|
|
114
|
+
|
|
115
|
+
**Pre-footer chrome**
|
|
116
|
+
- One `<ScrollToTop>` in the layout. No per-page back-to-top. Back-to-home is the Home crumb — no separate back-to-home button.
|
|
117
|
+
|
|
118
|
+
### C3. Layout width — single source of truth
|
|
119
|
+
The layout (`default.vue`'s outer content wrapper, e.g. `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8`) is the ONLY place page/content width is decided for the whole site.
|
|
120
|
+
|
|
121
|
+
- A page (`app/pages/**/*.vue`) or app/tool page (e.g. a mini-app's `app.vue`) must never put its own `max-w-*` (Tailwind) or a custom CSS `max-width` on its outermost template element. Nesting a second, narrower container inside the layout's wrapper silently shrinks that one route below the site-wide standard and drifts wider over time as different pages pick different values with no functional reason (seen in production: `max-w-3xl` through `max-w-7xl` scattered across routes, plus a scoped CSS container fully disconnected from the layout's width).
|
|
122
|
+
- If a page or app already has its own `max-w-*`/`max-width` wrapper on its outermost element, delete it — it must inherit the layout's width, not redeclare its own.
|
|
123
|
+
- Narrower widths are still fine on an inner **reading-measure or widget** element nested *inside* an already full-width page — a short intro paragraph, a search box, an article's prose column. That is deliberate typography/component sizing, not page layout, and is not what this rule forbids.
|
|
124
|
+
- If a page or app genuinely needs to be full-bleed or a different overall width than the layout's standard, that is a conscious layout decision — it belongs in the layout (or a documented, named per-route exception), never quietly overridden inside the page.
|
|
125
|
+
|
|
126
|
+
### C4. Admin isolation — English-only, `i18n.pages=false`, `localePath` trap
|
|
127
|
+
- **Admin UI is always English-only** — it is an internal SPA tool, not user-facing content, so it does NOT need i18n routing at all. With `@nuxtjs/i18n` `customRoutes: 'config'`, disable each admin page from locale routing via `i18n.pages['admin/xxx'] = false` (not a `{ vi, en }` mapping) — this prevents Nuxt from ever generating a locale-prefixed `/admin/**` variant, so there is only one canonical admin URL. Admin UI copy is hardcoded in English directly, never duplicated via locale ternaries (`isVI ? '...' : '...'`) — that pattern is presentation clutter with no real audience (admin has no locale switcher, and the project's default/public locale is irrelevant here). Domain-specific terms may stay in their original language when no English equivalent is precise — see the project's own domain-terminology exception if it has one.
|
|
128
|
+
- ⚠️ **Once a route has `i18n.pages[x] = false`, link to it with a plain string `to="/..."`, never `localePath()`.** `localePath()` silently returns `undefined` for a route that's been removed from i18n's route map — no error, no console warning — and `<NuxtLink :to="undefined">` renders an `<a>` with no `href` at all, so the link looks correct in code review but never navigates on click. This bites hardest when porting a component between sibling projects: one project may keep a given page (e.g. a `/me` profile page) under normal i18n routing while another disables it — copying the first project's `localePath('/me')` call into the second breaks silently. Always check that project's own `i18n.pages` entry before reusing a `localePath()`/`switchLocalePath()` call from elsewhere.
|
|
129
|
+
- **The admin layout is fully isolated from public UI**: `layouts/admin.vue` has its own chrome — navigation lives in its own `AdminSidebar.vue` — and never imports public chrome components (`AppTopNav`/`AppFooter`/`Breadcrumb`/…) unless there is a clear, recorded benefit. Admin and public UI evolve at different paces; sharing nav/UI couples them so a client change breaks admin and vice versa
|
|
130
|
+
- **Each admin feature area is its own route/page under `/admin/**`** (its own router view) — admin views are feature-dense, so give each area a real URL instead of cramming several areas into one page behind tab state; this keeps each view single-responsibility and code-splits naturally
|
|
131
|
+
- On multi-layout sites (default ↔ admin), any listener/timer/subscription registered while in the admin layout must be cleaned up in `onUnmounted` so it does not leak across a layout switch
|
|
132
|
+
|
|
133
|
+
### C5. Firebase / external integrations — composable boundary
|
|
134
|
+
- **Composable is the only boundary** — page components and layouts never import the provider SDK directly (no `import { getFirestore } from 'firebase/firestore'` in a `.vue` file)
|
|
135
|
+
- All provider-specific code lives in composables or utility modules; pages only call composable functions
|
|
136
|
+
- This means swapping a provider (Firebase → Supabase → D1) only touches the composable layer, not any page
|
|
137
|
+
- **Organize by domain, not by provider** — split into one file per data concern, not one god-file:
|
|
138
|
+
```
|
|
139
|
+
utils/firebase/core.ts ← init app, getDb(), getAuth() only
|
|
140
|
+
composables/useAuth.ts ← login, logout, session state
|
|
141
|
+
composables/useUser.ts ← user profile CRUD
|
|
142
|
+
composables/useProjects.ts ← project data
|
|
143
|
+
```
|
|
144
|
+
- Apply the Result pattern (see RULE-coding.md) at the composable boundary — composables return `Result<T>`, pages check `.ok`
|
|
145
|
+
|
|
146
|
+
### C6. aki-info-detect + AkiTao favicon tool
|
|
147
|
+
- Use [`aki-info-detect`](https://www.npmjs.com/package/aki-info-detect) (npm) to separate bot and real-browser behavior when needed. Import only the specific **named exports** you need — **never** the default `akiInfoDetect()`, which auto-fires `getNetworkInfo()` (ipinfo/ipwhois/ipify) in the background. Do NOT plugin-load the **default/whole** library. Tree-shaken **named local-only** exports (`parseUserAgent`/`getHighEntropyValues`/`getScreen`/`detectGPU`/`getBattery`) MAY run early via a `.client.ts` plugin in an isolated dynamic-import chunk — but verify the network functions (`getIP`/`getISP`/`getCountry`/`getLocation`/`getNetworkInfo`) are absent from the built chunk (`grep` the bundle for the IP URLs). Never auto-run `getIP`/network features unless explicitly requested.
|
|
148
|
+
- Recommended tool: [AkiTao Favicon Generator](https://akitao.com/t/favicon-generator/) — emits the full standard icon set (favicon.ico + PNG sizes + maskable + apple-touch-icon + manifest) in one pass
|
|
149
|
+
|
|
150
|
+
### C7. Dev workflow scripts (package.json)
|
|
151
|
+
Standard utility scripts — fixed names, per-project values (port, DB name):
|
|
152
|
+
|
|
153
|
+
- `killport`: `lsof -t -i:<port> | xargs kill -9 2>/dev/null || true` — each site pins ONE fixed dev port, and the `dev` script always runs `npm run killport && nuxt dev` so a stale process never blocks the port
|
|
154
|
+
- Projects with a D1 database:
|
|
155
|
+
- `db.init.local`: `rm -rf .wrangler/state/v3/d1 && wrangler d1 execute <db-name> --local --file=schema.sql` — wipe local D1 state, reload the schema
|
|
156
|
+
- `db.push`: `wrangler d1 execute <db-name> --remote --file=schema.sql`
|
|
157
|
+
- `db.pull`: `bash scripts/db-pull.sh` — export the remote D1 database to a local backup file; the backup path C8's execution-ownership clause requires before an agent runs a migration itself
|
|
158
|
+
|
|
159
|
+
### C8. Deploy verification — push is not done
|
|
160
|
+
A push only *requests* a Cloudflare build; the task is not closed until the newest build for this project reaches a terminal state. (This is deployment, not releasing — versioning and release artifacts are owned by `RULE-release.md`.)
|
|
161
|
+
|
|
162
|
+
Sites on this stack deploy via **Cloudflare Pages**, not Workers — the `cloudflare-builds` MCP only covers the Workers Builds API and will show zero builds for a Pages project. For Pages, use `wrangler pages deployment list --project-name=<pages-project-name>` (project name may differ from the repo/site name — check with `wrangler pages project list` if unsure) or the general-purpose `cloudflare` MCP (`https://mcp.cloudflare.com/mcp`, covers the full API including Pages). Only use `cloudflare-builds` for a project that is an actual standalone Worker.
|
|
163
|
+
|
|
164
|
+
After every push, watch the newest build/deployment (general `cloudflare` MCP if connected, otherwise `wrangler`), polling about every 30s:
|
|
165
|
+
- **running** → keep waiting. Do not fetch logs.
|
|
166
|
+
- **success** → **CRITICAL:** Do not claim a deploy is successful based on the CLI or Cloudflare dashboard status alone. Verification is ONLY complete when you explicitly fetch the live production URL (e.g. `curl -s -H "Cache-Control: no-cache" https://<production-domain>/releases.json`) ~3 minutes after the push, and confirm the new version or feature code is present in the returned payload. Report "✅ deployed" only after this manual check confirms it.
|
|
167
|
+
- **failed** → fetch the build log, isolate the failing lines, fix the cause in the working tree, and report. Do NOT commit or push the fix — the user decides.
|
|
168
|
+
|
|
169
|
+
**D1 migrations do not run themselves — a green Cloudflare build proves nothing about the database.** A build only compiles/deploys application code; it never executes a `scripts/migrate-*.sql` file. If a task ships a new migration script, closing that task requires, in order:
|
|
170
|
+
1. Run it against the real target: `wrangler d1 execute <db-name> --remote --file=scripts/migrate-*.sql` (never claim done from a `--local` run alone — local and remote are separate SQLite files).
|
|
171
|
+
2. Check the postconditions the migration itself states (row counts, `PRAGMA table_info`) against remote, not assumed from the script having no errors.
|
|
172
|
+
3. Move the file into `scripts/done/` — a migration file left in `scripts/` is itself a visible signal, to the next person or the next session, that step 1 may not have happened.
|
|
173
|
+
|
|
174
|
+
**Execution ownership — the agent runs steps 1-3 itself; this is [[RULE-coding]] B5's ladder, not [[RULE-agent-behavior]] B3's ask-first gate.** An additive, idempotent migration (`CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS` — no `ALTER`/`DROP`, no existing-row mutation) with a backup path available (`db.pull`, C7, or an equivalent remote export) is a two-way door: back up, run `--local` then `--remote`, verify postconditions, move the file — in the same task, without a separate confirmation turn. B5's rung 5 applies first: `wrangler` must be present **and authenticated** (`wrangler whoami`) on this machine; when it is not, the item is a rung-5 hand-off carrying that reason, never an "ask first". Treating "touches production DB" as always-ask by reflex collapses B5's ladder straight to rung 6 and reproduces the forbidden rationalization it names, "only the owner can decide." B3's ask-before gate stays live for the actual irreversible case: any migration that alters or drops existing structure, rewrites existing rows, or has no backup path.
|
|
175
|
+
|
|
176
|
+
See [[RULE-release]] B5 — the CHANGELOG/release entry for this change is not truthful until all three steps above are done, not just written.
|
|
177
|
+
|
|
178
|
+
## SEO
|
|
179
|
+
See `RULE-seo.md` for all SEO rules (meta limits, schema matrix, robots, sitemap, OG image, AI visibility, entity linking).
|