@@ -9,40 +9,65 @@ import {
99} from "node:fs" ;
1010import { tmpdir } from "node:os" ;
1111import { dirname , join , resolve } from "node:path" ;
12- import { describe , expect , it , vi } from "vitest" ;
12+ import { beforeAll , describe , expect , it , vi } from "vitest" ;
1313import { UI_COPY } from "../lib/ui/copy.js" ;
1414
1515const projectRoot = resolve ( process . cwd ( ) ) ;
1616
17- const userDocs = [
18- "docs/index.md" ,
19- "docs/README.md" ,
20- "docs/getting-started.md" ,
21- "docs/faq.md" ,
22- "docs/architecture.md" ,
23- "docs/features.md" ,
24- "docs/configuration.md" ,
25- "docs/troubleshooting.md" ,
26- "docs/privacy.md" ,
27- "docs/upgrade.md" ,
28- "docs/reference/commands.md" ,
29- "docs/reference/public-api.md" ,
30- "docs/reference/error-contracts.md" ,
31- "docs/reference/settings.md" ,
32- "docs/reference/storage-paths.md" ,
33- "docs/releases/v1.1.10.md" ,
34- "docs/releases/v0.1.9.md" ,
35- "docs/releases/v0.1.8.md" ,
36- "docs/releases/v0.1.7.md" ,
37- "docs/releases/v0.1.6.md" ,
38- "docs/releases/v0.1.5.md" ,
39- "docs/releases/v0.1.4.md" ,
40- "docs/releases/v0.1.3.md" ,
41- "docs/releases/v0.1.1.md" ,
42- "docs/releases/v0.1.0.md" ,
43- "docs/releases/v0.1.0-beta.0.md" ,
44- "docs/releases/legacy-pre-0.1-history.md" ,
45- ] ;
17+ function readPackageVersion ( ) : string {
18+ const packagePath = join ( projectRoot , "package.json" ) ;
19+ let parsed : { version ?: unknown } ;
20+ try {
21+ parsed = JSON . parse ( readFileSync ( packagePath , "utf-8" ) ) as {
22+ version ?: unknown ;
23+ } ;
24+ } catch ( error ) {
25+ const message = error instanceof Error ? error . message : String ( error ) ;
26+ throw new Error ( `Failed to read ${ packagePath } : ${ message } ` ) ;
27+ }
28+ if ( typeof parsed . version !== "string" || parsed . version . trim ( ) . length === 0 ) {
29+ throw new Error ( "package.json must define a non-empty version string" ) ;
30+ }
31+ return parsed . version . trim ( ) ;
32+ }
33+
34+ let packageVersion = "" ;
35+ let currentStableReleaseDoc = "" ;
36+ // These stay manual so the docs portal keeps an intentional short stable-history window.
37+ const previousStableReleaseDoc = "docs/releases/v1.2.1.md" ;
38+ const earlierStableReleaseDoc = "docs/releases/v1.2.0.md" ;
39+
40+ function getUserDocs ( ) : string [ ] {
41+ return [
42+ "docs/index.md" ,
43+ "docs/README.md" ,
44+ "docs/getting-started.md" ,
45+ "docs/faq.md" ,
46+ "docs/architecture.md" ,
47+ "docs/features.md" ,
48+ "docs/configuration.md" ,
49+ "docs/troubleshooting.md" ,
50+ "docs/privacy.md" ,
51+ "docs/upgrade.md" ,
52+ "docs/reference/commands.md" ,
53+ "docs/reference/public-api.md" ,
54+ "docs/reference/error-contracts.md" ,
55+ "docs/reference/settings.md" ,
56+ "docs/reference/storage-paths.md" ,
57+ currentStableReleaseDoc ,
58+ previousStableReleaseDoc ,
59+ earlierStableReleaseDoc ,
60+ "docs/releases/v0.1.7.md" ,
61+ "docs/releases/v0.1.6.md" ,
62+ "docs/releases/v0.1.5.md" ,
63+ "docs/releases/v0.1.4.md" ,
64+ "docs/releases/v0.1.3.md" ,
65+ "docs/releases/v0.1.1.md" ,
66+ "docs/releases/v0.1.0.md" ,
67+ "docs/releases/v0.1.0-beta.0.md" ,
68+ "docs/releases/legacy-pre-0.1-history.md" ,
69+ ] ;
70+ }
4671
4772const scopedLegacyAllowedFiles = new Set ( [
4873 "README.md" ,
@@ -65,6 +90,11 @@ const maintainerRunbooks = [
6590 "docs/development/RUNBOOK_CHANGE_ROUTING_POLICY.md" ,
6691] ;
6792
93+ beforeAll ( ( ) => {
94+ packageVersion = readPackageVersion ( ) ;
95+ currentStableReleaseDoc = `docs/releases/v${ packageVersion } .md` ;
96+ } ) ;
97+
6898function read ( filePath : string ) : string {
6999 return readFileSync ( join ( projectRoot , filePath ) , "utf-8" ) ;
70100}
@@ -110,9 +140,9 @@ function compareSemverDescending(left: string, right: string): number {
110140 return 0 ;
111141}
112142
113- describe ( "Documentation Integrity" , ( ) => {
143+ describe ( "Documentation Integrity" , ( ) => {
114144 it ( "has all required user docs and release notes" , ( ) => {
115- for ( const docPath of userDocs ) {
145+ for ( const docPath of getUserDocs ( ) ) {
116146 const fullPath = join ( projectRoot , docPath ) ;
117147 expect ( existsSync ( fullPath ) , `${ docPath } should exist` ) . toBe ( true ) ;
118148 expect (
@@ -122,10 +152,14 @@ describe("Documentation Integrity", () => {
122152 }
123153 } ) ;
124154
125- it ( "docs portal links to stable, beta, and archived release history" , ( ) => {
155+ it ( "docs portal and root README link to stable, beta, and archived release history" , ( ) => {
126156 const portal = read ( "docs/README.md" ) ;
157+ const readme = read ( "README.md" ) ;
127158 expect ( portal ) . toContain ( "reference/public-api.md" ) ;
128159 expect ( portal ) . toContain ( "reference/error-contracts.md" ) ;
160+ expect ( portal ) . toContain ( `releases/v${ packageVersion } .md` ) ;
161+ expect ( portal ) . toContain ( previousStableReleaseDoc . replace ( "docs/" , "" ) ) ;
162+ expect ( portal ) . toContain ( earlierStableReleaseDoc . replace ( "docs/" , "" ) ) ;
129163 expect ( portal ) . toContain ( "releases/v0.1.7.md" ) ;
130164 expect ( portal ) . toContain ( "releases/v0.1.6.md" ) ;
131165 expect ( portal ) . toContain ( "releases/v0.1.5.md" ) ;
@@ -134,6 +168,9 @@ describe("Documentation Integrity", () => {
134168 expect ( portal ) . toContain (
135169 "| [Daily Use release notes](#daily-use) | Stable, previous, and archived release notes |" ,
136170 ) ;
171+ expect ( readme ) . toContain ( currentStableReleaseDoc ) ;
172+ expect ( readme ) . toContain ( previousStableReleaseDoc ) ;
173+ expect ( readme ) . toContain ( earlierStableReleaseDoc ) ;
137174
138175 const beta = read ( "docs/releases/v0.1.0-beta.0.md" ) ;
139176 expect ( beta ) . toContain ( "Archived" ) ;
@@ -158,7 +195,7 @@ describe("Documentation Integrity", () => {
158195 } ) ;
159196
160197 it ( "uses scoped package only in explicit legacy migration notes" , ( ) => {
161- const files = [ "README.md" , ...userDocs ] ;
198+ const files = [ "README.md" , ...getUserDocs ( ) ] ;
162199
163200 for ( const filePath of files ) {
164201 const content = read ( filePath ) ;
@@ -176,7 +213,7 @@ describe("Documentation Integrity", () => {
176213
177214 it ( "does not include opencode wording in user docs" , ( ) => {
178215 const allowedOpencodeFiles = new Set ( [ "docs/reference/storage-paths.md" ] ) ;
179- for ( const filePath of userDocs ) {
216+ for ( const filePath of getUserDocs ( ) ) {
180217 const content = read ( filePath ) . toLowerCase ( ) ;
181218 const hasLegacyHostWord = content . includes ( "opencode" ) ;
182219 if ( hasLegacyHostWord ) {
@@ -189,7 +226,7 @@ describe("Documentation Integrity", () => {
189226 } ) ;
190227
191228 it ( "keeps compatibility command aliases scoped to reference, troubleshooting, or migration docs" , ( ) => {
192- const files = [ "README.md" , ...userDocs ] ;
229+ const files = [ "README.md" , ...getUserDocs ( ) ] ;
193230 const aliasPattern = / \b c o d e x ( m u l t i a u t h | m u l t i - a u t h | m u l t i a u t h ) \b / i;
194231
195232 for ( const filePath of files ) {
0 commit comments