@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +835 -0
  2. package/LICENSE +21 -0
  3. package/README.md +356 -0
  4. package/claude/CLAUDE.md +40 -0
  5. package/claude/agents/aki-challenger.md +38 -0
  6. package/claude/agents/aki-conduct.md +54 -0
  7. package/claude/agents/aki-hands.md +59 -0
  8. package/claude/agents/aki-judge.md +37 -0
  9. package/claude/agents/aki-maker.md +36 -0
  10. package/claude/fragments/settings.akidoc.fragment.json +15 -0
  11. package/claude/hooks/aki-update-check.mjs +160 -0
  12. package/claude/hooks/aki_version_check.mjs +83 -0
  13. package/docs/ref/macos-codesign-tcc.md +59 -0
  14. package/install.mjs +1067 -0
  15. package/install.ps1 +11 -0
  16. package/install.sh +12 -0
  17. package/package.json +52 -0
  18. package/payload/GEMINI.md +147 -0
  19. package/payload/METHOD-audit-flow.md +147 -0
  20. package/payload/METHOD-audit-subtraction.md +67 -0
  21. package/payload/METHOD-audit-zero-trust.md +49 -0
  22. package/payload/METHOD-deep-think.md +172 -0
  23. package/payload/METHOD-proportionality.md +62 -0
  24. package/payload/METHOD-ux-psych.md +60 -0
  25. package/payload/RULE-agent-behavior.md +138 -0
  26. package/payload/RULE-biz.md +51 -0
  27. package/payload/RULE-coding.md +130 -0
  28. package/payload/RULE-content-write.md +54 -0
  29. package/payload/RULE-db-design.md +26 -0
  30. package/payload/RULE-docs.md +144 -0
  31. package/payload/RULE-pattern-core.md +80 -0
  32. package/payload/RULE-release.md +215 -0
  33. package/payload/RULE-seo.md +173 -0
  34. package/payload/RULE-stack-akiNuxtCf.md +179 -0
  35. package/payload/RULE-stack-tauri.md +59 -0
  36. package/payload/RULE-ui-pattern.md +167 -0
  37. package/payload/index.md +91 -0
  38. package/skills/aki-article-writer/SKILL.md +50 -0
  39. package/skills/aki-article-writer/references/article-workflow.md +377 -0
  40. package/skills/akidevsync-notes/SKILL.md +48 -0
  41. package/skills/akidevsync-notes/scripts/notes_cli.py +212 -0
  42. package/skills/akiflow/SKILL.md +221 -0
  43. package/skills/akiflow/references/harness-facts.md +215 -0
  44. package/skills/akiflow/scripts/council-cost.sh +4 -0
  45. package/skills/akiflow/scripts/council-open.sh +4 -0
  46. package/skills/akiflow/scripts/council-read.sh +4 -0
  47. package/skills/akiflow/scripts/council-verify.sh +4 -0
  48. package/skills/akiflow/scripts/council_cost.py +149 -0
  49. package/skills/akiflow/scripts/council_open.py +323 -0
  50. package/skills/akiflow/scripts/council_read.py +148 -0
  51. package/skills/akiflow/scripts/council_verify.py +315 -0
  52. package/skills/akiflow/scripts/scythe.py +307 -0
  53. package/skills/akiflow/scripts/scythe.sh +4 -0
  54. package/skills/akigitcommit/SKILL.md +85 -0
  55. package/skills/akihelp/SKILL.md +47 -0
  56. package/skills/akihtmlreport/SKILL.md +59 -0
  57. package/skills/akilint/SKILL.md +29 -0
  58. package/skills/akirule/SKILL.md +155 -0
  59. package/skills/akiship/SKILL.md +55 -0
  60. 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 (`&amp;` = 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).