feat: add @zenstackhq/plugin-documentation — generate markdown docs from ZModel schemas#2398
feat: add @zenstackhq/plugin-documentation — generate markdown docs from ZModel schemas#2398sheldonj wants to merge 1 commit intozenstackhq:devfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughA new documentation generator plugin for ZenStack is introduced, comprising core plugin logic, multiple renderer modules for different entity types, comprehensive test coverage, schema examples, and generated documentation artifacts demonstrating the plugin's output capabilities. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (21)
packages/plugins/documentation/zenstack/showcase.zmodel (1)
158-160: Overly permissive write policy onProject.
@@allow('create,update', true)grants unrestricted create and update access to all users, including unauthenticated ones. While this is a showcase schema, readers learning from it may copy this pattern into production. Consider scoping it (e.g., to organization members) or adding a comment clarifying this is intentionally permissive for demonstration purposes.The same applies to
Task(lines 196-197).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/zenstack/showcase.zmodel` around lines 158 - 160, The Project and Task models currently use an overly permissive rule @@allow('create,update', true) which grants unrestricted write access; update those policy entries by scoping write permissions to an appropriate check (e.g., organization member or authenticated user) or replace the blanket true with a predicate (for example isMember(user, project) or auth != null), and if you intend to keep this for the demo add an explicit comment above the @@allow('create,update', true) lines stating this is intentionally permissive for showcase only to prevent accidental copy to production; reference the Project and Task model policy blocks and the @@allow('create,update', true) entries when making the change.packages/plugins/documentation/test/integration/multifile.test.ts (1)
9-9: Consider using a definite assignment assertion fortmpDir.
let tmpDir: stringwithout initialization risks a TS2454 "used before assigned" error in stricter tsconfig setups. The idiomatic pattern for test-scoped lifecycle variables is:♻️ Proposed refactor
- let tmpDir: string; + let tmpDir!: string;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/integration/multifile.test.ts` at line 9, The test-scoped variable tmpDir is declared as let tmpDir: string which can trigger TS2454 (used before assigned); change its declaration to use a definite assignment assertion (tmpDir!: string) or initialize it immediately so TypeScript knows it will be set before use, updating references in this test file (the tmpDir binding used in beforeEach/afterEach and tests) accordingly.packages/plugins/documentation/README.md (1)
280-280: Minor: consider strengthening "deeper context".The static analysis tool flags "deeper" as weak wording. Consider "richer context" or "fuller context" for a slightly more precise description.
✏️ Proposed wording
-it can read this single file instead of parsing `.zmodel` schemas. +it can read this single file instead of parsing `.zmodel` schemas for richer context.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/README.md` at line 280, Replace the wording in the README phrase "**Links to full docs** — each entity links to its full documentation page for deeper context" to use stronger phrasing; update "deeper context" to either "richer context" or "fuller context" (e.g., "**Links to full docs** — each entity links to its full documentation page for richer context") so the description reads more precise and avoids the flagged weak wording.packages/plugins/documentation/scripts/preview.ts (1)
43-46: Preferthrowoverprocess.exitinside a loop-called helper.
process.exit(1)insidegenerateTarget()terminates the process immediately, preventing subsequent targets from even being attempted (e.g. useful partial failures are silenced). Throwing lets the top-levelrun().catchhandle exit uniformly, and makes the control flow clearer.♻️ Suggested refactor
if (!r.success) { - console.error(`Failed to load ${target.name} schema:`, r.errors); - process.exit(1); + throw new Error(`Failed to load ${target.name} schema: ${JSON.stringify(r.errors)}`); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/scripts/preview.ts` around lines 43 - 46, Inside generateTarget(), replace the immediate process.exit(1) in the error branch with throwing an Error so the top-level run().catch can handle termination; specifically, in the block that currently logs and calls process.exit(1) (the if (!r.success) branch), throw a new Error that includes the descriptive message and r.errors (e.g., `throw new Error(\`Failed to load ${target.name} schema: ${JSON.stringify(r.errors)}\`)`) so generateTarget() fails upward instead of killing the process.packages/plugins/documentation/CONTRIBUTING.md (1)
122-122: Minor grammar nit: hyphenate the compound adjective.
full page content→full-page contentwhen used as a compound adjective modifying a noun.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/CONTRIBUTING.md` at line 122, Update the documentation phrase "Join with `'\n'` and return the full page content" to hyphenate the compound adjective — change "full page content" to "full-page content" (look for the exact line containing "Join with `'\n'` and return the full page content" in CONTRIBUTING.md).packages/plugins/documentation/zenstack/verbose/content.zmodel (1)
170-178:AuditLogdeniescreate— audit entries cannot be created through the ORM access layer.Line 172 denies
create,update,delete, which means audit log entries can only be inserted by bypassing ZenStack's access control (e.g., using Prisma directly or an unchecked client). This is fine for an immutable audit log pattern, but if this schema is also used as a reference example for users, it may be worth adding a brief comment (e.g.,// entries created via unchecked Prisma client) to clarify the intended creation path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/zenstack/verbose/content.zmodel` around lines 170 - 178, The @@deny('create,update,delete', true) rule on the AuditLog model prevents creating audit entries via the ORM; add a short inline comment adjacent to that rule (next to @@deny('create,update,delete', true) or above the AuditLog model) clarifying the intended insertion path (e.g., "entries created via unchecked Prisma client or bypassing ZenStack access control") so readers know creation must occur outside the normal ORM access layer.packages/plugins/documentation/test/generator/snapshot.test.ts (1)
14-65: Consider cleaning up the temporary directory after the test.
generateFromSchemacreates a temp directory that isn't cleaned up. While the OS eventually reclaims/tmpspace, explicitly removing it in anafterEach/afterAllor at test end prevents accumulation during CI runs.♻️ Suggested cleanup
+import fs from 'fs'; + describe('documentation plugin: snapshot', () => { it('snapshot: full representative schema output', async () => { const tmpDir = await generateFromSchema(` ... `); const indexContent = stabilize(readDoc(tmpDir, 'index.md')); ... expect(relDoc).toMatchSnapshot('relationships.md'); + + fs.rmSync(tmpDir, { recursive: true, force: true }); }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/generator/snapshot.test.ts` around lines 14 - 65, The test creates a temporary directory via generateFromSchema and assigns it to tmpDir but never cleans it up; move tmpDir to an outer scope (let tmpDir: string) or have generateFromSchema return a disposable, then add an afterEach or afterAll hook that removes the directory (use fs.promises.rm or fs.rmSync with recursive/force) to delete tmpDir after the test finishes; ensure the cleanup runs regardless of test outcome so readDoc/stabilize assertions still work before removal.packages/plugins/documentation/test/generator/enum-page.test.ts (1)
40-57: MissingafterAllcleanup for the nested describe'stmpDir.The
beforeAllat line 42 creates a temp directory but there is noafterAll(() => fs.rmSync(tmpDir, { recursive: true, force: true }))to pair with it, unlike the cleanup pattern used intype-view-page.test.ts. The OS will eventually reclaim these directories, but in high-frequency CI environments this can accumulate.🧹 Proposed fix
describe('enum with usage by models', () => { let tmpDir: string; beforeAll(async () => { tmpDir = await generateFromSchema(`...`); }); + afterAll(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/generator/enum-page.test.ts` around lines 40 - 57, The nested describe 'enum with usage by models' creates a temp dir in its beforeAll using the tmpDir variable but lacks a corresponding cleanup; add an afterAll that calls fs.rmSync(tmpDir, { recursive: true, force: true }) (or equivalent async removal) to remove the generated temp directory after the tests finish, pairing the teardown with the existing beforeAll in that describe block.packages/plugins/documentation/test/generator/type-view-page.test.ts (1)
62-63: Overly permissive link assertions (missing closing)).expect(typeDoc).toContain('[Post](../models/Post.md'); expect(typeDoc).toContain('[User](../models/User.md');The intentional omission of the closing
)means these assertions would also pass for a malformed link like[Post](../models/Post.md-broken. If the goal is to allow any anchor suffix, a.toMatch(/\[Post\]\(\.\.\/models\/Post\.md[^)]*\)/)regex would give the same flexibility with a closed, well-formed link.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/generator/type-view-page.test.ts` around lines 62 - 63, The assertions in type-view-page.test.ts are too permissive because they check typeDoc for '[Post](../models/Post.md' and '[User](../models/User.md' without the closing ')' — update the two expectations that reference the typeDoc variable (currently using toContain) to either include the trailing ')' so they assert a well-formed link, or replace them with regex assertions (e.g., use toMatch with /\[Post\]\(\.\.\/models\/Post\.md[^)]*\)/ and similarly for User) to allow optional anchor suffixes while ensuring the link is closed; adjust the two expect lines that reference '[Post]' and '[User]' accordingly.packages/plugins/documentation/src/renderers/type-page.ts (1)
22-35:anchoris loop-invariant but computed on every iteration.
const anchor = firstField ? \#field-${firstField}` : '';at line 28 depends only ontypeDef.fields[0]`, which doesn't change between iterations. Hoisting it before the loop removes the redundant per-iteration evaluation.♻️ Proposed refactor
function renderUsedBySection(typeDef: TypeDef, usedBy: DataModel[]): string[] { if (usedBy.length === 0) return []; const firstField = typeDef.fields[0]?.name; + const anchor = firstField ? `#field-${firstField}` : ''; const lines = [...sectionHeading('Used By'), '']; for (const m of usedBy) { const dir = m.isView ? 'views' : 'models'; - const anchor = firstField ? `#field-${firstField}` : ''; const fieldLinks = typeDef.fields🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/renderers/type-page.ts` around lines 22 - 35, In renderUsedBySection, the anchor variable is loop-invariant but recomputed each iteration; hoist its calculation out of the for loop by computing const anchor = firstField ? `#field-${firstField}` : '' before the for (const m of usedBy) loop (keep firstField derived from typeDef.fields[0]?.name), then use that anchor inside the loop when building the per-model link; no other logic changes to renderUsedBySection, fieldLinks, or dir computation are needed.packages/plugins/documentation/test/generator/model-page.test.ts (1)
1-6:afterAllis imported but never used.Line 4 imports
afterAllfrom vitest but there is noafterAllblock anywhere in this file. Remove it to keep imports clean.♻️ Proposed fix
-import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { beforeAll, describe, expect, it } from 'vitest';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/generator/model-page.test.ts` around lines 1 - 6, The import list in model-page.test.ts includes an unused symbol afterAll from vitest; remove afterAll from the import statement (the line importing afterAll, beforeAll, describe, expect, it) so the file only imports the actually used helpers (beforeAll, describe, expect, it) to clean up unused imports.packages/plugins/documentation/src/generator.ts (1)
166-173:filesGenerated++counted beforeindex.mdis written — timing is intentional but subtle.The increment on line 166 and
genCtx.filesGenerated = filesGeneratedon line 168 both happen beforewriteFile(index.md)on line 170, so the count embedded insideindex.mdalready includes itself. This is the correct design (the stat shows the final total), but a brief comment would prevent future confusion.💡 Suggested comment
+ // Count index.md before rendering it so the stats it embeds reflect the final total. filesGenerated++;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/generator.ts` around lines 166 - 173, The increment of filesGenerated and assignment genCtx.filesGenerated occur before calling writeFile for index.md, intentionally so the generated index embeds the final total including itself; add a concise inline comment above filesGenerated++ (near filesGenerated, genCtx.durationMs, genCtx.filesGenerated and the subsequent writeFile call that renders index.md via renderIndexPage) stating that the order is deliberate and that the index page should reflect the final filesGenerated count to avoid confusion for future maintainers.packages/plugins/documentation/test/integration/samples.test.ts (1)
8-41: Double schema generation — usebeforeAllto sharetmpDir.Both
itblocks in eachdescribecallgenerateFromFileindependently, meaning the schema is loaded and docs are generated twice per suite. Extract tobeforeAllto generate once.♻️ Suggested refactor (shared schema suite shown; same pattern applies to orm suite)
+import { beforeAll, describe, expect, it } from 'vitest'; -import { describe, expect, it } from 'vitest'; describe('integration: samples/shared schema', () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = await generateFromFile(path.join(SAMPLES_DIR, 'shared', 'schema.zmodel')); + }); + it('generates complete docs from real User/Post schema', async () => { - const tmpDir = await generateFromFile(path.join(SAMPLES_DIR, 'shared', 'schema.zmodel')); // ...assertions unchanged }); it('has zero broken links', async () => { - const tmpDir = await generateFromFile(path.join(SAMPLES_DIR, 'shared', 'schema.zmodel')); expect(findBrokenLinks(tmpDir)).toEqual([]); }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/integration/samples.test.ts` around lines 8 - 41, Move the repeated generateFromFile call into a beforeAll hook so the suite generates docs once and shares the result: declare a suite-scoped variable tmpDir in the describe block, call generateFromFile(SAMPLES_DIR + 'shared/schema.zmodel') inside beforeAll and assign to tmpDir, then update both it blocks to use that tmpDir (for readDoc and findBrokenLinks) instead of each calling generateFromFile again; keep references to generateFromFile, beforeAll, tmpDir, readDoc, and findBrokenLinks when making the change.packages/plugins/documentation/src/renderers/index-page.ts (1)
133-144:renderProceduresSectionskipsformatIndexEntry— deprecated procedures won't render with strikethrough.Every other section (models, views, types, enums) passes through
formatIndexEntry, which handlesmeta.deprecatedstrikethrough.renderProceduresSectionhand-builds its own format string. If procedures ever carry adoc:deprecatedmeta attribute, this section will silently omit the deprecation marker.♻️ Suggested alignment with other sections
function renderProceduresSection(data: IndexData): string[] { if (data.procedures.length === 0) return []; const lines = ['<a id="procedures"></a>', '', '## ⚡ Procedures', '']; for (const proc of data.procedures) { const kind = proc.mutation ? 'mutation' : 'query'; const desc = firstSentence(extractProcedureComments(proc, ' ')); - const descSuffix = desc ? ` — ${desc}` : ''; - lines.push(`- [${proc.name}](./procedures/${proc.name}.md) · *${kind}*${descSuffix}`); + // Procedures don't support @@meta today; pass empty meta so the + // suffix renders via formatIndexEntry when that changes. + const entry = formatIndexEntry(proc.name, `./procedures/${proc.name}.md`, desc, {}); + lines.push(`${entry} · *${kind}*`); } lines.push(''); return lines; }If you intentionally want to keep the
· *kind*suffix before the description, the above will need a slight adjustment — but the key point is routing throughformatIndexEntryfor future-proofing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/renderers/index-page.ts` around lines 133 - 144, renderProceduresSection manually builds its list entries and never uses formatIndexEntry, so procedures with meta.deprecated won't get strikethrough; update renderProceduresSection to call formatIndexEntry(proc, `[${proc.name}](./procedures/${proc.name}.md)`) (or equivalent) to produce the base formatted label, then append the kind suffix (use proc.mutation ? 'mutation' : 'query') and the description suffix (from firstSentence(extractProcedureComments(proc,' '))) so the final list item contains the formatted entry, the · *kind* marker, and the descSuffix; keep the existing link text and placement but route through formatIndexEntry to preserve deprecation handling.packages/plugins/documentation/src/renderers/common.ts (1)
188-205:renderSimpleFieldsTableuses unlinked types — intentional but limits cross-navigation from type/view pages.
getFieldTypeName(field, false)renders type names without links. Fields referencing enums or other types in type/view pages won't have clickable cross-links in their fields table. If cross-linking is desired for type/view fields too, this would needlinked: trueand adjusted relative paths.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/renderers/common.ts` around lines 188 - 205, renderSimpleFieldsTable currently calls getFieldTypeName(field, false) which intentionally returns unlinked type names; to enable cross-navigation from type/view pages change that call to request linked names (e.g., getFieldTypeName(field, true)) and ensure the surrounding renderer passes/derives any needed relative path context so links resolve correctly. Update renderSimpleFieldsTable to pass linked=true to getFieldTypeName and, if your getFieldTypeName signature or link generation needs a base path or page-type context, propagate that information from the caller so enum/custom type names are rendered as clickable links.packages/plugins/documentation/src/types.ts (1)
67-67: Import placed mid-file rather than at the top.The
import type { DataModel, Enum, Model, Procedure, TypeDef }on line 67 is placed after several interface definitions. While TypeScript hoists imports, conventional style places all imports at the file's top. This may cause lint warnings depending on your ESLint config.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/types.ts` at line 67, Move the stray "import type { DataModel, Enum, Model, Procedure, TypeDef } from '@zenstackhq/language/ast'" to the top of the file above all interface/type definitions so all imports are declared first; locate the import line and the nearby interfaces (e.g., any interface/type declarations that currently precede that import) and cut-and-paste the import to the file header, then run the linter/tsc to ensure there are no unused-import or ordering issues and adjust any other imports if necessary.packages/plugins/documentation/src/renderers/skill-page.ts (1)
531-532:schemafromSkillPagePropsis unused inrenderSkillPage.
SkillPagePropsincludesschema: Model, butrenderSkillPagedestructures only{ title, models, views, enums, typeDefs, procedures, hasRelationships }—schemais never accessed. If it's not needed, consider removing it from the interface to keep the contract clean.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/renderers/skill-page.ts` around lines 531 - 532, The SkillPageProps type includes a schema property that isn’t used in renderSkillPage; either remove schema from the SkillPageProps interface to keep the contract clean, or if schema is intended to be used, add schema to the destructuring in renderSkillPage (const { title, models, views, enums, typeDefs, procedures, hasRelationships, schema } = props) and use it where appropriate in the function (or pass it to helpers that need it); update the SkillPageProps declaration accordingly so the type and the renderSkillPage implementation stay consistent.packages/plugins/documentation/test/utils.ts (3)
42-71: Temp directories are never cleaned up after tests.Both
generateFromSchemaandgenerateFromFilecreate temp directories viafs.mkdtempSyncbut never remove them. While OS temp cleanup handles this eventually, consider adding anafterAllorafterEachcleanup in tests or returning a disposable handle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/utils.ts` around lines 42 - 71, generateFromSchema and generateFromFile create temp directories with fs.mkdtempSync but never remove them; modify these helpers to return a disposable handle (e.g., { tmpDir, cleanup: async () => rm(tmpDir, { recursive: true }) }) or collect created tmpDirs in a shared array and expose/implement a cleanup function that tests call from afterEach/afterAll to remove them; update callers/tests to invoke the cleanup to ensure temp dirs are removed.
86-113:findBrokenLinksvalidates file existence but not anchor fragments.The utility splits
hrefon#and only checks if the file part exists. If a link targets./index.md#nonexistent-anchor, it will pass validation even if the anchor doesn't exist in the file. This is a reasonable scope limitation for now, but worth noting for future enhancement.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/utils.ts` around lines 86 - 113, findBrokenLinks currently only checks that the target file exists (uses href, filePart, resolved) but ignores anchor fragments after “#”, so links like ./index.md#nonexistent-anchor are treated as valid; update findBrokenLinks to extract the fragment portion from href (the part after ‘#’), and when a fragment exists read the resolved file contents and verify the anchor is present by checking for either an explicit HTML anchor (<a name="..."> or id="...">) or a generated heading id (convert markdown headings to their slug form or search for the heading text), and if the anchor is missing push the same broken record (source, link, target) into broken; keep the existing file-exists check and only perform the fragment validation when a fragment is present.
19-30: Add explicitcryptoimport for consistency with other node module imports.Line 21 uses
crypto.randomUUID()without importingcrypto. While this works on Node.js 18+ (tested versions use 22.x), add an explicit import to match the style of other imports in the file that use thenode:prefix.🔧 Proposed fix
import { invariant } from '@zenstackhq/common-helpers'; import { loadDocument } from '@zenstackhq/language'; +import { randomUUID } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';- const tempFile = path.join(os.tmpdir(), `zenstack-schema-${crypto.randomUUID()}.zmodel`); + const tempFile = path.join(os.tmpdir(), `zenstack-schema-${randomUUID()}.zmodel`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/utils.ts` around lines 19 - 30, The loadSchema function uses crypto.randomUUID() but the file lacks an explicit import; add an explicit node-style import for crypto (e.g., import crypto from 'node:crypto') at the top of the module so loadSchema can call crypto.randomUUID() consistently with other node imports and avoid relying on global availability.packages/plugins/documentation/test/integration/showcase.test.ts (1)
9-10: Consider generating the showcase docs once inbeforeAllto speed up the test suite.Each of the 17+ tests independently calls
generateFromFile(SHOWCASE_SCHEMA), re-parsing and regenerating all docs from scratch. Since the showcase schema and default options are the same for most tests, a sharedbeforeAll+tmpDirvariable would dramatically reduce test execution time while maintaining the same assertions.Tests that use non-default options (
generateSkill: true,includeInternalModels: true) would still need their own generation call.♻️ Sketch of the refactor
describe('integration: showcase schema', () => { + let tmpDir: string; + + beforeAll(async () => { + tmpDir = await generateFromFile(SHOWCASE_SCHEMA); + }); + it('generates expected file structure with correct model and enum counts', async () => { - const tmpDir = await generateFromFile(SHOWCASE_SCHEMA); expect(fs.existsSync(path.join(tmpDir, 'index.md'))).toBe(true); // ... rest of assertions }); // ... similarly for other tests that use default options + // Tests with non-default options remain as-is: it('generates SKILL.md with comprehensive content when generateSkill=true', async () => { const tmpDir = await generateFromFile(SHOWCASE_SCHEMA, { generateSkill: true }); // ... });Also applies to: 43-44, 49-50, 74-75, 89-91, 112-114, 148-150, 180-182, 195-197, 214-216, 230-232, 239-241, 255-257, 279-281, 293-295, 303-305, 324-326, 355-357, 367-369, 381-383, 391-393, 476-478, 481-483
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/test/integration/showcase.test.ts` around lines 9 - 10, Move the repeated generateFromFile(SHOWCASE_SCHEMA) calls into a shared beforeAll that assigns the result to a top-level tmpDir variable so all tests reuse the same generated docs; update tests to use that tmpDir instead of calling generateFromFile themselves, but keep per-test generateFromFile calls only for cases that need non-default options (e.g., tests that pass generateSkill: true or includeInternalModels: true). Locate usages of generateFromFile and SHOWCASE_SCHEMA in the suite and replace duplicates with the shared tmpDir, and add a beforeAll block (using the same test framework helpers) that sets tmpDir by calling generateFromFile(SHOWCASE_SCHEMA) once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/plugins/documentation/package.json`:
- Line 9: The lint script in package.json ("lint": "eslint src --ext ts") should
drop the unsupported --ext ts flag; update the "lint" script value to just run
eslint on the src directory (e.g., "eslint src") so ESLint v9 flat config and
the typescript handling from eslint.config.js (tseslint.configs.recommended) are
used correctly.
In `@packages/plugins/documentation/src/extractors.ts`:
- Around line 83-109: The current resolveTypeLink wraps only the base scalar (in
the t.type branch) before appending array/optional modifiers, causing outputs
like `String`[]; update resolveTypeLink so modifiers (t.array, t.optional) are
appended to the base name first and then the entire scalar type (including
modifiers) is wrapped in backticks—mirroring getFieldTypeName; keep the
linked-reference branches (isDataModel/isEnum/isTypeDef and t.reference.ref
paths) unchanged but ensure any non-linked scalar path uses the delayed wrapping
after modifiers are applied.
In `@packages/plugins/documentation/src/generator.ts`:
- Around line 125-138: renderEnumPage is being called with only allModels:
models, so any views that reference an enum won't appear in the "Used By"
cross-reference; update the call that writes enum files (inside the enums loop
where enumDecl is iterated) to pass both models and views (the same combined
array used when calling renderTypePage: allModels: [...models, ...views]) so
renderEnumPage receives views too and can list referencing views.
In `@packages/plugins/documentation/src/renderers/enum-page.ts`:
- Around line 7-27: EnumUsage must include whether the consumer is a view so
links are generated correctly: update the EnumUsage interface to add isView:
boolean and modify collectEnumUsage to populate isView from each DataModel (use
m.isView) when pushing { modelName, fieldNames, isView } for consumers; then
update renderUsedBySection to read usage.isView and choose the correct directory
('views' vs 'models') when building the link. Ensure sorting still uses
modelName and adjust types/usages accordingly.
In `@packages/plugins/documentation/src/renderers/model-page.ts`:
- Around line 215-222: The current heuristic in model-page.ts (setting relType
based only on field.type.array and field.type.optional) mislabels mandatory
one-to-one relations as "Many→One"; update the logic to detect one-to-one by
checking the relation/field metadata for uniqueness (e.g., presence of a unique
attribute on the FK or relation descriptor) and set relType = 'One\u2192One'
when that uniqueness is present; apply the same fix where the heuristic is
duplicated (extractors.ts -> collectRelationships) and ensure you reference the
RelationType enum/alias so both model-page.ts and collectRelationships produce
"One\u2192One" for unique non-array relations.
In `@packages/plugins/documentation/src/renderers/skill-page.ts`:
- Around line 130-147: detectIdConvention currently only scans field-level `@id`
attributes and misses models that declare composite primary keys with @@id or
otherwise have no field-level `@id`, skewing the counts; update detectIdConvention
to also inspect model-level attributes (e.g., scan each DataModel for an @@id
attribute or a model.primaryKey-like property) and treat those models as a
distinct bucket (e.g., "composite") when incrementing the defaults map, keep the
existing handling for fields where you extract `@default` values, and ensure
models with no `@id` at all are counted as "none" so the final comparison (count
=== models.length) and the generated messages reflect composite/absent IDs
correctly.
In `@packages/plugins/documentation/test/generator/model-page.test.ts`:
- Around line 591-609: Import the randomUUID function from 'node:crypto' and
wrap the temp file/dir creation in a try/finally so they are removed after the
test; specifically, add "import { randomUUID } from 'node:crypto'" and replace
uses of crypto.randomUUID() with randomUUID(), and ensure tmpSchemaFile and
tmpDir (created in the test for model with @@schema) are cleaned up in finally
via fs.unlinkSync/fs.rmSync (or fs.rm with recursive for tmpDir) after
loadSchemaFromFile and plugin.generate complete.
In `@packages/plugins/documentation/test/integration/e2e-schemas.test.ts`:
- Line 6: E2E_SCHEMAS_DIR is using a brittle five-level relative path; change it
to resolve from the monorepo root (e.g. using
require.resolve('tests/e2e/orm/schemas') or path.join(process.cwd(),
'tests/e2e/orm/schemas')) and replace the current E2E_SCHEMAS_DIR assignment
with that root-resolved value, then add an explicit existence check
(fs.existsSync or assert) right after the assignment that throws/fails the test
if the directory is missing so failures are immediate and obvious.
In `@packages/plugins/documentation/tsconfig.json`:
- Line 3: The current tsconfig ("include": ["src/**/*"]) omits test files so tsc
--noEmit skips tests; add a test-aware configuration by creating a
tsconfig.test.json that extends the existing tsconfig and sets "include" to
["src/**/*", "test/**/*"] (or alternatively update the existing tsconfig include
to add "test/**/*") and ensure Vitest (e.g., vitest.config.ts) is pointed at
tsconfig.test.json or configured to run type-checking so test files are covered
by tsc; reference the existing packages/plugins/documentation/tsconfig.json
include setting and the Vitest config entry when making the change.
In `@packages/plugins/documentation/zenstack/showcase.zmodel`:
- Around line 79-99: The schema currently declares the email field with a
field-level `@unique` and also defines a compound @@unique([email,
organizationId]), which is redundant; decide the intended constraint and update
accordingly: if emails should be unique only within an organization
(multi-tenant), remove the field-level `@unique` from the email field (symbol:
email) and keep the compound @@unique([email, organizationId]); if emails must
be globally unique, remove the compound @@unique([email, organizationId])
instead; alternatively, if both were left intentionally for documentation, add a
brief clarifying comment near the email field explaining the dual constraints
for readers.
In `@packages/plugins/documentation/zenstack/verbose/billing.zmodel`:
- Around line 84-99: The doc comment for paymentAttempts claims "Capped at 4
retries" but the @@validate currently allows paymentAttempts <= 10; update the
validation to match the comment by changing the @@validate condition for
paymentAttempts to require paymentAttempts >= 0 && paymentAttempts <= 4 (adjust
the error message if needed) so the paymentAttempts field and its validation are
consistent with the comment; locate the validation on the Invoice model (the
@@validate that references paymentAttempts) and make this change.
In `@packages/plugins/documentation/zenstack/verbose/notifications.zmodel`:
- Around line 56-70: NotificationPreference.userId lacks a Prisma relation and a
back-reference on User; add an explicit relation by keeping userId String and
adding a corresponding user User field on NotificationPreference with
`@relation`(name: "UserNotificationPreferences", fields: [userId], references:
[id]) (or equivalent relation attributes used elsewhere), and then add the
matching notificationPreferences NotificationPreference[]
`@relation`("UserNotificationPreferences") back-reference to the User model in
identity.zmodel so a foreign key and navigation properties are generated.
---
Nitpick comments:
In `@packages/plugins/documentation/CONTRIBUTING.md`:
- Line 122: Update the documentation phrase "Join with `'\n'` and return the
full page content" to hyphenate the compound adjective — change "full page
content" to "full-page content" (look for the exact line containing "Join with
`'\n'` and return the full page content" in CONTRIBUTING.md).
In `@packages/plugins/documentation/README.md`:
- Line 280: Replace the wording in the README phrase "**Links to full docs** —
each entity links to its full documentation page for deeper context" to use
stronger phrasing; update "deeper context" to either "richer context" or "fuller
context" (e.g., "**Links to full docs** — each entity links to its full
documentation page for richer context") so the description reads more precise
and avoids the flagged weak wording.
In `@packages/plugins/documentation/scripts/preview.ts`:
- Around line 43-46: Inside generateTarget(), replace the immediate
process.exit(1) in the error branch with throwing an Error so the top-level
run().catch can handle termination; specifically, in the block that currently
logs and calls process.exit(1) (the if (!r.success) branch), throw a new Error
that includes the descriptive message and r.errors (e.g., `throw new
Error(\`Failed to load ${target.name} schema: ${JSON.stringify(r.errors)}\`)`)
so generateTarget() fails upward instead of killing the process.
In `@packages/plugins/documentation/src/generator.ts`:
- Around line 166-173: The increment of filesGenerated and assignment
genCtx.filesGenerated occur before calling writeFile for index.md, intentionally
so the generated index embeds the final total including itself; add a concise
inline comment above filesGenerated++ (near filesGenerated, genCtx.durationMs,
genCtx.filesGenerated and the subsequent writeFile call that renders index.md
via renderIndexPage) stating that the order is deliberate and that the index
page should reflect the final filesGenerated count to avoid confusion for future
maintainers.
In `@packages/plugins/documentation/src/renderers/common.ts`:
- Around line 188-205: renderSimpleFieldsTable currently calls
getFieldTypeName(field, false) which intentionally returns unlinked type names;
to enable cross-navigation from type/view pages change that call to request
linked names (e.g., getFieldTypeName(field, true)) and ensure the surrounding
renderer passes/derives any needed relative path context so links resolve
correctly. Update renderSimpleFieldsTable to pass linked=true to
getFieldTypeName and, if your getFieldTypeName signature or link generation
needs a base path or page-type context, propagate that information from the
caller so enum/custom type names are rendered as clickable links.
In `@packages/plugins/documentation/src/renderers/index-page.ts`:
- Around line 133-144: renderProceduresSection manually builds its list entries
and never uses formatIndexEntry, so procedures with meta.deprecated won't get
strikethrough; update renderProceduresSection to call formatIndexEntry(proc,
`[${proc.name}](./procedures/${proc.name}.md)`) (or equivalent) to produce the
base formatted label, then append the kind suffix (use proc.mutation ?
'mutation' : 'query') and the description suffix (from
firstSentence(extractProcedureComments(proc,' '))) so the final list item
contains the formatted entry, the · *kind* marker, and the descSuffix; keep the
existing link text and placement but route through formatIndexEntry to preserve
deprecation handling.
In `@packages/plugins/documentation/src/renderers/skill-page.ts`:
- Around line 531-532: The SkillPageProps type includes a schema property that
isn’t used in renderSkillPage; either remove schema from the SkillPageProps
interface to keep the contract clean, or if schema is intended to be used, add
schema to the destructuring in renderSkillPage (const { title, models, views,
enums, typeDefs, procedures, hasRelationships, schema } = props) and use it
where appropriate in the function (or pass it to helpers that need it); update
the SkillPageProps declaration accordingly so the type and the renderSkillPage
implementation stay consistent.
In `@packages/plugins/documentation/src/renderers/type-page.ts`:
- Around line 22-35: In renderUsedBySection, the anchor variable is
loop-invariant but recomputed each iteration; hoist its calculation out of the
for loop by computing const anchor = firstField ? `#field-${firstField}` : ''
before the for (const m of usedBy) loop (keep firstField derived from
typeDef.fields[0]?.name), then use that anchor inside the loop when building the
per-model link; no other logic changes to renderUsedBySection, fieldLinks, or
dir computation are needed.
In `@packages/plugins/documentation/src/types.ts`:
- Line 67: Move the stray "import type { DataModel, Enum, Model, Procedure,
TypeDef } from '@zenstackhq/language/ast'" to the top of the file above all
interface/type definitions so all imports are declared first; locate the import
line and the nearby interfaces (e.g., any interface/type declarations that
currently precede that import) and cut-and-paste the import to the file header,
then run the linter/tsc to ensure there are no unused-import or ordering issues
and adjust any other imports if necessary.
In `@packages/plugins/documentation/test/generator/enum-page.test.ts`:
- Around line 40-57: The nested describe 'enum with usage by models' creates a
temp dir in its beforeAll using the tmpDir variable but lacks a corresponding
cleanup; add an afterAll that calls fs.rmSync(tmpDir, { recursive: true, force:
true }) (or equivalent async removal) to remove the generated temp directory
after the tests finish, pairing the teardown with the existing beforeAll in that
describe block.
In `@packages/plugins/documentation/test/generator/model-page.test.ts`:
- Around line 1-6: The import list in model-page.test.ts includes an unused
symbol afterAll from vitest; remove afterAll from the import statement (the line
importing afterAll, beforeAll, describe, expect, it) so the file only imports
the actually used helpers (beforeAll, describe, expect, it) to clean up unused
imports.
In `@packages/plugins/documentation/test/generator/snapshot.test.ts`:
- Around line 14-65: The test creates a temporary directory via
generateFromSchema and assigns it to tmpDir but never cleans it up; move tmpDir
to an outer scope (let tmpDir: string) or have generateFromSchema return a
disposable, then add an afterEach or afterAll hook that removes the directory
(use fs.promises.rm or fs.rmSync with recursive/force) to delete tmpDir after
the test finishes; ensure the cleanup runs regardless of test outcome so
readDoc/stabilize assertions still work before removal.
In `@packages/plugins/documentation/test/generator/type-view-page.test.ts`:
- Around line 62-63: The assertions in type-view-page.test.ts are too permissive
because they check typeDoc for '[Post](../models/Post.md' and
'[User](../models/User.md' without the closing ')' — update the two expectations
that reference the typeDoc variable (currently using toContain) to either
include the trailing ')' so they assert a well-formed link, or replace them with
regex assertions (e.g., use toMatch with
/\[Post\]\(\.\.\/models\/Post\.md[^)]*\)/ and similarly for User) to allow
optional anchor suffixes while ensuring the link is closed; adjust the two
expect lines that reference '[Post]' and '[User]' accordingly.
In `@packages/plugins/documentation/test/integration/multifile.test.ts`:
- Line 9: The test-scoped variable tmpDir is declared as let tmpDir: string
which can trigger TS2454 (used before assigned); change its declaration to use a
definite assignment assertion (tmpDir!: string) or initialize it immediately so
TypeScript knows it will be set before use, updating references in this test
file (the tmpDir binding used in beforeEach/afterEach and tests) accordingly.
In `@packages/plugins/documentation/test/integration/samples.test.ts`:
- Around line 8-41: Move the repeated generateFromFile call into a beforeAll
hook so the suite generates docs once and shares the result: declare a
suite-scoped variable tmpDir in the describe block, call
generateFromFile(SAMPLES_DIR + 'shared/schema.zmodel') inside beforeAll and
assign to tmpDir, then update both it blocks to use that tmpDir (for readDoc and
findBrokenLinks) instead of each calling generateFromFile again; keep references
to generateFromFile, beforeAll, tmpDir, readDoc, and findBrokenLinks when making
the change.
In `@packages/plugins/documentation/test/integration/showcase.test.ts`:
- Around line 9-10: Move the repeated generateFromFile(SHOWCASE_SCHEMA) calls
into a shared beforeAll that assigns the result to a top-level tmpDir variable
so all tests reuse the same generated docs; update tests to use that tmpDir
instead of calling generateFromFile themselves, but keep per-test
generateFromFile calls only for cases that need non-default options (e.g., tests
that pass generateSkill: true or includeInternalModels: true). Locate usages of
generateFromFile and SHOWCASE_SCHEMA in the suite and replace duplicates with
the shared tmpDir, and add a beforeAll block (using the same test framework
helpers) that sets tmpDir by calling generateFromFile(SHOWCASE_SCHEMA) once.
In `@packages/plugins/documentation/test/utils.ts`:
- Around line 42-71: generateFromSchema and generateFromFile create temp
directories with fs.mkdtempSync but never remove them; modify these helpers to
return a disposable handle (e.g., { tmpDir, cleanup: async () => rm(tmpDir, {
recursive: true }) }) or collect created tmpDirs in a shared array and
expose/implement a cleanup function that tests call from afterEach/afterAll to
remove them; update callers/tests to invoke the cleanup to ensure temp dirs are
removed.
- Around line 86-113: findBrokenLinks currently only checks that the target file
exists (uses href, filePart, resolved) but ignores anchor fragments after “#”,
so links like ./index.md#nonexistent-anchor are treated as valid; update
findBrokenLinks to extract the fragment portion from href (the part after ‘#’),
and when a fragment exists read the resolved file contents and verify the anchor
is present by checking for either an explicit HTML anchor (<a name="..."> or
id="...">) or a generated heading id (convert markdown headings to their slug
form or search for the heading text), and if the anchor is missing push the same
broken record (source, link, target) into broken; keep the existing file-exists
check and only perform the fragment validation when a fragment is present.
- Around line 19-30: The loadSchema function uses crypto.randomUUID() but the
file lacks an explicit import; add an explicit node-style import for crypto
(e.g., import crypto from 'node:crypto') at the top of the module so loadSchema
can call crypto.randomUUID() consistently with other node imports and avoid
relying on global availability.
In `@packages/plugins/documentation/zenstack/showcase.zmodel`:
- Around line 158-160: The Project and Task models currently use an overly
permissive rule @@allow('create,update', true) which grants unrestricted write
access; update those policy entries by scoping write permissions to an
appropriate check (e.g., organization member or authenticated user) or replace
the blanket true with a predicate (for example isMember(user, project) or auth
!= null), and if you intend to keep this for the demo add an explicit comment
above the @@allow('create,update', true) lines stating this is intentionally
permissive for showcase only to prevent accidental copy to production; reference
the Project and Task model policy blocks and the @@allow('create,update', true)
entries when making the change.
In `@packages/plugins/documentation/zenstack/verbose/content.zmodel`:
- Around line 170-178: The @@deny('create,update,delete', true) rule on the
AuditLog model prevents creating audit entries via the ORM; add a short inline
comment adjacent to that rule (next to @@deny('create,update,delete', true) or
above the AuditLog model) clarifying the intended insertion path (e.g., "entries
created via unchecked Prisma client or bypassing ZenStack access control") so
readers know creation must occur outside the normal ORM access layer.
|
@coderabbitai help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
Status, support, documentation and community
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
packages/plugins/documentation/src/generator.ts (1)
53-57: Only the top-level output directory creation is wrapped in a descriptive try-catch.The subdirectory
mkdirSynccalls formodelsDir,viewsDir,typesDir,enumsDir, andproceduresDir(lines 75, 90, 113, 129, 142) are not guarded. If any of these fail (e.g., permission error), the thrown Node.js error lacks contextual information about which directory was involved.♻️ Proposed helper
+function mkdirSafe(dir: string): void { + try { + fs.mkdirSync(dir, { recursive: true }); + } catch (err) { + throw new Error(`Failed to create directory "${dir}": ${err instanceof Error ? err.message : String(err)}`); + } +}Replace all
fs.mkdirSync(…, { recursive: true })call-sites (including the top-level one) withmkdirSafe(…).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/generator.ts` around lines 53 - 57, The various fs.mkdirSync calls for outputDir, modelsDir, viewsDir, typesDir, enumsDir, and proceduresDir currently lack contextual error messages; implement a small helper function mkdirSafe(dir: string) that calls fs.mkdirSync(dir, { recursive: true }) inside a try/catch and rethrows a descriptive Error including the directory path and original error (preserving message), then replace every direct mkdirSync call in generator.ts (including the existing top-level outputDir usage and the subdirectory calls) with mkdirSafe(...) so any failure clearly indicates which directory failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/plugins/documentation/src/extractors.ts`:
- Around line 117-143: getFieldTypeName currently only links DataModel and Enum
refs; when linked is true it should also detect TypeDef refs and link them like
resolveTypeLink does. Update getFieldTypeName (which inspects
field.type.reference.ref) to check isTypeDef(ref) in the linked branch and set
typeName to `[${ref.name}](../types/${ref.name}.md)` when true, keeping existing
isDataModel and isEnum checks intact so TypeDef names render as links in
model/view field tables.
In `@packages/plugins/documentation/test/generator/procedure-page.test.ts`:
- Around line 38-39: The test uses procDoc.split(...).find(...) to set roleLine
(and a second similar variable at lines 93–94) which can be undefined, causing
an unhelpful Vitest error when calling expect(...).toContain; update both places
to first assert the found line is defined (e.g., expect(roleLine).toBeDefined()
or throw a clear test failure if undefined) and only then call
expect(roleLine).toContain('No') so the failure message pinpoints the missing
table row rather than producing a cryptic undefined error.
In `@packages/plugins/documentation/zenstack/verbose/billing.zmodel`:
- Line 56: The model contains redundant table-level indexes for fields that are
already declared unique; remove the explicit @@index entries that target
stripeSubscriptionId and invoiceNumber (the @@index directives corresponding to
stripeSubscriptionId and invoiceNumber) because the `@unique` on the
stripeSubscriptionId and invoiceNumber fields already creates a unique
index—delete those redundant @@index lines to avoid duplicate indexes.
In `@packages/plugins/documentation/zenstack/verbose/identity.zmodel`:
- Around line 65-96: The Workspace model currently defines @@allow('read',
members?[user == auth()]) and @@allow('update', members?[role == 'OWNER' || role
== 'ADMIN']) but lacks an @@allow('create', ...) rule, so creation is implicitly
denied; either add an explicit creation policy (e.g., @@allow('create',
members?[role == 'OWNER' || role == 'ADMIN']) or whichever role/group should be
allowed) on the Workspace model or add a short doc comment on the Workspace
model explaining that creation is intentionally handled outside policy
(privileged service/batch) so reviewers understand the denial is deliberate.
- Around line 49-50: The file contains redundant @@index declarations for fields
that already have `@unique` — remove the duplicated index directives: delete the
@@index([primaryEmail]) and @@index([handle]) entries associated with the model
that defines the primaryEmail and handle fields (keep the existing `@unique` on
those fields), and likewise remove the @@index([slug]) in the Workspace model
while preserving Workspace.slug's `@unique`; this eliminates duplicate database
indexes without changing uniqueness semantics.
In `@packages/plugins/documentation/zenstack/verbose/notifications.zmodel`:
- Around line 56-71: NotificationPreference currently has no access-control
policy so any authenticated user can CRUD others' preferences; add an allow rule
to restrict operations to the owning user by applying a model-level policy like
@@allow('read,create,update,delete', user == auth()) on the
NotificationPreference model (target the model name NotificationPreference and
its relation fields user/userId) so only the authenticated owner can perform
read/create/update/delete on their own preferences.
---
Duplicate comments:
In `@packages/plugins/documentation/zenstack/verbose/billing.zmodel`:
- Around line 84-85: The doc comment for the field paymentAttempts was
previously inconsistent with the validation; ensure the comment and the
@@validate rule both state the same cap (10) by confirming the paymentAttempts
Int `@default`(0) docstring matches the @@validate constraint in this model
(paymentAttempts and the model-level @@validate entry) and update any other
stray references to the retry cap so all mentions consistently say "Capped at 10
retries."
In `@packages/plugins/documentation/zenstack/verbose/notifications.zmodel`:
- Around line 65-66: This is a duplicate review comment: the relation is already
correctly defined as NotificationPreference.user (user User `@relation`(fields:
[userId], references: [id])) and the back-reference exists as
User.notificationPreferences; no code change is required—please resolve/close
the duplicate review comment or remove the redundant note.
---
Nitpick comments:
In `@packages/plugins/documentation/src/generator.ts`:
- Around line 53-57: The various fs.mkdirSync calls for outputDir, modelsDir,
viewsDir, typesDir, enumsDir, and proceduresDir currently lack contextual error
messages; implement a small helper function mkdirSafe(dir: string) that calls
fs.mkdirSync(dir, { recursive: true }) inside a try/catch and rethrows a
descriptive Error including the directory path and original error (preserving
message), then replace every direct mkdirSync call in generator.ts (including
the existing top-level outputDir usage and the subdirectory calls) with
mkdirSafe(...) so any failure clearly indicates which directory failed.
|
@coderabbitai review |
|
@coderabbitai resolve |
✅ Actions performedReview triggered.
|
✅ Actions performedComments resolved. Auto-approval is disabled; enable |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/plugins/documentation/src/generator.ts (1)
112-140:[...models, ...views]is recomputed on each enum/type call — minor allocation opportunity.The combined array
[...models, ...views]is created identically at lines 121 and 136. Consider computing it once before the type/enum loops to avoid the repeated allocation:♻️ Suggested refactor
+ const allModelsAndViews = [...models, ...views]; + const typesDir = path.join(outputDir, 'types'); const typeDefs = context.model.declarations.filter(isTypeDef); if (typeDefs.length > 0) { ... for (const typeDef of sortedTypes) { writeFile( path.join(typesDir, `${typeDef.name}.md`), - renderTypePage({ typeDef, allModels: [...models, ...views], options, navigation: typeNav.get(typeDef.name) }), + renderTypePage({ typeDef, allModels: allModelsAndViews, options, navigation: typeNav.get(typeDef.name) }), ); ... for (const enumDecl of sortedEnums) { writeFile( path.join(enumsDir, `${enumDecl.name}.md`), - renderEnumPage({ enumDecl, allModels: [...models, ...views], options, navigation: enumNav.get(enumDecl.name) }), + renderEnumPage({ enumDecl, allModels: allModelsAndViews, options, navigation: enumNav.get(enumDecl.name) }), );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/generator.ts` around lines 112 - 140, Compute the combined array of models and views once and reuse it instead of recreating [...models, ...views] inside each renderTypePage and renderEnumPage call: create a variable (e.g., combinedModelsViews = [...models, ...views]) before the types/enums blocks and pass combinedModelsViews to renderTypePage and renderEnumPage (where currently allModels: [...models, ...views] is used); leave other logic (sorting, nav building, filesGenerated++) unchanged.packages/plugins/documentation/src/renderers/skill-page.ts (1)
307-383: Validation rule collection duplicates logic frommodel-page.ts.
renderConstraints(lines 316-330) andcollectValidationRulesinmodel-page.ts(lines 36-67) both iterate model fields checking for@@@validationmeta-attributes. Consider extracting the shared field-level validation scanning into a reusable helper inextractors.tsto keep these in sync.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/src/renderers/skill-page.ts` around lines 307 - 383, renderConstraints duplicates the field-level validation scanning logic found in collectValidationRules (model-page.ts); extract that shared logic into a new helper in extractors.ts (e.g., getFieldValidationEntries or collectFieldValidations) that accepts models: DataModel[] and returns Array<{ model: string; field: string; rule: string }>, then replace the inline loop in renderConstraints and the implementation in collectValidationRules to call that helper so both use a single source of truth for detecting @@@validation attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/plugins/documentation/src/renderers/model-page.ts`:
- Around line 222-232: The relation cardinality heuristic in the loop over
relationFields incorrectly treats all non-array, non-optional relations as
'Many→One'; update the logic in the loop that sets relType (currently using
field.type.array and field.type.optional) to also detect One→One by checking
whether the FK/field is marked unique (e.g., inspect field.attributes or
field.type.reference?.ref?.unique or any attribute like a.name === 'unique' on
the field) and set relType = 'One\u2192One' for mandatory unique FKs; preserve
existing handling for array -> 'One\u2192Many' and optional -> 'Many\u2192One?'
and default -> 'Many\u2192One'.
---
Nitpick comments:
In `@packages/plugins/documentation/src/generator.ts`:
- Around line 112-140: Compute the combined array of models and views once and
reuse it instead of recreating [...models, ...views] inside each renderTypePage
and renderEnumPage call: create a variable (e.g., combinedModelsViews =
[...models, ...views]) before the types/enums blocks and pass
combinedModelsViews to renderTypePage and renderEnumPage (where currently
allModels: [...models, ...views] is used); leave other logic (sorting, nav
building, filesGenerated++) unchanged.
In `@packages/plugins/documentation/src/renderers/skill-page.ts`:
- Around line 307-383: renderConstraints duplicates the field-level validation
scanning logic found in collectValidationRules (model-page.ts); extract that
shared logic into a new helper in extractors.ts (e.g., getFieldValidationEntries
or collectFieldValidations) that accepts models: DataModel[] and returns Array<{
model: string; field: string; rule: string }>, then replace the inline loop in
renderConstraints and the implementation in collectValidationRules to call that
helper so both use a single source of truth for detecting @@@validation
attributes.
bcc35db to
1a1b0a7
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
packages/plugins/documentation/scripts/preview-themes.ts (1)
11-25: Mixing sync file I/O with anasyncfunction — consider usingfs/promisesthroughout.
readFileSync,mkdirSync, andwriteFileSyncblock the event loop inside anasyncfunction that alreadyawaitsrenderMermaid. For a one-shot script this is harmless, but consistent use offs/promisesis more idiomatic and eliminates the mixture.♻️ Suggested refactor
-import fs from 'node:fs'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; ... async function run() { - const mermaidSource = fs.readFileSync(mmdPath, 'utf-8'); - fs.mkdirSync(outDir, { recursive: true }); + const mermaidSource = await fsp.readFile(mmdPath, 'utf-8'); + await fsp.mkdir(outDir, { recursive: true }); const themeNames = Object.keys(THEMES) as Array<keyof typeof THEMES>; console.log(`Rendering ${themeNames.length} themes...\n`); for (const name of themeNames) { const svg = await renderMermaid(mermaidSource, THEMES[name]); const outPath = path.join(outDir, `schema-erd-${name}.svg`); - fs.writeFileSync(outPath, svg); + await fsp.writeFile(outPath, svg); console.log(` ✓ ${name} → ${path.relative(process.cwd(), outPath)}`); } const defaultSvg = await renderMermaid(mermaidSource); - fs.writeFileSync(path.join(outDir, 'schema-erd-default.svg'), defaultSvg); + await fsp.writeFile(path.join(outDir, 'schema-erd-default.svg'), defaultSvg); console.log(' ✓ default (no theme)'); ... - fs.writeFileSync(path.join(outDir, 'index.md'), indexLines.join('\n')); + await fsp.writeFile(path.join(outDir, 'index.md'), indexLines.join('\n'));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/scripts/preview-themes.ts` around lines 11 - 25, Replace the synchronous fs calls in the async flow with fs/promises async equivalents: read the mermaid source with await fs.readFile(mmdPath, 'utf-8'), create the output directory with await fs.mkdir(outDir, { recursive: true }), and write each SVG and the default SVG with await fs.writeFile(outPath, svg) (and await fs.writeFile for schema-erd-default.svg); keep using renderMermaid(THEMES[name]) and the existing variables (mermaidSource, THEMES, outDir, mmdPath) but import/require 'fs/promises' instead of the sync 'fs' so the function remains fully non-blocking.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/plugins/documentation/RFC-erd-svg-export.md`:
- Line 107: Add missing fenced-code language specifiers in
RFC-erd-svg-export.md: label the "module structure" code fence with text, the
Mermaid diagram fence with mermaid, and the directory tree fence with text so
markdownlint MD040 is satisfied; update the three unlabelled code fences that
appear in the module structure, Mermaid diagram, and directory tree sections to
include those language identifiers.
In `@packages/plugins/documentation/scripts/preview-themes.ts`:
- Around line 7-11: The code calls fs.readFileSync(mmdPath) in run() which
throws a cryptic ENOENT if schema-erd.mmd hasn't been generated; add a
pre-flight guard that checks fs.existsSync(mmdPath) (or fs.promises.access)
before reading and if the file is missing, emit a clear error mentioning that
preview.ts must be run to produce schema-erd.mmd (include mmdPath in the
message) and then exit/throw (e.g., process.exit(1) or throw new Error) so
mermaidSource read never hits a raw ENOENT.
In `@packages/plugins/documentation/src/extractors.ts`:
- Around line 213-233: collectRelationships currently iterates model.fields and
therefore omits inherited/mixin relation fields; replace the direct field
iteration with the canonical field getter used elsewhere (call
getAllFields(model, true) instead of model.fields) inside collectRelationships
so inherited and mixed-in relation fields are included when building
Relationship entries; update any local variable names if needed and ensure
relationship entries still use model.name, field.name and the same RelationType
logic so renderRelationshipsSection and buildFullErDiagram receive the complete
set of connectors.
In `@packages/plugins/documentation/src/generator.ts`:
- Around line 174-204: The code sets hasErd = pluginOpts.generateErd === true
but doesn’t reflect which files were actually written (mmd/svg), causing broken
links; update the generation block around buildFullErDiagram / renderErdSvg /
writeFile to track two booleans (hasErdMmd and hasErdSvg) that are set only when
you successfully write schema-erd.mmd or schema-erd.svg (and adjust the fallback
logic so you only mark the fallback file you wrote), increment filesGenerated
accordingly, then pass hasErdMmd and hasErdSvg (instead of or in addition to
hasErd) into renderIndexPage and update IndexPageProps and renderErdSection to
conditionally render the SVG embed and MMD link based on those flags.
In `@packages/plugins/documentation/src/renderers/model-page.ts`:
- Around line 114-128: The overflow indicator currently injected inside the
entity block in renderRelatedEntityFields (the line pushing `_more_
and_${remaining}_more_fields`) produces invalid Mermaid erDiagram syntax; change
the function so the entity block contains only the capped fields (remove the
in-block overflow push) and, if fields.length > MAX_RELATED_FIELDS, append a
separate trailing comment line after the closing '}' using Mermaid comment
syntax (e.g., a line starting with %% noting the remaining count) so the diagram
parser won't break; update the logic around getAllFields/capped/remaining and
the lines array construction in renderRelatedEntityFields accordingly.
---
Duplicate comments:
In `@packages/plugins/documentation/src/extractors.ts`:
- Around line 117-143: getFieldTypeName currently treats non-data-model/non-enum
references as bare names, so TypeDef references (custom types) aren't linked;
update the linked branch inside getFieldTypeName to detect isTypeDef(ref)
(isTypeDef is already imported) and set typeName to a markdown link to the type
doc (e.g. `../types/${ref.name}.md`) before falling back to the plain ref.name;
keep existing handling for isDataModel and isEnum and leave the final fallback
for unknown refs.
---
Nitpick comments:
In `@packages/plugins/documentation/scripts/preview-themes.ts`:
- Around line 11-25: Replace the synchronous fs calls in the async flow with
fs/promises async equivalents: read the mermaid source with await
fs.readFile(mmdPath, 'utf-8'), create the output directory with await
fs.mkdir(outDir, { recursive: true }), and write each SVG and the default SVG
with await fs.writeFile(outPath, svg) (and await fs.writeFile for
schema-erd-default.svg); keep using renderMermaid(THEMES[name]) and the existing
variables (mermaidSource, THEMES, outDir, mmdPath) but import/require
'fs/promises' instead of the sync 'fs' so the function remains fully
non-blocking.
| const mmdPath = path.resolve(__dirname, '../preview-output/showcase/schema-erd.mmd'); | ||
| const outDir = path.resolve(__dirname, '../preview-output/themes'); | ||
|
|
||
| async function run() { | ||
| const mermaidSource = fs.readFileSync(mmdPath, 'utf-8'); |
There was a problem hiding this comment.
Add a pre-flight check for the required input file.
readFileSync will throw a cryptic ENOENT if preview.ts hasn't been run first to generate schema-erd.mmd. A clear guard makes the failure mode obvious.
🛡️ Proposed fix
const mmdPath = path.resolve(__dirname, '../preview-output/showcase/schema-erd.mmd');
const outDir = path.resolve(__dirname, '../preview-output/themes');
async function run() {
+ if (!fs.existsSync(mmdPath)) {
+ console.error(`Input file not found: ${mmdPath}`);
+ console.error('Run `preview.ts` first to generate the showcase ERD.');
+ process.exit(1);
+ }
const mermaidSource = fs.readFileSync(mmdPath, 'utf-8');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const mmdPath = path.resolve(__dirname, '../preview-output/showcase/schema-erd.mmd'); | |
| const outDir = path.resolve(__dirname, '../preview-output/themes'); | |
| async function run() { | |
| const mermaidSource = fs.readFileSync(mmdPath, 'utf-8'); | |
| const mmdPath = path.resolve(__dirname, '../preview-output/showcase/schema-erd.mmd'); | |
| const outDir = path.resolve(__dirname, '../preview-output/themes'); | |
| async function run() { | |
| if (!fs.existsSync(mmdPath)) { | |
| console.error(`Input file not found: ${mmdPath}`); | |
| console.error('Run `preview.ts` first to generate the showcase ERD.'); | |
| process.exit(1); | |
| } | |
| const mermaidSource = fs.readFileSync(mmdPath, 'utf-8'); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/scripts/preview-themes.ts` around lines 7 -
11, The code calls fs.readFileSync(mmdPath) in run() which throws a cryptic
ENOENT if schema-erd.mmd hasn't been generated; add a pre-flight guard that
checks fs.existsSync(mmdPath) (or fs.promises.access) before reading and if the
file is missing, emit a clear error mentioning that preview.ts must be run to
produce schema-erd.mmd (include mmdPath in the message) and then exit/throw
(e.g., process.exit(1) or throw new Error) so mermaidSource read never hits a
raw ENOENT.
| /** Collects all relation fields across the given models into a flat list of `Relationship` entries. */ | ||
| export function collectRelationships(models: DataModel[]): Relationship[] { | ||
| const rels: Relationship[] = []; | ||
| for (const model of models) { | ||
| for (const field of model.fields) { | ||
| if (field.type.reference?.ref && isDataModel(field.type.reference.ref)) { | ||
| const to = field.type.reference.ref.name; | ||
| let relType: RelationType; | ||
| if (field.type.array) { | ||
| relType = 'One\u2192Many'; | ||
| } else if (field.type.optional) { | ||
| relType = 'Many\u2192One?'; | ||
| } else { | ||
| relType = 'Many\u2192One'; | ||
| } | ||
| rels.push({ from: model.name, field: field.name, to, type: relType }); | ||
| } | ||
| } | ||
| } | ||
| return rels; | ||
| } |
There was a problem hiding this comment.
collectRelationships iterates model.fields, missing inherited and mixin relation fields in the ERD.
Line 217 loops over model.fields directly. For models that inherit from a base model or mix in a TypeDef, relation fields defined on the parent/mixin won't be included. In contrast, renderRelationshipsSection in model-page.ts correctly uses getAllFields(model, true) to capture the full field set.
The ERD produced by buildFullErDiagram could therefore be missing connectors for schema patterns that rely on inheritance.
🐛 Proposed fix
+import { getAllFields } from '@zenstackhq/language/utils';
export function collectRelationships(models: DataModel[]): Relationship[] {
const rels: Relationship[] = [];
for (const model of models) {
- for (const field of model.fields) {
+ for (const field of getAllFields(model)) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/src/extractors.ts` around lines 213 - 233,
collectRelationships currently iterates model.fields and therefore omits
inherited/mixin relation fields; replace the direct field iteration with the
canonical field getter used elsewhere (call getAllFields(model, true) instead of
model.fields) inside collectRelationships so inherited and mixed-in relation
fields are included when building Relationship entries; update any local
variable names if needed and ensure relationship entries still use model.name,
field.name and the same RelationType logic so renderRelationshipsSection and
buildFullErDiagram receive the complete set of connectors.
| if (pluginOpts.generateErd) { | ||
| const mermaidSource = buildFullErDiagram({ models, relations: allRelations }); | ||
| const format = pluginOpts.erdFormat ?? 'both'; | ||
|
|
||
| if (format === 'mmd' || format === 'both') { | ||
| writeFile(path.join(outputDir, 'schema-erd.mmd'), mermaidSource); | ||
| filesGenerated++; | ||
| } | ||
|
|
||
| if (format === 'svg' || format === 'both') { | ||
| const svg = await renderErdSvg(mermaidSource, pluginOpts.erdTheme); | ||
| if (svg) { | ||
| writeFile(path.join(outputDir, 'schema-erd.svg'), svg); | ||
| filesGenerated++; | ||
| } else if (format === 'svg') { | ||
| writeFile(path.join(outputDir, 'schema-erd.mmd'), mermaidSource); | ||
| filesGenerated++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const hasErd = pluginOpts.generateErd === true; | ||
|
|
||
| filesGenerated++; | ||
| genCtx.durationMs = Math.round((performance.now() - startTime) * 100) / 100; | ||
| genCtx.filesGenerated = filesGenerated; | ||
|
|
||
| writeFile( | ||
| path.join(outputDir, 'index.md'), | ||
| renderIndexPage({ astModel: context.model, pluginOptions: pluginOpts, hasRelationships, hasErd, genCtx }), | ||
| ); |
There was a problem hiding this comment.
hasErd doesn't reflect erdFormat, causing broken links/images in index.md.
hasErd is always true when generateErd: true, but renderErdSection in index-page.ts unconditionally embeds ./schema-erd.svg and links ./schema-erd.mmd. This produces broken references in three real scenarios:
erdFormat |
beautiful-mermaid |
Result |
|---|---|---|
'mmd' |
any | SVG not written → broken image |
'svg' |
installed | MMD not written → broken link |
'svg' |
not installed | Fallback writes MMD, no SVG → broken image |
Proposed fix: track what was actually written and pass granular flags to renderIndexPage.
🐛 Proposed fix
- if (pluginOpts.generateErd) {
+ let hasErdSvg = false;
+ let hasErdMmd = false;
+ if (pluginOpts.generateErd) {
const mermaidSource = buildFullErDiagram({ models, relations: allRelations });
const format = pluginOpts.erdFormat ?? 'both';
if (format === 'mmd' || format === 'both') {
writeFile(path.join(outputDir, 'schema-erd.mmd'), mermaidSource);
filesGenerated++;
+ hasErdMmd = true;
}
if (format === 'svg' || format === 'both') {
const svg = await renderErdSvg(mermaidSource, pluginOpts.erdTheme);
if (svg) {
writeFile(path.join(outputDir, 'schema-erd.svg'), svg);
filesGenerated++;
+ hasErdSvg = true;
} else if (format === 'svg') {
writeFile(path.join(outputDir, 'schema-erd.mmd'), mermaidSource);
filesGenerated++;
+ hasErdMmd = true;
}
}
}
- const hasErd = pluginOpts.generateErd === true;Then pass hasErdSvg and hasErdMmd to renderIndexPage (requires updating IndexPageProps and renderErdSection in index-page.ts to conditionally render the image embed and Mermaid source link).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/src/generator.ts` around lines 174 - 204, The
code sets hasErd = pluginOpts.generateErd === true but doesn’t reflect which
files were actually written (mmd/svg), causing broken links; update the
generation block around buildFullErDiagram / renderErdSvg / writeFile to track
two booleans (hasErdMmd and hasErdSvg) that are set only when you successfully
write schema-erd.mmd or schema-erd.svg (and adjust the fallback logic so you
only mark the fallback file you wrote), increment filesGenerated accordingly,
then pass hasErdMmd and hasErdSvg (instead of or in addition to hasErd) into
renderIndexPage and update IndexPageProps and renderErdSection to conditionally
render the SVG embed and MMD link based on those flags.
| function renderRelatedEntityFields(model: DataModel): string[] { | ||
| const fields = getAllFields(model, true) | ||
| .filter((f) => !(f.type.reference?.ref && isDataModel(f.type.reference.ref))); | ||
| const capped = fields.slice(0, MAX_RELATED_FIELDS); | ||
| const lines: string[] = [` ${model.name} {`]; | ||
| for (const field of capped) { | ||
| lines.push(` ${resolveTypeName(field.type)} ${field.name}`); | ||
| } | ||
| if (fields.length > MAX_RELATED_FIELDS) { | ||
| const remaining = fields.length - MAX_RELATED_FIELDS; | ||
| lines.push(` _more_ and_${remaining}_more_fields`); | ||
| } | ||
| lines.push(' }'); | ||
| return lines; | ||
| } |
There was a problem hiding this comment.
Overflow indicator in Mermaid entity block is invalid syntax and may break diagram rendering.
Line 124 pushes _more_ and_${remaining}_more_fields inside an erDiagram entity block. Mermaid parses every line inside { } as FieldType FieldName [annotation], so this will either render as a spurious field row or cause a parse error that silently drops the entity.
Mermaid erDiagram does not support %%-style comments inside entity blocks. The cleanest fix is to emit only the capped fields and, if needed, append the overflow note as a trailing comment line after the closing }.
🐛 Proposed fix
- const lines: string[] = [` ${model.name} {`];
+ const lines: string[] = [` ${model.name} {`];
for (const field of capped) {
lines.push(` ${resolveTypeName(field.type)} ${field.name}`);
}
- if (fields.length > MAX_RELATED_FIELDS) {
- const remaining = fields.length - MAX_RELATED_FIELDS;
- lines.push(` _more_ and_${remaining}_more_fields`);
- }
lines.push(' }');
+ if (fields.length > MAX_RELATED_FIELDS) {
+ const remaining = fields.length - MAX_RELATED_FIELDS;
+ lines.push(` %% … and ${remaining} more fields`);
+ }
return lines;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function renderRelatedEntityFields(model: DataModel): string[] { | |
| const fields = getAllFields(model, true) | |
| .filter((f) => !(f.type.reference?.ref && isDataModel(f.type.reference.ref))); | |
| const capped = fields.slice(0, MAX_RELATED_FIELDS); | |
| const lines: string[] = [` ${model.name} {`]; | |
| for (const field of capped) { | |
| lines.push(` ${resolveTypeName(field.type)} ${field.name}`); | |
| } | |
| if (fields.length > MAX_RELATED_FIELDS) { | |
| const remaining = fields.length - MAX_RELATED_FIELDS; | |
| lines.push(` _more_ and_${remaining}_more_fields`); | |
| } | |
| lines.push(' }'); | |
| return lines; | |
| } | |
| function renderRelatedEntityFields(model: DataModel): string[] { | |
| const fields = getAllFields(model, true) | |
| .filter((f) => !(f.type.reference?.ref && isDataModel(f.type.reference.ref))); | |
| const capped = fields.slice(0, MAX_RELATED_FIELDS); | |
| const lines: string[] = [` ${model.name} {`]; | |
| for (const field of capped) { | |
| lines.push(` ${resolveTypeName(field.type)} ${field.name}`); | |
| } | |
| lines.push(' }'); | |
| if (fields.length > MAX_RELATED_FIELDS) { | |
| const remaining = fields.length - MAX_RELATED_FIELDS; | |
| lines.push(` %% … and ${remaining} more fields`); | |
| } | |
| return lines; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/src/renderers/model-page.ts` around lines 114
- 128, The overflow indicator currently injected inside the entity block in
renderRelatedEntityFields (the line pushing `_more_
and_${remaining}_more_fields`) produces invalid Mermaid erDiagram syntax; change
the function so the entity block contains only the capped fields (remove the
in-block overflow push) and, if fields.length > MAX_RELATED_FIELDS, append a
separate trailing comment line after the closing '}' using Mermaid comment
syntax (e.g., a line starting with %% noting the remaining count) so the diagram
parser won't break; update the logic around getAllFields/capped/remaining and
the lines array construction in renderRelatedEntityFields accordingly.
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (22)
packages/plugins/documentation/preview-output/showcase/types/ProjectStats.md-1-7 (1)
1-7:⚠️ Potential issue | 🟡 MinorFix the un-tagged fenced code block in the generator template to resolve MD040.
The "DO NOT MODIFY" banner is emitted inside a bare
```fence with no language identifier, which violates markdownlint rule MD040 (fenced-code-language). Since this file is auto-generated, the fix belongs in the renderer/template that emits this banner, not here.The simplest remediation is to tag the fence as
text:♻️ Proposed fix in the generator template
-``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// // DO NOT MODIFY THIS FILE // // This file is automatically generated by ZenStack CLI and should not be manually updated. // // Source: showcase.zmodel · Generated: 2026-02-22 // //////////////////////////////////////////////////////////////////////////////////////////////</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@packages/plugins/documentation/preview-output/showcase/types/ProjectStats.md
around lines 1 - 7, The auto-generated DO NOT MODIFY banner in ProjectStats.md
is emitted inside an un-tagged fenced code block causing markdownlint MD040;
update the generator/template that renders this banner so the fence includes a
language identifier (e.g., change the leadingtotext) when emitting the
block containing the "DO NOT MODIFY THIS FILE" banner and the subsequent lines
("This file is automatically generated..." and "Source: showcase.zmodel ·
Generated: ...") so the generated output uses a tagged fenced code block.</details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/showcase/procedures/createAndAssignTask.md-1-7 (1)</summary><blockquote> `1-7`: _⚠️ Potential issue_ | _🟡 Minor_ **Replace the fenced code block banner with an HTML comment.** The "DO NOT MODIFY" banner is placed inside a bare fenced code block (no language tag), which triggers MD040 in markdownlint. More importantly, an unlabelled code block renders visibly in every Markdown viewer as a code snippet at the top of the file, which is noisy. The idiomatic Markdown approach for an invisible machine-readable marker is an HTML comment. Since this is auto-generated output, the fix belongs in the generator template: <details> <summary>♻️ Proposed fix</summary> ```diff -``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -// Source: showcase.zmodel · Generated: 2026-02-22 // -////////////////////////////////////////////////////////////////////////////////////////////// -``` +<!-- AUTO-GENERATED — DO NOT MODIFY + Source: showcase.zmodel · Generated: 2026-02-22 + This file is automatically generated by ZenStack CLI and should not be manually updated. +--> ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/procedures/createAndAssignTask.md` around lines 1 - 7, Replace the top-of-file fenced code block containing the "DO NOT MODIFY THIS FILE" banner in createAndAssignTask.md with an HTML comment so the marker is invisible to Markdown renderers; remove the triple-backtick block that encloses the banner and instead emit an HTML comment starting with <!-- AUTO-GENERATED — DO NOT MODIFY and including the "Source: showcase.zmodel · Generated: 2026-02-22" and the single-line note about ZenStack CLI. Update the generator template that emits this banner (the template that produces the "DO NOT MODIFY" text) so it writes the HTML comment rather than a bare fenced code block. Ensure the exact banner text (including "DO NOT MODIFY THIS FILE", "ZenStack CLI", and the source/generation metadata) is preserved inside the HTML comment. ``` </details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/showcase/procedures/getProjectStats.md-1-7 (1)</summary><blockquote> `1-7`: _⚠️ Potential issue_ | _🟡 Minor_ **"DO NOT MODIFY" banner renders visibly; also triggers MD040 lint warning.** Two related concerns with the banner block: 1. **MD040 (fenced-code-language):** The opening fence has no language identifier, which triggers the markdownlint warning. Adding `text` (or `plaintext`) silences the lint. 2. **Rendered visibility:** A fenced code block is visible in every Markdown renderer and documentation site. End users reading the generated docs will see this boilerplate banner at the top of every page. An HTML comment is a better fit — it is completely invisible when rendered while still being readable in raw source. Since this file says "DO NOT MODIFY", both fixes must be applied in the generator (the renderer that produces procedure pages). <details> <summary>✏️ Suggested generator-side change (invisible banner via HTML comment)</summary> ```diff -``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -// Source: showcase.zmodel · Generated: 2026-02-22 // -////////////////////////////////////////////////////////////////////////////////////////////// -``` +<!-- DO NOT MODIFY THIS FILE + This file is automatically generated by ZenStack CLI and should not be manually updated. + Source: showcase.zmodel · Generated: 2026-02-22 --> ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/procedures/getProjectStats.md` around lines 1 - 7, Replace the visible fenced-code "DO NOT MODIFY" banner in generated procedure pages (e.g., getProjectStats.md) with an invisible HTML comment in the generator/template that emits these pages: remove the triple-backtick fenced block and instead emit <!-- DO NOT MODIFY THIS FILE ... --> containing the same lines; if you cannot emit an HTML comment for any reason, at minimum add a language identifier like text or plaintext to the opening fence to silence markdownlint (MD040). Update the generator/template function that renders procedure pages (the code that writes the banner for procedure Markdown files) to produce the HTML comment (or the fenced block with language identifier) so future generated files no longer render the banner and no longer trigger MD040. ``` </details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/verbose/procedures/unpublishArticle.md-1-7 (1)</summary><blockquote> `1-7`: _⚠️ Potential issue_ | _🟡 Minor_ **"DO NOT MODIFY" banner uses an unlanguaged code fence — consider HTML comments instead** The banner is wrapped in a plain ` ``` ` fence with no language identifier, which triggers `markdownlint` MD040 and renders a visible code block at the top of every generated page. Since this is a machine/generator annotation rather than user-facing content, an HTML comment would suppress it from rendered output entirely and avoid the lint warning: <details> <summary>♻️ Suggested generator-side fix (in `procedure-page.ts` or equivalent renderer)</summary> ```diff -``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -// Source: schema.zmodel · Generated: 2026-02-22 // -////////////////////////////////////////////////////////////////////////////////////////////// -``` +<!-- +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +// Source: schema.zmodel · Generated: 2026-02-22 // +////////////////////////////////////////////////////////////////////////////////////////////// +--> ``` </details> If keeping the banner visible is intentional (e.g., to warn humans editing the raw file), the minimal fix that satisfies MD040 is specifying a language on the fence: ```diff -``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// ``` Since this is a generated file, the change needs to be applied in the generator renderer, not here. <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@packages/plugins/documentation/preview-output/verbose/procedures/unpublishArticle.md
around lines 1 - 7, The generated banner is emitted as an unlanguaged Markdown
code fence causing MD040 and visible code blocks; update the generator (e.g.,
procedure-page.ts or the procedure renderer) to emit the banner as an HTML
comment instead of a triple-backtick fence (wrap with ) so it is
hidden from rendered output and lint-safe, or if visibility is required emit it
as a fenced block with a language (e.g., ```text) to satisfy markdownlint MD040;
modify the template/emit function that produces the banner (search for the
banner string or the function that writes the top-of-file header in
procedure-page.ts) and replace the fence emission accordingly.</details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/verbose/schema-erd.mmd-134-134 (1)</summary><blockquote> `134-134`: _⚠️ Potential issue_ | _🟡 Minor_ **Self-referential cardinality should be `o|` (zero or one), not `||` (exactly one).** `Comment }o--|| Comment : "parent"` asserts that every `Comment` **must** have a parent, but `parentId` is nullable in any threaded comment model (root-level comments have no parent). The correct notation is: <details> <summary>📐 Proposed fix</summary> ```diff - Comment }o--|| Comment : "parent" + Comment }o--o| Comment : "parent" ``` </details> Since this file is generated by `src/renderers/erd.ts`, the fix belongs in the renderer's cardinality logic for optional/nullable FK fields, not in this artifact directly. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/schema-erd.mmd` at line 134, The ERD shows a self-referential relation "Comment }o--|| Comment" which wrongly uses "||" (required) for the parent side despite parentId being nullable; update the cardinality logic in src/renderers/erd.ts (the function that computes/prints relation cardinalities—look for functions named renderRelation, formatCardinality or similar) so nullable/optional FK fields (e.g., parentId on Comment) render as "o|" (zero or one) instead of "||"; ensure the generator emits "Comment }o--o| Comment" or "Comment }o--o| Comment : \"parent\"" as appropriate for self-references. ``` </details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/verbose/SKILL.md-154-154 (1)</summary><blockquote> `154-154`: _⚠️ Potential issue_ | _🟡 Minor_ **Renderer bug: multi-line doc comment continuation lines are missing the `///` prefix throughout all embedded schema blocks.** Every wrapped doc comment in the embedded `prisma` code blocks has the `///` prefix only on the first line; continuation lines are emitted as bare text. In Prisma/ZModel syntax, each line of a doc comment must start with `///` — without it, the continuation is syntactically invalid (it would parse as an unrecognized statement, not a comment). Representative example from the `Article` block (lines 153–155): ```diff - /// Short summary shown in listings and social media previews. -Auto-generated from `body` if not explicitly provided. + /// Short summary shown in listings and social media previews. + /// Auto-generated from `body` if not explicitly provided. excerpt String? `@length`(0, 500) ``` This pattern repeats for every multi-line field or model doc comment across all embedded schema blocks in this file (~35+ occurrences). Since SKILL.md is designed to be consumed by AI agents as a machine-readable schema reference, having syntactically invalid code blocks risks the agent misinterpreting field-level documentation as field declarations. The root cause is in the generator source — likely in the doc-comment serialization logic in `src/renderers/common.ts` or `src/renderers/skill-page.ts`, where long doc strings appear to be wrapped on newlines but `///` is only prepended to the first segment. The fix should ensure every line of a multi-line doc comment emits a leading ` ///` (with appropriate indentation). Also applies to: 159-160, 162-163, 169-169, 172-172, 224-224, 235-235, 269-269, 306-306, 354-354, 364-364, 405-405, 455-455, 498-498, 507-507, 511-511, 514-514, 517-517, 555-555, 559-559, 562-562, 564-564, 615-615, 619-619, 623-623, 626-626, 629-629, 669-669, 672-672, 675-675, 715-715, 811-811, 814-814, 818-818, 821-821 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/SKILL.md` at line 154, Multi-line doc comments in generated prisma code blocks are only prefixed on the first line, making continuations invalid; update the doc-comment serialization (e.g., functions like renderDocComment, formatDocString, or emitPrismaBlock) so that when you wrap or split long docstrings you map each resulting line to include the leading " ///" (preserving the existing indentation), i.e., split on newlines or wrap output, then prepend " /// " to every line before joining, and ensure any wrapping utility used in common renderer code applies the same prefix for all lines. ``` </details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md-5-5 (1)</summary><blockquote> `5-5`: _⚠️ Potential issue_ | _🟡 Minor_ **`Source` in the banner header is inconsistent with `Defined in` in the body.** Line 5 reports `Source: schema.zmodel`, while Lines 21 and 24 correctly report `procedures.zmodel`. The banner is likely substituting the schema entry-point file rather than the declaring file. The generator should thread the per-entity source file name through to the banner template. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md` at line 5, The banner header is using the schema entry-point filename instead of the declaring file; update the generator so the per-entity source filename is passed into and used by the banner template. Locate the banner rendering code (e.g., renderEntityBanner/generateBanner or the template that emits "Source: …") and replace the use of the global schema entry-point variable with the entity-level property (entity.sourceFile or entity.declaredIn) so the banner and the "Defined in" body use the same per-entity source file value. ``` </details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md-1-7 (1)</summary><blockquote> `1-7`: _⚠️ Potential issue_ | _🟡 Minor_ **Generator should use a language tag (or HTML comment) for the "DO NOT MODIFY" banner.** The fenced code block used as the header banner specifies no language, which triggers the `MD040` (`fenced-code-language`) markdownlint rule. Because this file is auto-generated, the fix belongs in the renderer that emits the banner. Two options, in order of preference: <details> <summary>🛠️ Option A – Replace with an HTML comment (no visible block, no lint noise)</summary> ```diff -``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -// Source: schema.zmodel · Generated: 2026-02-22 // -////////////////////////////////////////////////////////////////////////////////////////////// -``` +<!-- ⚠️ DO NOT MODIFY THIS FILE — auto-generated by ZenStack CLI. + Source: procedures.zmodel · Generated: 2026-02-22 --> ``` </details> <details> <summary>🛠️ Option B – Keep the visible block, add `text` language tag</summary> ```diff -``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// // DO NOT MODIFY THIS FILE // ... ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md
around lines 1 - 7, The generated header banner for procedures/searchArticles.md
currently emits an untagged fenced code block which triggers MD040; update the
renderer that emits the banner so it either outputs an HTML comment (e.g., ) instead of the fenced block, or emits a fenced block
with a language tag such astext; locate the banner-emitting code in the generator for the procedures renderer (the routine that writes the top-of-file header for searchArticles.md) and replace the raw "" output with the chosen
HTML comment or "```text" output.</details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md-73-73 (1)</summary><blockquote> `73-73`: _⚠️ Potential issue_ | _🟡 Minor_ **Update the reference link to point to dedicated ZModel procedures documentation.** The link text "ZModel procedures" currently resolves to a generic reference category page (`https://zenstack.dev/docs/category/reference`). ZenStack provides dedicated procedures documentation at: - `https://zenstack.dev/docs/modeling/custom-proc` (declaring procedures) - `https://zenstack.dev/docs/orm/custom-proc` (implementing and calling procedures) Update the generator's reference-link table to point to one of these specific pages instead of the generic category. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md` at line 73, The generated reference link currently points to the generic URL "https://zenstack.dev/docs/category/reference" using the markdown link text "[ZModel procedures — ZenStack documentation]"; update the reference-link table entry that emits that link so it targets one of the dedicated ZModel procedure pages (either "https://zenstack.dev/docs/modeling/custom-proc" for declaring procedures or "https://zenstack.dev/docs/orm/custom-proc" for implementing/calling procedures). Locate the generator output that produces the exact link text "[ZModel procedures — ZenStack documentation]" and replace the href with the chosen specific URL, keeping the visible link text unchanged. ``` </details> </blockquote></details> <details> <summary>packages/plugins/documentation/preview-output/showcase/procedures/signUp.md-1-7 (1)</summary><blockquote> `1-7`: _⚠️ Potential issue_ | _🟡 Minor_ **Generator template: add a language identifier to the "DO NOT MODIFY" fence block.** All four generated procedure pages (and likely every generated file) open with a plain triple-backtick fence, triggering MD040 across the entire output. Since the files are auto-generated, the fix must go into the generator template, not the files themselves. The simplest fix is to use `text` as the language specifier. Alternatively, replacing the visible code block with an HTML comment would suppress the warning **and** avoid it appearing as a rendered code block in Markdown viewers while still protecting it from accidental editing. <details> <summary>✏️ Option A — use `text` language (minimal change in generator template)</summary> ```diff -``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// // DO NOT MODIFY THIS FILE // // This file is automatically generated by ZenStack CLI and should not be manually updated. // // Source: showcase.zmodel · Generated: 2026-02-22 // ////////////////////////////////////////////////////////////////////////////////////////////// ``` ``` </details> <details> <summary>✏️ Option B — emit as an HTML comment (invisible in rendered Markdown)</summary> ```diff -``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -// Source: showcase.zmodel · Generated: 2026-02-22 // -////////////////////////////////////////////////////////////////////////////////////////////// -``` +<!-- DO NOT MODIFY THIS FILE + Auto-generated by ZenStack CLI · Source: showcase.zmodel · Generated: 2026-02-22 --> ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@packages/plugins/documentation/preview-output/showcase/procedures/signUp.md
around lines 1 - 7, The generated files start with a plain triple-backtick fence
containing the "DO NOT MODIFY THIS FILE" banner which triggers MD040; update the
generator template that emits this header so it either emits the fence asidentifier) or replace the entire banner with an HTML comment (containing the "DO NOT MODIFY THIS FILE" text and the Source/Generated metadata). Locate the template that writes the starting fence and the banner (search for the literal triple-backtick, the string "DO NOT MODIFY THIS FILE", or the generated header block used for pages like signUp.md) and modify it to output one of the two safe variants.packages/plugins/documentation/preview-output/showcase/models/Organization.md-1-7 (1)
1-7:⚠️ Potential issue | 🟡 MinorGenerator should add a language tag to the "DO NOT MODIFY" header code fence.
All generated pages open with a fenced block containing only a comment banner but no language specifier, causing
MD040(fenced-code-language) across every output file. The fix belongs in the generator, not in these files.📝 Proposed generator-side fix
-``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// // DO NOT MODIFY THIS FILE // ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/models/Organization.md` around lines 1 - 7, The generated Markdown header fence currently opens with an empty-language code fence (```) containing the banner lines like "DO NOT MODIFY THIS FILE" and "Generated: 2026-02-22"; update the generator that emits this block so it uses a language tag (e.g., "text") on the opening fence (change from ``` to ```text) so the banner code block is flagged with a language and avoids MD040; ensure the generator emits the same banner content (e.g., "DO NOT MODIFY THIS FILE") but with the fenced code block language applied.packages/plugins/documentation/preview-output/showcase/models/Organization.md-135-141 (1)
135-141:⚠️ Potential issue | 🟡 MinorRemove the blank line between blockquote segments in the Access Policies section.
The blank line separating the description
>quote from the> [!IMPORTANT]callout triggersMD028(no-blanks-blockquote). This pattern likely affects every generated model page that includes access policies. Fix in the generator by emitting the two blockquote blocks without an intervening blank line, or by merging them.📝 Proposed generator-side fix (rendered output diff)
> ZenStack access control rules that determine who can read, create, update, or delete records. - > [!IMPORTANT] > Operations are **denied by default**. ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/models/Organization.md` around lines 135 - 141, The Access Policies section output contains an unwanted blank line between the description blockquote (`> ZenStack access control rules...`) and the callout blockquote (`> [!IMPORTANT]`), triggering MD028; update the generator that emits the "Access Policies" section (the template that writes the `>` block and the `> [!IMPORTANT]` callout for model pages such as Organization.md) to either emit those two blockquote lines consecutively with no intervening blank line or merge them into a single blockquote so the output contains `>` immediately followed by `> [!IMPORTANT]` with no blank line.packages/plugins/documentation/preview-output/verbose/models/Comment.md-107-174 (1)
107-174:⚠️ Potential issue | 🟡 MinorGenerator bug:
Commententity defined twice in the self-referential ER diagramThe Mermaid
erDiagramblock defines aComment {}entity at lines 109–120 (withPK/FKannotations) and again at lines 147–158 (without those annotations). This is caused by the self-referentialparent/repliesrelation — the generator emits the related entity block forCommenteven though it is the subject entity.Duplicate entity definitions may cause Mermaid rendering inconsistencies and produce confusing output (missing PK/FK markers in one block). The generator should skip re-emitting the entity block when the related entity is identical to the subject entity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/models/Comment.md` around lines 107 - 174, The Mermaid erDiagram output duplicates the Comment entity (marked as Comment {...} twice) due to the generator re-emitting related entities when the relation points back to the subject; update the generator logic that builds the erDiagram so it detects self-relations and skips re-emitting the subject entity block. Specifically, when rendering relations for the Comment model (and other models), check if the related entity name equals the current subject name (e.g., "Comment") and do not append a second Comment {...} block for parent/replies; ensure PK/FK annotations remain only on the original subject block.packages/plugins/documentation/preview-output/verbose/enums/BillingInterval.md-1-7 (1)
1-7:⚠️ Potential issue | 🟡 MinorSystemic: fenced code block in generated header should declare a language identifier
The "DO NOT MODIFY" banner uses a bare
```fence with no language tag, triggering MD040 in every one of the 8 generated files reviewed. This is a generator-level fix — the renderer that emits this header should specify a language (e.g.,```textor```plain).♻️ Suggested change in the renderer (applied once)
-``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// // DO NOT MODIFY THIS FILE // ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/enums/BillingInterval.md` around lines 1 - 7, The generated header's fenced code block lacks a language identifier, causing MD040; update the generator/template that emits the "DO NOT MODIFY THIS FILE" banner so the opening fence includes a language token (e.g., change ``` to ```text or ```plain) when producing the header around the block containing "DO NOT MODIFY THIS FILE" so all generated files (including the banner in BillingInterval.md) use a fenced code block with a declared language.packages/plugins/documentation/preview-output/showcase/views/ProjectTaskSummary.md-1-7 (1)
1-7:⚠️ Potential issue | 🟡 MinorAll generated files: add language identifier to the "DO NOT MODIFY" banner code fence (MD040).
Every generated file opens with a plain
```fence for the banner block, triggering MD040 across the entire output. Since these files are auto-generated, the fix belongs in the renderer that emits the banner. Usetext(orplaintext) as the language hint.✏️ Proposed renderer fix (applies to all generated files)
-``` +```text ////////////////////////////////////////////////////////////////////////////////////////////// // DO NOT MODIFY THIS FILE // ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/views/ProjectTaskSummary.md` around lines 1 - 7, The generated banner block currently starts with a plain code fence ``` which triggers MD040; update the renderer that emits the banner so it includes a language identifier (e.g., "text" or "plaintext") on the opening fence instead of a bare fence—look for the banner-emitting logic (the function or template that writes the initial banner code fence in the renderer for ProjectTaskSummary / showcase output) and change the emitted opening fence from "```" to "```text" so all generated files include the language hint.packages/plugins/documentation/preview-output/showcase/models/Activity.md-115-115 (1)
115-115:⚠️ Potential issue | 🟡 MinorMermaid ERD uses
||(exactly one) for a nullable/optional relationship — should beo|(zero or one).
taskIdisString?(nullable), making the Task side optional. The relationships table at line 77 correctly labels thisMany→One?, but the diagram uses}o--||which means "zero-or-more Activities to exactly one Task".In each cardinality marker there are two characters: the outermost character represents a maximum value, and the innermost represents a minimum value. The
||inner character (min 1, max 1) is wrong for a nullable FK;o|(min 0, max 1) is correct.The same bug is present in relationships.md at lines 66 and 68 (also flagged below). Fix is in the ERD renderer: when a FK field is nullable (
field.type.endsWith('?')), emito|instead of||on that side.✏️ Proposed fix
- Activity }o--|| Task : "task" + Activity }o--o| Task : "task"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/models/Activity.md` at line 115, The ERD renderer is emitting "||" for nullable foreign keys; update the function that builds relationship cardinalities (e.g., the ERD renderer method that constructs relation strings / the cardinality emitter used for Activity→Task and the relationships.md generator) to detect nullable FK fields by checking field.type.endsWith('?') and emit "o|" (zero-or-one) for the FK side instead of "||"; apply the same change where relationships.md is generated so Activity }o--o| Task and the other two relations use "o|" when the target FK is nullable.packages/plugins/documentation/preview-output/showcase/models/Activity.md-123-126 (1)
123-126:⚠️ Potential issue | 🟡 MinorMD028: blank line between adjacent blockquotes prevents
[!IMPORTANT]from rendering as a GitHub callout.A blank line between the section-description
>blockquote (line 123) and the> [!IMPORTANT]block (line 125) causes GitHub to parse them as two separate blockquotes. On GitHub, the[!IMPORTANT]callout syntax requires the>prefix to be on a continuous (uninterrupted) blockquote block to render as the styled alert box. The blank line breaks this continuity. Fix in the renderer that emits the Access Policies section — this pattern is consistent across all model pages.✏️ Proposed renderer fix
> ZenStack access control rules that determine who can read, create, update, or delete records. - > [!IMPORTANT] > Operations are **denied by default**. `@@allow` rules grant access; `@@deny` rules override any allow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/models/Activity.md` around lines 123 - 126, The Access Policies renderer is emitting a blank line between the descriptive blockquote and the callout blockquote (causing MD028 / GitHub to treat them as separate blockquotes); update the renderer that generates the "Access Policies" section (e.g., renderAccessPoliciesSection / emitBlockquoteLines) to avoid emitting an empty line between the description blockquote and the subsequent callout line so the `> [!IMPORTANT]` line remains part of the same continuous blockquote; ensure this change is applied to the template/function used across all model pages and add a unit/integration test that verifies no blank line is produced between adjacent blockquote lines.packages/plugins/documentation/preview-output/showcase/models/Task.md-199-212 (1)
199-212:⚠️ Potential issue | 🟡 MinorBlank line between adjacent blockquotes triggers MD028 in Access Policies section
The blank line separating the section description blockquote from the
> [!IMPORTANT]callout causesMD028. This pattern is repeated inProject.md(same section) andSubscription.md.The generator should either omit the blank line between adjacent blockquotes in this section or convert the description to plain text.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/models/Task.md` around lines 199 - 212, The Access Policies section contains two adjacent blockquotes (" > ZenStack access control rules that determine who can read, create, update, or delete records." and the callout starting with "> [!IMPORTANT]") separated by a blank line which triggers MD028; update the generation logic that emits the "## 🔐 Access Policies" section (and equivalents in Project.md and Subscription.md) so it either removes the blank line between adjacent blockquotes or converts the first blockquote into plain paragraph text, ensuring the emitted output places the descriptive sentence immediately above the "> [!IMPORTANT]" callout with no blank line between them.packages/plugins/documentation/preview-output/verbose/index.md-1-7 (1)
1-7:⚠️ Potential issue | 🟡 MinorGenerated header block is missing the fenced-code language specifier (applies to all generated files)
All generated files open with a triple-backtick code fence that carries no language tag, triggering
MD040in every output file. The fix belongs in the generator's header-rendering logic, not in the output files.💡 Suggested generator fix
Change the opening fence in whatever function emits the
generatedHeaderblock from:- ``` + ```text //////////////////////////////...Alternatively, render the notice as an HTML comment (
<!-- ... -->), which is invisible to readers but passes linters; or as a blockquote if visibility matters.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/index.md` around lines 1 - 7, The generated header block lacks a fenced-code language specifier, causing MD040; update the generator's header-rendering logic (the function that emits the generatedHeader block) to emit a language-tagged opening fence (e.g., change "```" to "```text") or alternatively render the notice as an HTML comment ("<!-- ... -->") or a blockquote so the generated files no longer trigger the linter; adjust the code path that produces the header (the generatedHeader emitter) to use one of these options consistently for all generated files.packages/plugins/documentation/preview-output/showcase/index.md-70-72 (1)
70-72:⚠️ Potential issue | 🟡 MinorERD image embed is unconditional but SVG requires an optional dependency
The index always renders
, but the SVG is only produced whenerdFormatis'svg'or'both'and the optionalbeautiful-mermaiddependency is installed. When users run the plugin witherdFormat = 'mmd'(or without the optional dep), the image link resolves to a missing file — a broken image on the generated index page.The generator should conditionally emit the SVG embed only when
erdFormatincludes'svg'and the rendered file exists.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/index.md` around lines 70 - 72, The index currently always embeds "./schema-erd.svg" which can be missing; update the generator that writes packages/plugins/documentation/preview-output/showcase/index.md to only emit the image markdown when the configuration value erdFormat includes "svg" (i.e., erdFormat === 'svg' or 'both') and the rendered file exists on disk (use a filesystem existence check for "schema-erd.svg" before writing the `` line); if the check fails, omit the SVG embed and still emit the Mermaid source link (`./schema-erd.mmd`) so the page won’t contain a broken image.packages/plugins/documentation/preview-output/verbose/models/User.md-1-7 (1)
1-7:⚠️ Potential issue | 🟡 MinorGenerator emits the "DO NOT MODIFY" banner without a language specifier
All five generated files trigger MD040 because the
```fence at line 1 has no language tag. The fix belongs in the generator template, not in these artifacts. Using an HTML comment would also suppress the banner from being rendered as a visible code block:💡 Suggested generator fix
-``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -... -``` +<!-- DO NOT MODIFY THIS FILE --> +<!-- This file is automatically generated by ZenStack CLI and should not be manually updated. --> +<!-- Source: {sourceFile} · Generated: {date} -->Alternatively, change the opening fence to
```textto satisfy MD040 while keeping the visible banner.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/models/User.md` around lines 1 - 7, The generated README banner emits a fenced code block with no language (the opening fence "```") which triggers MD040; update the generator template that produces the User.md banner so it either replaces the fenced block with HTML comments (e.g., emit "<!-- DO NOT MODIFY THIS FILE -->" etc.) or changes the opening fence to include a language tag (e.g., "```text") so the banner is no longer a language-less code fence; adjust the template lines that emit the "DO NOT MODIFY THIS FILE" banner and the opening "```" fence accordingly.packages/plugins/documentation/preview-output/showcase/models/Team.md-126-138 (1)
126-138:⚠️ Potential issue | 🟡 MinorMD028 — blank line between the description blockquote and
[!IMPORTANT]callout is flagged by markdownlintThe generator consistently emits a blank line between the section-description
> ...and the> [!IMPORTANT]callout in the Access Policies section. markdownlint treats these as a single interrupted blockquote and raises MD028 across all model pages that have access policies. (The linter misfires at line 115 inside the Mermaid fence; the real site is around line 131.)The fix is either to suppress MD028 in the generated
.markdownlint.json/.markdownlint-cli2.jsoncfor the output directory, or to restructure the template to avoid the blank separator:💡 Template fix option
-> ZenStack access control rules that determine who can read, create, update, or delete records. -> -> [!IMPORTANT] -> Operations are **denied by default**. `@@allow` rules grant access; `@@deny` rules override any allow. +> ZenStack access control rules that determine who can read, create, update, or delete records. + +> [!IMPORTANT] +> Operations are **denied by default**. `@@allow` rules grant access; `@@deny` rules override any allow.(keep the two blockquotes as separate paragraphs separated by a blank line outside the blockquote context — but note this still triggers MD028 in some linter versions; disabling the rule for generated output may be the cleaner path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/models/Team.md` around lines 126 - 138, The generated Access Policies block emits two consecutive blockquote lines with a blank line between them, triggering markdownlint rule MD028; fix by updating the generator/template that renders the "Access Policies" section (the block that outputs the description `> ...` and the `> [!IMPORTANT]` callout) so the blank line is either removed inside the blockquote or the callout is rendered as a separate paragraph outside the blockquote context, or alternatively add a suppression for MD028 in the generated output's lint config (`.markdownlint.json` or `.markdownlint-cli2.jsonc`) to ignore this rule for the output directory.
🧹 Nitpick comments (7)
packages/plugins/documentation/preview-output/showcase/procedures/getProjectStats.md (1)
44-48: Redundant Mermaid node label on second reference.The
procnode is given its label["getProjectStats"]on both lines 46 and 47. Mermaid uses the label from the first definition; the second is redundant. The second reference can use the bare node ID.♻️ Proposed cleanup
flowchart LR projectId["projectId: String"] --> proc["getProjectStats"] - proc["getProjectStats"] --> ret["ProjectStats"] + proc --> ret["ProjectStats"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/procedures/getProjectStats.md` around lines 44 - 48, The Mermaid graph repeats the label for the proc node (proc["getProjectStats"] defined twice); remove the redundant label on the second reference so the node ID proc is used without repeating the text. Edit the flow lines involving proc (the line that currently reads proc["getProjectStats"] --> ret["ProjectStats"]) and change it to use proc --> ret (or otherwise remove the second ["getProjectStats"]), leaving the first proc["getProjectStats"] definition intact and keeping node IDs projectId and ret as-is.packages/plugins/documentation/preview-output/verbose/enums/ArticleStatus.md (1)
1-7: "DO NOT MODIFY" banner renders as a visible code block.Wrapping the notice in backtick code fences causes it to appear as a prominent code block at the top of every generated page. If the intent is to hide it from rendered output, use an HTML comment instead; if it must be visible, a blockquote or admonition would be semantically cleaner.
✨ Proposed alternative — HTML comment (invisible in rendered output)
-``` -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -// Source: schema.zmodel · Generated: 2026-02-22 // -////////////////////////////////////////////////////////////////////////////////////////////// -``` +<!-- +DO NOT MODIFY THIS FILE +This file is automatically generated by ZenStack CLI and should not be manually updated. +Source: schema.zmodel · Generated: 2026-02-22 +-->🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/enums/ArticleStatus.md` around lines 1 - 7, Replace the leading triple-backtick fenced banner that contains the "DO NOT MODIFY THIS FILE" notice with a non-rendered HTML comment (or alternatively a semantic blockquote/admonition) so the notice does not appear as a visible code block; locate the top-of-file fenced block starting with ``` and the lines containing "DO NOT MODIFY THIS FILE" and swap them for an HTML comment wrapping the same text (or a markdown blockquote) to keep the message present but not rendered as a code block.packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md (1)
58-65: Mermaid flowchart re-declaresproc["searchArticles"]on every edge.Each source line redefines the same
procnode with its label:workspaceId["workspaceId: String"] --> proc["searchArticles"] query["query: String"] --> proc["searchArticles"] ...Mermaid uses the first encountered label, so subsequent redeclarations are ignored but add noise. The renderer could emit the node definition once and reference only the bare ID on subsequent edges:
♻️ Cleaner Mermaid output (to be generated by the renderer)
flowchart LR + proc["searchArticles"] workspaceId["workspaceId: String"] --> proc query["query: String"] --> proc offset["offset: Int?"] --> proc limit["limit: Int?"] --> proc - workspaceId["workspaceId: String"] --> proc["searchArticles"] - query["query: String"] --> proc["searchArticles"] - offset["offset: Int?"] --> proc["searchArticles"] - limit["limit: Int?"] --> proc["searchArticles"] proc --> ret["Article[]"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/procedures/searchArticles.md` around lines 58 - 65, The Mermaid flowchart repeatedly redeclares the proc node label proc["searchArticles"] on each edge; change the renderer to emit a single node definition for proc (e.g., proc["searchArticles"]) once and then reference the bare node ID proc on subsequent edges (use workspaceId --> proc, query --> proc, offset --> proc, limit --> proc) while keeping the return edge proc --> ret["Article[]"]; update the generator code that builds the nodes for the searchArticles procedure so it emits one proc node and reuses the proc identifier for all incoming edges (unique symbols: proc, searchArticles, workspaceId, query, offset, limit, ret).packages/plugins/documentation/README.md (1)
147-148: ClarifyerdTheme's actual default and noteerdFormat's dependency ongenerateErd.Two small clarity gaps in the options table:
erdThemedefault — the column saysdefault, butbeautiful-mermaidships 15 built-in themes and none of them is named"default". A reader may tryerdTheme = 'default'and get an error. Specifying what actually renders whenerdThemeis omitted (e.g., the library's mono/unthemed mode, or a specific fallback theme) would prevent confusion.
erdFormatdependency — the table doesn't make it clear that this option is a no-op unlessgenerateErd = true. A short parenthetical in the description column (e.g., "Only effective whengenerateErd = true") would avoid silent misconfiguration.✏️ Suggested table row updates
-| `erdFormat` | `"svg"`, `"mmd"`, or `"both"` | `"both"` | Which ERD output format(s) to produce | -| `erdTheme` | `string` | default | [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) theme name for SVG rendering | +| `erdFormat` | `"svg"`, `"mmd"`, or `"both"` | `"both"` | Which ERD output format(s) to produce. Only effective when `generateErd = true` | +| `erdTheme` | `string` | *(library default — unthemed)* | [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) theme name for SVG rendering. Valid values: `zinc-dark`, `tokyo-night`, `github-light`, etc. |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/README.md` around lines 147 - 148, Update the README options table to clarify the true default for erdTheme and the dependency of erdFormat on generateErd: replace the generic "default" with the actual fallback behavior or theme name used when erdTheme is omitted (e.g., specify the library's unthemed/mono mode or the concrete fallback theme), and append a parenthetical to the erdFormat description such as "(only effective when generateErd = true)" so readers know erdFormat is a no-op unless generateErd is enabled; reference the erdTheme and erdFormat option names and the generateErd flag when making these text edits.packages/plugins/documentation/preview-output/showcase/types/Timestamps.md (1)
44-51: "Used By" model links anchor to a field rather than the model root.Each bullet like
[Activity](../models/Activity.md#field-createdAt)links the model name directly to the#field-createdAtanchor. A reader clicking the model name likely expects to land at the top of the model page. The field-specific anchor (#field-createdAt) is already surfaced in the inline field links that follow (, [\createdAt`](...)`), so the model-name link could point to the model root instead.📝 Proposed generator-side fix (example)
-[Activity](../models/Activity.md#field-createdAt) — [`createdAt`](../models/Activity.md#field-createdAt), [`updatedAt`](../models/Activity.md#field-updatedAt) +[Activity](../models/Activity.md) — [`createdAt`](../models/Activity.md#field-createdAt), [`updatedAt`](../models/Activity.md#field-updatedAt)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/types/Timestamps.md` around lines 44 - 51, Replace the model-level links in Timestamps.md that currently point to field anchors (e.g., the `[Activity](../models/Activity.md#field-createdAt)` style links) so they link to the model root instead (e.g., `[Activity](../models/Activity.md)`); do this for all listed entries: Activity, Comment, Organization, Project, Task, Team, TeamMember, and User, while leaving the inline field links (`createdAt`, `updatedAt`) unchanged so they continue to target their `#field-createdAt`/`#field-updatedAt` anchors.packages/plugins/documentation/preview-output/showcase/procedures/bulkUpdateTaskStatus.md (1)
56-59: Replace generic reference link with procedure-specific documentation URL.Line 58 links to
/docs/category/referencewhich is generic. Use/docs/modeling/custom-procinstead for ZModel procedure syntax and declarations, which is directly relevant to this showcase file about procedures.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/showcase/procedures/bulkUpdateTaskStatus.md` around lines 56 - 59, Update the generic reference link in the "## 📚 References" section that currently points to /docs/category/reference (the "ZModel procedures — ZenStack documentation" entry) to the procedure-specific URL /docs/modeling/custom-proc so the showcase about procedures links directly to ZModel procedure syntax and declarations.packages/plugins/documentation/preview-output/verbose/models/User.md (1)
144-146:_more_ and_11_more_fieldsrenders as a literal ER diagram field, not as a truncation hintMermaid parses
_more_as the attribute type andand_11_more_fieldsas the attribute name, so diagrams display this as a real field rather than indicating truncation. Consider replacing with a Mermaid line comment (%% … and N more fields) so it doesn't appear as data in the rendered diagram, or include all fields for accuracy.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/plugins/documentation/preview-output/verbose/models/User.md` around lines 144 - 146, The ER diagram in User.md currently contains literal fields "_more_" and "and_11_more_fields" which Mermaid treats as real attributes; update the model block (around the ArticleStatus status and the placeholder fields) to remove these literal attributes and replace them with a Mermaid comment like "%% … and 11 more fields" (or alternatively expand to include the actual remaining fields) so the truncation hint does not render as data in the diagram; target the model/diagram block that defines the User (the lines with ArticleStatus status and the _more_ placeholder) when making this change.
| | Field | Related Model | Type | | ||
| | --- | --- | --- | | ||
| | `projects` | [Project](./Project.md) | One→Many | | ||
| | `tasks` | [Task](./Task.md) | One→Many | | ||
|
|
||
| ```mermaid | ||
| erDiagram | ||
| Tag { | ||
| String id PK | ||
| String name UK | ||
| String color | ||
| } | ||
| Project { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String name | ||
| String description | ||
| Boolean archived | ||
| Int taskCount | ||
| Float completionRate | ||
| Boolean hasOverdueTasks | ||
| String organizationId | ||
| _more_ and_1_more_fields | ||
| } | ||
| Task { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String title | ||
| String body | ||
| String slug | ||
| TaskStatus status | ||
| Priority priority | ||
| DateTime dueDate | ||
| Float estimatedHours | ||
| _more_ and_4_more_fields | ||
| } | ||
| Tag ||--o{ Project : "projects" | ||
| Tag ||--o{ Task : "tasks" | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "showcase.zmodel" -o -name "*.zmodel" | head -20Repository: zenstackhq/zenstack
Length of output: 1218
🏁 Script executed:
cat ./packages/plugins/documentation/zenstack/showcase.zmodelRepository: zenstackhq/zenstack
Length of output: 11117
Fix relationship cardinality: Tag ↔ Task and Tag ↔ Project should be Many→Many, not One→Many
The schema uses implicit many-to-many relationships: both Task and Project declare tags Tag[] without explicit FK fields, and Tag declares tasks Task[] and projects Project[] without explicit FKs. This creates implicit join tables in Prisma. Update the table labels from One→Many to Many→Many and change the Mermaid diagram cardinality from ||--o{ to }o--o{ for both relationships.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/showcase/models/Tag.md` around
lines 56 - 96, Update the Tag relationships to reflect Many→Many: change the
table labels for the `projects` and `tasks` fields from "One→Many" to
"Many→Many" and update the Mermaid ER diagram edges that currently use `||--o{`
to use `}o--o{` for both `Tag ||--o{ Project : "projects"` and `Tag ||--o{ Task
: "tasks"` lines so they read `Tag }o--o{ Project : "projects"` and `Tag }o--o{
Task : "tasks"`, matching the implicit many-to-many schema where `Task.tags` and
`Project.tags` are arrays and `Tag.tasks`/`Tag.projects` are arrays.
| ```mermaid | ||
| erDiagram | ||
| Task { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id PK | ||
| String title | ||
| String body | ||
| String slug | ||
| TaskStatus status | ||
| Priority priority | ||
| DateTime dueDate | ||
| Float estimatedHours | ||
| Int commentCount | ||
| String projectId FK | ||
| String assigneeId FK | ||
| String parentId FK | ||
| } | ||
| Activity { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String action | ||
| String detail | ||
| String userId | ||
| String taskId | ||
| } | ||
| User { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String email | ||
| String name | ||
| String avatarUrl | ||
| Role role | ||
| Int taskCount | ||
| String organizationId | ||
| } | ||
| Task { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String title | ||
| String body | ||
| String slug | ||
| TaskStatus status | ||
| Priority priority | ||
| DateTime dueDate | ||
| Float estimatedHours | ||
| _more_ and_4_more_fields | ||
| } | ||
| Comment { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String body | ||
| String taskId | ||
| String authorId | ||
| } | ||
| Project { | ||
| DateTime createdAt | ||
| DateTime updatedAt | ||
| String id | ||
| String name | ||
| String description | ||
| Boolean archived | ||
| Int taskCount | ||
| Float completionRate | ||
| Boolean hasOverdueTasks | ||
| String organizationId | ||
| _more_ and_1_more_fields | ||
| } | ||
| Tag { | ||
| String id | ||
| String name | ||
| String color | ||
| } | ||
| Task ||--o{ Activity : "activities" | ||
| Task }o--|| User : "assignee" | ||
| Task ||--o{ Task : "children" | ||
| Task ||--o{ Comment : "comments" | ||
| Task }o--|| Project : "project" | ||
| Task ||--o{ Tag : "tags" | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, check if the file exists and read the relevant lines
if [ -f "packages/plugins/documentation/preview-output/showcase/models/Task.md" ]; then
echo "=== File exists. Checking lines 114-197 ==="
sed -n '114,197p' "packages/plugins/documentation/preview-output/showcase/models/Task.md"
else
echo "File not found at packages/plugins/documentation/preview-output/showcase/models/Task.md"
# Try to find similar files
find . -name "Task.md" -type f 2>/dev/null | head -10
fiRepository: zenstackhq/zenstack
Length of output: 1949
🏁 Script executed:
#!/bin/bash
# Run the verification script from the review comment to find duplicate entity definitions
python3 - <<'EOF'
import re, pathlib, sys
issues = []
for md in pathlib.Path("packages/plugins/documentation/preview-output").rglob("*.md"):
text = md.read_text()
for block in re.findall(r"```mermaid\nerDiagram\n(.*?)```", text, re.DOTALL):
entities = re.findall(r"^\s+(\w+)\s*\{", block, re.MULTILINE)
seen = set()
for e in entities:
if e in seen:
issues.append(f"{md}: duplicate entity '{e}'")
seen.add(e)
if issues:
print("\n".join(issues))
else:
print("No duplicates found")
EOFRepository: zenstackhq/zenstack
Length of output: 257
🏁 Script executed:
#!/bin/bash
# Check Comment.md to confirm similar pattern
if [ -f "packages/plugins/documentation/preview-output/verbose/models/Comment.md" ]; then
echo "=== Comment.md mermaid diagram ==="
grep -A 100 '```mermaid' "packages/plugins/documentation/preview-output/verbose/models/Comment.md" | head -120
else
echo "File not found"
fiRepository: zenstackhq/zenstack
Length of output: 2522
Duplicate entity definitions in Mermaid ERD blocks for self-referential models
The Mermaid erDiagram block defines the Task entity twice — once for the entity being documented (lines 116–131) and again as a "related entity" (lines 152–164) because Task relates to itself via parentId/children. Some Mermaid renderers silently ignore the second definition; others may error or produce garbled output.
This issue affects multiple self-referential models across the documentation (confirmed in both Task.md and Comment.md). The ERD renderer's "collect related entities" logic should exclude the entity being documented from the related-entity list to prevent this duplication.
Expected corrected diagram structure
erDiagram
Task {
...
String parentId FK
}
Activity { ... }
User { ... }
- Task {
- DateTime createdAt
- ...
- _more_ and_4_more_fields
- }
Comment { ... }
Project { ... }
Tag { ... }
Task ||--o{ Activity : "activities"
...
Task ||--o{ Task : "children"🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 131-131: Blank line inside blockquote
(MD028, no-blanks-blockquote)
[warning] 138-138: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/showcase/models/Task.md` around
lines 114 - 197, The Mermaid ERD includes the Task entity twice because the
"collect related entities" logic adds related models even when they are the same
as the current model; update that logic to skip the current entity when building
the related-entity list so you only render one Task block in the erDiagram.
Locate the code that generates the Mermaid erDiagram for Task (the routine that
collects related entities for Task.md and Comment.md) and add a guard that
compares relatedEntity.name (or id) against the currentModel.name (or id) and
excludes matches, then regenerate the diagram so self-referential relations
(parentId/children) produce relation lines without re-defining the entity.
|
|
||
| ## 📚 References | ||
|
|
||
| - [ZModel procedures — ZenStack documentation](https://zenstack.dev/docs/category/reference) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
https://zenstack.dev/docs/category/reference ZenStack ZModel procedures documentation
💡 Result:
ZenStack Custom Procedures are a ZModel-only feature (not in Prisma Schema Language) that let you declare reusable, type-safe routines in your schema and then implement/invoke them through the ORM client. They’re preview and available since v3.2.0. [1][2]
1) Define procedures in ZModel
- Query procedure:
procedure getUserFeeds(userId: Int, limit: Int?) : Post[]
- Mutation procedure (writes):
mutation procedure signUp(email: String) : User
You can use ZModel-supported types for params/returns (primitives, models, enums, custom types, arrays). Optional params use ?. Use Void for no return. [1]
2) Implement procedures in the ORM client
When constructing ZenStackClient, provide an implementation for each procedure under procedures, receiving { client, args }. Args are runtime-validated against the ZModel signature. [2]
3) Call procedures
Procedures are exposed under db.$procs and invoked with { args: ... }:
await db.$procs.signUp({ args: { email: 'alice@example.com' } });[2]
Docs pages:
https://zenstack.dev/docs/modeling/custom-proc
https://zenstack.dev/docs/orm/custom-proc
Update the reference link to point to the actual procedures documentation.
The link https://zenstack.dev/docs/category/reference does not exist. The correct documentation URLs for ZModel procedures are:
- https://zenstack.dev/docs/modeling/custom-proc (modeling perspective)
- https://zenstack.dev/docs/orm/custom-proc (ORM client perspective)
Update the link in the file to one of these correct URLs to prevent broken references in generated documentation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/showcase/procedures/signUp.md`
at line 61, Replace the broken reference URL in
packages/plugins/documentation/preview-output/showcase/procedures/signUp.md:
change the existing link text "https://zenstack.dev/docs/category/reference" to
one of the valid ZModel procedures docs (either
"https://zenstack.dev/docs/modeling/custom-proc" for the modeling perspective or
"https://zenstack.dev/docs/orm/custom-proc" for the ORM client perspective) so
the link points to the correct procedures documentation.
| | `ARCHIVED` | Removed from public listings but still accessible via direct URL. | ||
| Can be moved back to DRAFT for revision. | |
There was a problem hiding this comment.
Multi-line table cell will break table rendering for ARCHIVED.
GFM/CommonMark tables require each row to occupy a single line. The newline inside the ARCHIVED description splits the row: the first half becomes a malformed cell and Can be moved back to DRAFT for revision. | is emitted as a paragraph below the table, leaving the ARCHIVED entry with a truncated description. The LanguageTool fragment-sentence hint is a downstream symptom of the same structural break.
🐛 Proposed fix — collapse the cell onto one line
-| `ARCHIVED` | Removed from public listings but still accessible via direct URL.
-Can be moved back to DRAFT for revision. |
+| `ARCHIVED` | Removed from public listings but still accessible via direct URL. It can be moved back to DRAFT for revision. |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | `ARCHIVED` | Removed from public listings but still accessible via direct URL. | |
| Can be moved back to DRAFT for revision. | | |
| | `ARCHIVED` | Removed from public listings but still accessible via direct URL. It can be moved back to DRAFT for revision. | |
🧰 Tools
🪛 LanguageTool
[style] ~46-~46: To form a complete sentence, be sure to include a subject.
Context: ...gs but still accessible via direct URL. Can be moved back to DRAFT for revision. | ...
(MISSING_IT_THERE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/verbose/enums/ArticleStatus.md`
around lines 46 - 47, The table row for the `ARCHIVED` enum contains a newline
inside the cell which breaks GFM tables; open ArticleStatus.md, find the
`ARCHIVED` table row and collapse the description into a single line (replace
the internal newline with a space) so the entire row is on one line and the
trailing pipe remains intact, ensuring the `ARCHIVED` entry renders correctly.
| | <a id="field-sortOrder"></a>`sortOrder` | `Int` | Yes | `0` | — | — | Ordering weight for display. Lower values appear first. | ||
| Categories with equal weight are sorted alphabetically. | |
There was a problem hiding this comment.
Multi-line table cell breaks GFM table rendering — fix the renderer.
The sortOrder description text wraps to the next line without a leading |. GitHub Flavored Markdown tables require all cell content to be on a single line; the continuation row is treated as a paragraph, corrupting the sortOrder row and every row below it in the table.
The same defect appears in Invoice.md (lines 79–80, 84–85) and Notification.md (lines 77–78), confirming this is a systematic generator issue. The renderer that builds field-description table cells must collapse or escape embedded newlines before writing them.
✏️ Proposed renderer fix
-| `sortOrder` | `Int` | Yes | `0` | — | — | Ordering weight for display. Lower values appear first.
-Categories with equal weight are sorted alphabetically. |
+| `sortOrder` | `Int` | Yes | `0` | — | — | Ordering weight for display. Lower values appear first. Categories with equal weight are sorted alphabetically. |In the renderer, replace \n (and \r\n) in the description string with a space (or <br>) before interpolating into a table row:
-const desc = field.description ?? '—';
+const desc = (field.description ?? '—').replace(/\r?\n/g, ' ');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | <a id="field-sortOrder"></a>`sortOrder` | `Int` | Yes | `0` | — | — | Ordering weight for display. Lower values appear first. | |
| Categories with equal weight are sorted alphabetically. | | |
| | <a id="field-sortOrder"></a>`sortOrder` | `Int` | Yes | `0` | — | — | Ordering weight for display. Lower values appear first. Categories with equal weight are sorted alphabetically. | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/verbose/models/Category.md`
around lines 72 - 73, The table renderer is outputting multi-line field
descriptions which breaks GFM tables (seen for `sortOrder` in Category.md and
similar in Invoice.md and Notification.md); update the field-description table
rendering code (the function responsible for building table rows, e.g., the
renderFieldDescription/buildTableRow logic) to normalize embedded newlines by
replacing "\r\n" and "\n" with a single space (or an explicit "<br>") before
interpolating the description into the table cell so each table row remains a
single line and GFM rendering is preserved.
| | <a id="field-body"></a>`body` | `String` | Yes | — | `@length(1, 5000)` | — | Comment body in Markdown format. Supports inline formatting | ||
| and `@mention` syntax for notifying other users. | |
There was a problem hiding this comment.
Generator bug: newlines in field descriptions break Markdown table rendering
The body field description crosses a line boundary inside a table cell. GFM (and virtually all compliant Markdown parsers) do not support multiline table cells — the bare newline on line 82 is parsed as the start of a new table row, producing a malformed row with only one cell.
The same defect appears in verbose/models/Workspace.md (lines 78–81, 83–84) and verbose/models/AuditLog.md (lines 76–77).
The renderer that builds the Fields table must strip or collapse embedded newlines in cell content before emitting the table row, e.g.:
-description.trim()
+description.replace(/\n/g, ' ').trim()🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 81-81: Table pipe style
Expected: leading_and_trailing; Actual: leading_only; Missing trailing pipe
(MD055, table-pipe-style)
[warning] 82-82: Table pipe style
Expected: leading_and_trailing; Actual: trailing_only; Missing leading pipe
(MD055, table-pipe-style)
[warning] 82-82: Table column count
Expected: 7; Actual: 1; Too few cells, row will be missing data
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/verbose/models/Comment.md`
around lines 81 - 82, The Markdown table for model fields is broken because
field descriptions (e.g., the `body` field in Comment.md and similar entries in
Workspace.md and AuditLog.md) contain raw newlines that produce invalid GFM
table rows; update the fields-table rendering logic (the renderer that emits the
"Fields" table / function responsible for generating model field rows, e.g., the
Fields table renderer or generateFieldsTable/renderFieldsTable) to sanitize cell
text by collapsing or replacing embedded newlines (trim and replace "\n" with a
single space) and escape any remaining problematic characters before emitting
each table row so descriptions stay on a single line in the generated table.
| | Operation | Rule | Effect | | ||
| | --- | --- | --- | | ||
| | read | `article.status == 'PUBLISHED' || workspace.members?[user == auth()]` | Allow | | ||
| | create | `workspace.members?[user == auth()]` | Allow | | ||
| | update | `author == auth() && deletedAt == null` | Allow | | ||
| | delete | `true` | Deny | | ||
|
|
There was a problem hiding this comment.
Generator bug: unescaped | in policy expressions corrupts the Access Policies table
The policy expression on line 187:
`article.status == 'PUBLISHED' || workspace.members?[user == auth()]`
contains ||. Even inside a Markdown inline code span (backticks), GFM-compatible parsers treat | as a column separator. The linter confirms this: it sees 5 columns instead of 3, breaking the row.
The same defect appears in verbose/models/Workspace.md (lines 148–149: members?[role == 'OWNER' || role == 'ADMIN']).
The renderer must escape pipe characters in table cell content:
-cell content
+cell.replace(/\|/g, '\\|')🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 187-187: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/verbose/models/Comment.md`
around lines 185 - 191, The markdown table cells containing expressions with
pipe characters (e.g. the code spans `article.status == 'PUBLISHED' ||
workspace.members?[user == auth()]` in Comment.md and `members?[role == 'OWNER'
|| role == 'ADMIN']` in Workspace.md) are being split by GFM's table parser; fix
by escaping the pipe characters inside the inline code (replace `||` with `\|\|`
or use HTML entity `||`) or render the expression with an HTML <code>
wrapper so the cell is treated as a single column, updating the Access Policies
table rows that contain those expressions.
| | <a id="field-type"></a>`type` | `String` | Yes | — | `@length(1, 80)` | — | Example: `comment.created` The notification type this preference applies to. | ||
| Use `*` as a wildcard to set defaults for all types. | |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's check if the files exist
echo "=== Checking files existence ==="
ls -la packages/plugins/documentation/preview-output/verbose/models/NotificationPreference.md 2>/dev/null && echo "✓ NotificationPreference.md exists" || echo "✗ NotificationPreference.md not found"
ls -la packages/plugins/documentation/preview-output/verbose/models/WorkspaceRole.md 2>/dev/null && echo "✓ WorkspaceRole.md exists" || echo "✗ WorkspaceRole.md not found"
ls -la packages/plugins/documentation/preview-output/verbose/models/Subscription.md 2>/dev/null && echo "✓ Subscription.md exists" || echo "✗ Subscription.md not found"
# Check directory structure
echo -e "\n=== Directory structure ==="
find packages/plugins/documentation/preview-output -type f -name "*.md" | head -20Repository: zenstackhq/zenstack
Length of output: 1980
🏁 Script executed:
#!/bin/bash
# Run the suggested diagnostic to find broken table rows
echo "=== Finding lines with bare line-breaks in table cells ==="
rg --type md -n '^\| [^|]+$' packages/plugins/documentation/preview-output/ | head -50Repository: zenstackhq/zenstack
Length of output: 121
🏁 Script executed:
#!/bin/bash
# Examine the specific files and lines mentioned
echo "=== NotificationPreference.md lines 65-75 ==="
sed -n '65,75p' packages/plugins/documentation/preview-output/verbose/models/NotificationPreference.md 2>/dev/null || echo "File not found"
echo -e "\n=== WorkspaceRole.md lines 38-55 ==="
sed -n '38,55p' packages/plugins/documentation/preview-output/verbose/models/WorkspaceRole.md 2>/dev/null || echo "File not found"
echo -e "\n=== Subscription.md lines 83-105 ==="
sed -n '83,105p' packages/plugins/documentation/preview-output/verbose/models/Subscription.md 2>/dev/null || echo "File not found"Repository: zenstackhq/zenstack
Length of output: 4158
🏁 Script executed:
#!/bin/bash
# Search for WorkspaceRole.md anywhere in the codebase
echo "=== Searching for WorkspaceRole.md ==="
find packages/plugins/documentation/preview-output -name "*WorkspaceRole*" -o -name "*workspace-role*" 2>/dev/null | head -20
# Also check what other enum/model files exist in the output
echo -e "\n=== All model files in verbose/models ==="
ls -1 packages/plugins/documentation/preview-output/verbose/models/ 2>/dev/null
echo -e "\n=== All enum files in verbose/enums ==="
ls -1 packages/plugins/documentation/preview-output/verbose/enums/ 2>/dev/nullRepository: zenstackhq/zenstack
Length of output: 539
🏁 Script executed:
#!/bin/bash
# Verify more instances of the multiline issue
echo "=== Scanning for ALL lines not starting with | inside tables ==="
python3 <<'PYEOF'
import re
files = [
"packages/plugins/documentation/preview-output/verbose/models/NotificationPreference.md",
"packages/plugins/documentation/preview-output/verbose/models/Subscription.md"
]
for filepath in files:
try:
with open(filepath, 'r') as f:
lines = f.readlines()
in_table = False
prev_line_is_table = False
for i, line in enumerate(lines, 1):
stripped = line.rstrip('\n')
# Detect table header/separator
if stripped.startswith('|') and '---' in stripped:
in_table = True
continue
# Check if we're in a table and current line doesn't start with |
if in_table and stripped and not stripped.startswith('|'):
# This is a malformed continuation line
print(f"{filepath}:{i} [BROKEN ROW]")
print(f" Content: {stripped[:100]}")
continue
# If we see a non-table line, exit table mode
if in_table and stripped and not stripped.startswith('|'):
in_table = False
except FileNotFoundError:
pass
PYEOFRepository: zenstackhq/zenstack
Length of output: 16382
🏁 Script executed:
#!/bin/bash
# Check the WorkspaceRole.md enum file
echo "=== WorkspaceRole.md lines 38-55 ==="
sed -n '38,55p' packages/plugins/documentation/preview-output/verbose/enums/WorkspaceRole.md
echo -e "\n=== Full WorkspaceRole.md to see structure ==="
cat packages/plugins/documentation/preview-output/verbose/enums/WorkspaceRole.mdRepository: zenstackhq/zenstack
Length of output: 3082
🏁 Script executed:
#!/bin/bash
# Find the renderer code that generates these markdown files
echo "=== Searching for documentation plugin renderer ==="
find . -type f \( -name "*.ts" -o -name "*.js" \) -path "*/plugins/documentation*" | grep -v node_modules | grep -v dist | head -20
echo -e "\n=== Looking for template or rendering logic ==="
rg "description" packages/plugins/documentation --type ts -l | head -10Repository: zenstackhq/zenstack
Length of output: 1957
🏁 Script executed:
#!/bin/bash
# Check the model-page renderer which generates the field tables
echo "=== model-page.ts renderer ==="
cat packages/plugins/documentation/src/renderers/model-page.ts | head -100
echo -e "\n=== Searching for description rendering in model-page ==="
rg "description" packages/plugins/documentation/src/renderers/model-page.ts -A 3 -B 1Repository: zenstackhq/zenstack
Length of output: 4331
🏁 Script executed:
#!/bin/bash
# Check the enum-page renderer which generates the enum value tables
echo "=== enum-page.ts renderer ==="
rg "description" packages/plugins/documentation/src/renderers/enum-page.ts -A 3 -B 1Repository: zenstackhq/zenstack
Length of output: 372
🏁 Script executed:
#!/bin/bash
# Check common.ts for shared rendering utilities
echo "=== common.ts utilities ==="
rg "table\|description\|<br>" packages/plugins/documentation/src/renderers/common.ts -A 2 -B 1 | head -50Repository: zenstackhq/zenstack
Length of output: 90
🏁 Script executed:
#!/bin/bash
# Get the full model-page.ts file to find description rendering
wc -l packages/plugins/documentation/src/renderers/model-page.tsRepository: zenstackhq/zenstack
Length of output: 125
🏁 Script executed:
#!/bin/bash
# Search for where field descriptions are added to table rows
echo "=== Looking for field description rendering in model-page ==="
sed -n '200,350p' packages/plugins/documentation/src/renderers/model-page.ts
echo -e "\n=== Searching for table row rendering ==="
rg "^\| " packages/plugins/documentation/src/renderers/model-page.ts -B 3 -A 1Repository: zenstackhq/zenstack
Length of output: 6291
🏁 Script executed:
#!/bin/bash
# Check enum-page.ts for how it renders field values in table rows
echo "=== Full enum-page.ts rendering logic ==="
cat packages/plugins/documentation/src/renderers/enum-page.tsRepository: zenstackhq/zenstack
Length of output: 4089
🏁 Script executed:
#!/bin/bash
# Get the full model-page.ts file focusing on field rendering
echo "=== Complete model-page.ts ==="
cat packages/plugins/documentation/src/renderers/model-page.tsRepository: zenstackhq/zenstack
Length of output: 16618
Fix multiline field descriptions breaking Markdown table rendering in the documentation renderer
The Fields and Values tables in generated documentation break when descriptions contain newlines. Markdown parsers treat the continuation lines as malformed rows (missing the leading | pipe).
This affects:
NotificationPreference.mdline 70:typefield descriptionWorkspaceRole.mdenum (lines 43–51): all four value descriptions (OWNER,ADMIN,MEMBER,VIEWER)Subscription.mdlines 89, 94, 96, 98, 100: five field descriptions (priceAmountCents,currentPeriodEnd,trialEndsAt,canceledAt,stripeSubscriptionId)
Root cause: renderFieldsSection() in model-page.ts (line 215) and renderValuesSection() in enum-page.ts (line 50) insert field/enum descriptions directly into table cells without escaping newlines. Markdown requires <br> instead.
Fix: Replace \n with <br> when building description strings before inserting them into table rows in both renderers.
Example: fixing a multiline field description
- | `type` | `String` | Yes | — | `@length(1, 80)` | — | Example: `comment.created` The notification type this preference applies to.
- Use `*` as a wildcard to set defaults for all types. |
+ | `type` | `String` | Yes | — | `@length(1, 80)` | — | Example: `comment.created` The notification type this preference applies to.<br>Use `*` as a wildcard to set defaults for all types. |🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 70-70: Table pipe style
Expected: leading_and_trailing; Actual: leading_only; Missing trailing pipe
(MD055, table-pipe-style)
[warning] 71-71: Table pipe style
Expected: leading_and_trailing; Actual: trailing_only; Missing leading pipe
(MD055, table-pipe-style)
[warning] 71-71: Table column count
Expected: 7; Actual: 1; Too few cells, row will be missing data
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/plugins/documentation/preview-output/verbose/models/NotificationPreference.md`
around lines 70 - 71, Field/enum descriptions containing newlines are breaking
generated Markdown tables because renderFieldsSection and renderValuesSection
insert raw text into table cells; update renderFieldsSection (function) and
renderValuesSection (function) to replace newline characters with HTML line
breaks (replace "\n" with "<br>") before assembling the table row strings so
each multiline description becomes a single table cell; ensure this
transformation is applied to the description variable used when constructing the
Markdown table rows in model-page.ts and enum-page.ts.
| | <a id="field-primaryEmail"></a>`primaryEmail` | `String` | Yes | — | `@unique`, `@email` | — | Example: `alex.chen@example.com` Primary email address used for authentication and notifications. | ||
| Must be verified before the account is fully activated. | | ||
| | <a id="field-displayName"></a>`displayName` | `String` | Yes | — | `@length(1, 120)` | — | Example: `Alex Chen` User-facing display name shown in the UI. Can contain spaces, | ||
| emoji, and Unicode characters. | | ||
| | <a id="field-handle"></a>`handle` | `String` | Yes | — | `@unique`, `@length(3, 30)`, `@regex('^[a-z0-9][a-z0-9-]*[a-z0-9]$')`, `@lower` | — | Example: `alex-chen` URL-safe unique handle for mentions and profile URLs. | ||
| Lowercase alphanumeric and hyphens only. Immutable after first set. | | ||
| | <a id="field-avatarUrl"></a>`avatarUrl` | `String?` | No | — | `@url` | — | URL to the user's profile avatar. Served via CDN with automatic | ||
| resizing. Falls back to Gravatar if not set. | |
There was a problem hiding this comment.
Multiline description text breaks Markdown table rows — generator bug
Lines 85–86, 87–88, 89–90, and 91–92 each span two physical lines because the description contains a literal newline. Standard Markdown table parsers require each row to occupy exactly one line; the continuation line is rendered as a detached 1-cell row, corrupting the table in every Markdown renderer (GitHub, MkDocs, VS Code preview, etc.). The MD056 "too many cells" warning on line 199 is a cascading parse artefact.
Root cause is in the generator's table-cell rendering logic. Newlines in description strings must be escaped before being inserted into a | … | row:
🐛 Generator fix (table cell renderer)
- function renderTableCell(text: string): string {
- return text;
- }
+ function renderTableCell(text: string): string {
+ // Markdown table rows cannot span multiple lines
+ return text.replace(/\n+/g, ' '); // or '<br>' for explicit line-breaks
+ }🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 85-85: Table pipe style
Expected: leading_and_trailing; Actual: leading_only; Missing trailing pipe
(MD055, table-pipe-style)
[warning] 86-86: Table pipe style
Expected: leading_and_trailing; Actual: trailing_only; Missing leading pipe
(MD055, table-pipe-style)
[warning] 86-86: Table column count
Expected: 7; Actual: 1; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 87-87: Table pipe style
Expected: leading_and_trailing; Actual: leading_only; Missing trailing pipe
(MD055, table-pipe-style)
[warning] 88-88: Table pipe style
Expected: leading_and_trailing; Actual: trailing_only; Missing leading pipe
(MD055, table-pipe-style)
[warning] 88-88: Table column count
Expected: 7; Actual: 1; Too few cells, row will be missing data
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/plugins/documentation/preview-output/verbose/models/User.md` around
lines 85 - 92, The Markdown table renderer is inserting raw description strings
containing literal newlines into a single `| ... |` row, which breaks table
parsing; update the generator's table-cell rendering logic that emits the
"description" cell (the code path that builds the
`primaryEmail/displayName/handle/avatarUrl` row content) to escape or normalize
newline characters before writing the cell (e.g., replace `\n` with a safe
single-line representation such as a space or HTML `<br>` or `\n`-escaped
sequence) so each table row is emitted as one physical line and no continuation
lines are produced.
0b3e82c to
0447256
Compare
Automatically generate rich, browsable Markdown documentation from ZModel schemas. Every model, view, enum, type, and procedure gets its own page with fields, relationships, access policies, validation rules, Mermaid diagrams, and cross-links. Key capabilities: - Full-schema ERD generation with SVG export via beautiful-mermaid - Per-page SVG diagrams (diagramFormat: 'svg' | 'both' | 'mermaid') - AI agent skill file generation (SKILL.md) - 15 built-in themes for SVG rendering - Multi-file schema support with source file tracking - Configurable sections: policies, validation, indexes, relationships
0447256 to
8f60c7a
Compare
| if (field.type.reference?.ref && isDataModel(field.type.reference.ref)) { | ||
| const to = field.type.reference.ref.name; | ||
| let relType: RelationType; | ||
| if (field.type.array) { |
There was a problem hiding this comment.
It seems one-to-one and many-to-many (implicit ones) relations are not handled?
Summary
Adds
@zenstackhq/plugin-documentation— a ZenStack plugin that automatically generates rich, browsable Markdown documentation from ZModel schemas.What it generates
Every model, view, enum, type, and procedure gets its own documentation page with:
@@allow/@@denyrules in a readable table@email,@length,@regex, etc.@@index,@@uniqueconstraintsERD SVG Export
Set
generateErd = trueto produce a complete Entity Relationship Diagram covering all models with PK/FK/UK annotations and relationship connectors, rendered to SVG via beautiful-mermaid. 15 built-in themes available.Per-Page SVG Diagrams
Set
diagramFormat = 'svg'to render all per-page Mermaid diagrams (model ER, enum class, type class, procedure flowchart, relationships ER) as SVG images instead of inline Mermaid code blocks. This makes diagrams visible in any Markdown viewer — including Notion, plain text viewers, and GitHub — without requiring Mermaid rendering support.diagramFormat"mermaid"(default)"svg""both"AI Agent Skill (SKILL.md)
Set
generateSkill = trueto produce aSKILL.mdoptimized for LLM consumption — information-dense, consistent structure, compatible with the skills.sh ecosystem.Example output
Browse the generated preview output committed to this PR:
Test plan