tidaldb/.sdlc/tools/quality-check/tool.ts
jordan 5ceef74f3b chore: bootstrap SDLC state machine for tidalDB
- Initialize .sdlc/ with config, guidance, and state machine
- Register M0-M8 as released milestones (full engine track history)
- Seed M9 (Community Sync & Revocation) and M10 (Governance & Agent Rights) with features
- Seed product milestones P0-P4 and PG1 gate with features from existing planning docs
- Add Team section to AGENTS.md (tidal-engineer, tidal-visionary, tidal-researcher, tidal-storyteller)
- Add knowledge-librarian agent for .sdlc/knowledge/ curation
- Gitignore .sdlc/telemetry.redb (volatile binary state)
- Add .ai/ scaffold (project knowledge index)
2026-03-03 00:41:41 -07:00

295 lines
8.4 KiB
TypeScript

/**
* Quality Check
* =============
* Runs checks defined in .sdlc/tools/quality-check/config.yaml and reports pass/fail.
*
* WHAT IT DOES
* ------------
* --run: Reads JSON from stdin: { "scope"?: "string" }
* Loads checks from .sdlc/tools/quality-check/config.yaml.
* Runs each check's script as a shell command, records pass/fail + output.
* If scope is provided, only runs checks whose name matches the filter string.
* Returns ToolResult<{ passed, failed, checks[] }>.
*
* --meta: Writes ToolMeta JSON to stdout. Used by `sdlc tool sync`.
*
* WHAT IT READS
* -------------
* - .sdlc/tools/quality-check/config.yaml
* → checks[]: { name, description, script }
*
* WHAT IT WRITES
* --------------
* - STDERR: structured log lines via _shared/log.ts
* - STDOUT: JSON only (ToolResult shape from _shared/types.ts)
*
* EXTENDING
* ---------
* Add or edit checks in .sdlc/tools/quality-check/config.yaml:
* checks:
* - name: test
* description: Run unit tests
* script: cargo test --all
* The quality-check tool picks them up automatically — no code changes needed.
*/
import type { ToolMeta, ToolResult } from '../_shared/types.ts'
import { makeLogger } from '../_shared/log.ts'
import { getArgs, readStdin, exit } from '../_shared/runtime.ts'
import { execSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const log = makeLogger('quality-check')
// ---------------------------------------------------------------------------
// Tool metadata
// ---------------------------------------------------------------------------
export const meta: ToolMeta = {
name: 'quality-check',
display_name: 'Quality Check',
description: 'Runs checks from .sdlc/tools/quality-check/config.yaml and reports pass/fail',
version: '0.3.0',
requires_setup: false,
input_schema: {
type: 'object',
properties: {
scope: {
type: 'string',
description: 'Optional filter — only run checks whose name matches this string',
},
},
},
output_schema: {
type: 'object',
properties: {
passed: { type: 'number' },
failed: { type: 'number' },
checks: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
command: { type: 'string' },
status: { type: 'string', enum: ['passed', 'failed'] },
output: { type: 'string' },
duration_ms: { type: 'number' },
},
},
},
},
},
}
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface PlatformCommand {
name: string
description?: string
script: string
}
interface CheckResult {
name: string
description: string
command: string
status: 'passed' | 'failed'
output: string
duration_ms: number
}
interface QualityCheckOutput {
passed: number
failed: number
checks: CheckResult[]
}
// ---------------------------------------------------------------------------
// Config YAML parser — reads checks[] from tool-local config.yaml
// ---------------------------------------------------------------------------
/**
* Parse the `checks:` array from the tool's config.yaml.
* Handles the specific YAML shape used by quality-check:
* checks:
* - name: <string>
* description: <string>
* script: <single-quoted or bare string>
*/
function parseChecksFromYaml(content: string): PlatformCommand[] {
const checks: PlatformCommand[] = []
const lines = content.split('\n')
let inChecks = false
let current: Partial<PlatformCommand> | null = null
for (const line of lines) {
// Top-level `checks:` section header
if (/^checks:/.test(line)) {
inChecks = true
continue
}
// Any other top-level key ends the checks section
if (/^\S/.test(line) && !/^checks:/.test(line)) {
inChecks = false
}
if (!inChecks) continue
// New item: ` - name: <value>`
const itemMatch = line.match(/^\s{2}-\s+name:\s*(.*)$/)
if (itemMatch) {
if (current?.name && current?.script) {
checks.push(current as PlatformCommand)
}
current = { name: unquoteYaml(itemMatch[1].trim()), description: '', script: '' }
continue
}
if (!current) continue
const descMatch = line.match(/^\s+description:\s*(.*)$/)
if (descMatch) {
current.description = unquoteYaml(descMatch[1].trim())
continue
}
const scriptMatch = line.match(/^\s+script:\s*(.*)$/)
if (scriptMatch) {
current.script = unquoteYaml(scriptMatch[1].trim())
continue
}
}
if (current?.name && current?.script) {
checks.push(current as PlatformCommand)
}
return checks
}
/** Strip surrounding single or double quotes from a YAML scalar value. */
function unquoteYaml(s: string): string {
return s.replace(/^'([\s\S]*)'$/, '$1').replace(/^"([\s\S]*)"$/, '$1')
}
/** Load checks from the tool's own config.yaml. Returns [] on any error. */
function loadChecks(root: string): PlatformCommand[] {
const configPath = join(root, '.sdlc', 'tools', 'quality-check', 'config.yaml')
try {
const raw = readFileSync(configPath, 'utf8')
return parseChecksFromYaml(raw)
} catch (e) {
log.warn(`Could not read tool config at ${configPath}: ${e}`)
return []
}
}
// ---------------------------------------------------------------------------
// Run — execute platform checks
// ---------------------------------------------------------------------------
export async function run(
input: { scope?: string },
root: string,
): Promise<ToolResult<QualityCheckOutput>> {
const start = Date.now()
const commands = loadChecks(root)
if (commands.length === 0) {
log.warn('No checks configured in .sdlc/tools/quality-check/config.yaml — nothing to run')
const duration_ms = Date.now() - start
return {
ok: true,
data: { passed: 0, failed: 0, checks: [] },
duration_ms,
}
}
// Apply scope filter
const scope = input.scope?.trim()
const filtered = scope
? commands.filter(c => c.name.includes(scope))
: commands
log.info(`running ${filtered.length} check(s)${scope ? ` (scope: "${scope}")` : ''}`)
const checks: CheckResult[] = []
for (const cmd of filtered) {
const checkStart = Date.now()
log.info(`running check: ${cmd.name}`)
let status: 'passed' | 'failed' = 'passed'
let output = ''
try {
const result = execSync(cmd.script, {
cwd: root,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
})
output = result.slice(-500) // last 500 chars
} catch (e: unknown) {
status = 'failed'
if (e && typeof e === 'object' && 'stdout' in e && 'stderr' in e) {
const err = e as { stdout?: string; stderr?: string }
const combined = `${err.stdout ?? ''}${err.stderr ?? ''}`
output = combined.slice(-500)
} else {
output = String(e).slice(-500)
}
}
const duration_ms = Date.now() - checkStart
log.info(` ${cmd.name}: ${status} (${duration_ms}ms)`)
checks.push({
name: cmd.name,
description: cmd.description ?? '',
command: cmd.script,
status,
output,
duration_ms,
})
}
const passed = checks.filter(c => c.status === 'passed').length
const failed = checks.filter(c => c.status === 'failed').length
const duration_ms = Date.now() - start
log.info(`done: ${passed} passed, ${failed} failed in ${duration_ms}ms`)
return {
ok: failed === 0,
data: { passed, failed, checks },
duration_ms,
}
}
// ---------------------------------------------------------------------------
// CLI entrypoint
// ---------------------------------------------------------------------------
const mode = getArgs()[0] ?? '--run'
const root = process.env.SDLC_ROOT ?? process.cwd()
if (mode === '--meta') {
console.log(JSON.stringify(meta))
exit(0)
} else if (mode === '--run') {
readStdin()
.then(raw => run(JSON.parse(raw || '{}') as { scope?: string }, root))
.then(result => { console.log(JSON.stringify(result)); exit(result.ok ? 0 : 1) })
.catch(e => { console.log(JSON.stringify({ ok: false, error: String(e) })); exit(1) })
} else {
console.error(`Unknown mode: ${mode}. Use --meta or --run.`)
exit(1)
}