mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
docs: constrain package README summaries
This commit is contained in:
@@ -172,6 +172,12 @@ describe('gate graph validation', () => {
|
||||
expect(ids).toContain('subsystem-pages')
|
||||
})
|
||||
|
||||
it('keeps the package README Summary limit in the documentation gate', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
||||
|
||||
expect(ids).toContain('package-readme-summaries')
|
||||
})
|
||||
|
||||
it('derives the quick documentation aggregate from marked doc-sync leaves', () => {
|
||||
const full = withPnpmEntrypoint(() => gatesForMode('doc-sync'))
|
||||
const quick = withPnpmEntrypoint(() => gatesForMode('doc-quick'))
|
||||
|
||||
@@ -747,6 +747,7 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }),
|
||||
pnpmScript('tsconfig-paths', 'verify-tsconfig-paths', { label: 'tsconfig paths' }),
|
||||
pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }),
|
||||
pnpmScript('package-readme-summaries', 'verify-package-readme-summaries', { label: 'package README Summaries', quick: true }),
|
||||
pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience', quick: true }),
|
||||
pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification', quick: true }),
|
||||
pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format', quick: true }),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
MAX_PACKAGE_README_SUMMARY_WORDS,
|
||||
packageReadmeSummaryErrors,
|
||||
} from './verify-package-readme-summaries.ts'
|
||||
|
||||
function readme(summary: string, kind = 'package-reference'): string {
|
||||
return `---\nkind: "${kind}"\n---\n# Example\n\n## Summary\n\n${summary}\n\n## Table of Contents\n`
|
||||
}
|
||||
|
||||
describe('package README Summary limit', () => {
|
||||
it('accepts exactly 100 whitespace-delimited words', () => {
|
||||
const summary = Array.from({ length: MAX_PACKAGE_README_SUMMARY_WORDS }, () => 'word').join(' ')
|
||||
|
||||
expect(packageReadmeSummaryErrors('packages/example/example/README.md', readme(summary))).toEqual([])
|
||||
})
|
||||
|
||||
it.each([
|
||||
'package-group',
|
||||
'package-reference',
|
||||
'package-library',
|
||||
'package-bundle',
|
||||
])('rejects 101 words and directs the author to the skill and %s template', (kind) => {
|
||||
const summary = Array.from({ length: MAX_PACKAGE_README_SUMMARY_WORDS + 1 }, () => 'word').join(' ')
|
||||
|
||||
expect(packageReadmeSummaryErrors('packages/example/example/README.md', readme(summary, kind))).toEqual([
|
||||
`packages/example/example/README.md: Summary has 101 words; the limit is 100. Read .agents/skills/dsh-doc/SKILL.md and .agents/skills/dsh-doc/templates/${kind}.md before rewriting it.`,
|
||||
])
|
||||
})
|
||||
|
||||
it('counts only the Summary body', () => {
|
||||
const laterSection = Array.from({ length: 101 }, () => 'detail').join(' ')
|
||||
|
||||
expect(packageReadmeSummaryErrors(
|
||||
'packages/example/example/README.md',
|
||||
`${readme('Short summary.')}\n${laterSection}`,
|
||||
)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects a missing Summary instead of silently narrowing the corpus', () => {
|
||||
expect(packageReadmeSummaryErrors(
|
||||
'packages/example/example/README.md',
|
||||
'---\nkind: "package-reference"\n---\n# Example\n',
|
||||
)).toEqual(['packages/example/example/README.md: missing `## Summary`'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
/** Enforce the English package README Summary entry-length limit. */
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** Maximum `wc -w`-style length of an English package README Summary. */
|
||||
export const MAX_PACKAGE_README_SUMMARY_WORDS = 100
|
||||
|
||||
const PACKAGE_README_PATTERNS = [
|
||||
'packages/README.md',
|
||||
'packages/*/README.md',
|
||||
'packages/*/*/README.md',
|
||||
] as const
|
||||
|
||||
/** `wc -w` equivalent used by the documentation budget gate. */
|
||||
function countWords(text: string): number {
|
||||
return text.split(/\s+/u).filter(Boolean).length
|
||||
}
|
||||
|
||||
/** Extract one H2 section body without consuming the next H2. */
|
||||
function h2Body(source: string, heading: string): string | undefined {
|
||||
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
|
||||
const match = new RegExp(`^## ${escaped}\\s*\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'mu').exec(source)
|
||||
return match?.[1]?.trim()
|
||||
}
|
||||
|
||||
/** Read the package README kind for a diagnostic template link. */
|
||||
function readKind(source: string): string | undefined {
|
||||
return /^kind:\s*["']?([a-z-]+)["']?\s*$/mu.exec(source)?.[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Report Summary length violations for one English package README.
|
||||
* @param file - Repository-relative README path.
|
||||
* @param source - Complete README source.
|
||||
* @returns Diagnostics for a missing or oversized Summary.
|
||||
*/
|
||||
export function packageReadmeSummaryErrors(file: string, source: string): string[] {
|
||||
const summary = h2Body(source, 'Summary')
|
||||
if (summary === undefined) return [`${file}: missing \`## Summary\``]
|
||||
|
||||
const words = countWords(summary)
|
||||
if (words <= MAX_PACKAGE_README_SUMMARY_WORDS) return []
|
||||
|
||||
const kind = readKind(source)
|
||||
const template = kind === undefined
|
||||
? '.agents/skills/dsh-doc/templates/'
|
||||
: `.agents/skills/dsh-doc/templates/${kind}.md`
|
||||
return [
|
||||
`${file}: Summary has ${String(words)} words; the limit is ${String(MAX_PACKAGE_README_SUMMARY_WORDS)}. Read .agents/skills/dsh-doc/SKILL.md and ${template} before rewriting it.`,
|
||||
]
|
||||
}
|
||||
|
||||
/** Find every authored English package README covered by the kind templates. */
|
||||
function packageReadmes(): string[] {
|
||||
return PACKAGE_README_PATTERNS
|
||||
.flatMap(pattern => globSync(pattern, { cwd: root, exclude: ['**/node_modules/**'] }))
|
||||
.map(file => file.replaceAll('\\', '/'))
|
||||
.sort()
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const files = packageReadmes()
|
||||
const failures = files.length === 0
|
||||
? ['no English package READMEs found; the scan is empty or narrowed']
|
||||
: files.flatMap(file => packageReadmeSummaryErrors(file, readFileSync(resolve(root, file), 'utf8')))
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('verify-package-readme-summaries: violations found:')
|
||||
for (const failure of failures) console.error(` ${failure}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`verify-package-readme-summaries: ${String(files.length)} English package README Summaries are within ${String(MAX_PACKAGE_README_SUMMARY_WORDS)} words.`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user