/// pql CLI wrapper — shells out per D-003 (wrap, don't duplicate). /// /// Every function runs `pql ` as a subprocess, parses /// the JSON stdout, and returns typed Dart maps. Errors surface as /// [PqlException] with the stderr diagnostics attached. library; import 'dart:convert'; import 'dart:io'; class PqlException implements Exception { const PqlException(this.message, {this.exitCode = 1, this.stderr = ''}); final String message; final int exitCode; final String stderr; @override String toString() => 'PqlException($exitCode): $message'; } class PqlClient { PqlClient({required this.workDir, this.pqlBinary = 'pql'}); final Directory workDir; final String pqlBinary; Future>> files({String? glob, int? limit}) async { final args = ['files']; if (glob != null) args.add(glob); if (limit != null) args.addAll(['--limit', '$limit']); return _runList(args); } Future> meta(String path) async { return _runObject(['meta', path]); } Future>> backlinks(String path) async { return _runList(['backlinks', path]); } Future>> outlinks(String path) async { return _runList(['outlinks', path]); } Future>> tags({int? limit}) async { final args = ['tags']; if (limit != null) args.addAll(['--limit', '$limit']); return _runList(args); } Future>> schema() async { return _runList(['schema']); } Future>> query(String dsl, {int? limit}) async { final args = ['query', dsl]; if (limit != null) args.addAll(['--limit', '$limit']); return _runList(args); } Future>> search(String terms, {int? limit}) async { final args = ['search', terms]; if (limit != null) args.addAll(['--limit', '$limit']); return _runList(args); } Future> doctor() async { return _runObject(['doctor']); } Future> decisionSync() async { return _runObject(['decisions', 'sync']); } Future decisionValidate() async { return _runObject(['decisions', 'validate']); } Future>> decisionList({ String? type, String? domain, String? status, }) async { final args = ['decisions', 'list']; if (type != null) args.addAll(['--type', type]); if (domain != null) args.addAll(['--domain', domain]); if (status != null) args.addAll(['--status', status]); return _runList(args); } Future> decisionShow( String id, { bool withRefs = false, bool withTickets = false, }) async { final args = ['decisions', 'show', id]; if (withRefs) args.add('--with-refs'); if (withTickets) args.add('--with-tickets'); return _runObject(args); } Future> decisionRead(String id) async { return _runObject(['decisions', 'read', id]); } Future>> decisionCoverage() async { return _runList(['decisions', 'coverage']); } Future>> ticketList({ String? status, String? team, String? assigned, String? decision, }) async { final args = ['ticket', 'list']; if (status != null) args.addAll(['--status', status]); if (team != null) args.addAll(['--team', team]); if (assigned != null) args.addAll(['--assigned', assigned]); if (decision != null) args.addAll(['--decision', decision]); return _runList(args); } Future> ticketShow( String id, { bool withDecision = false, bool withBlockers = false, }) async { final args = ['ticket', 'show', id]; if (withDecision) args.add('--with-decision'); if (withBlockers) args.add('--with-blockers'); return _runObject(args); } Future>> ticketBoard({String? team}) async { final args = ['ticket', 'board']; if (team != null) args.addAll(['--team', team]); return _runList(args); } Future> planStatus() async { return _runObject(['plan', 'status']); } // ------------------------------------------------------------------- Future>> _runList(List args) async { final result = await _run(args); if (result == null) return const []; if (result is List) { return [for (final e in result) (e as Map).cast()]; } return const []; } Future> _runObject(List args) async { final result = await _run(args); if (result is Map) return result.cast(); return const {}; } Future _run(List args) async { final r = await Process.run( pqlBinary, args, workingDirectory: workDir.path, ); final stderr = (r.stderr as String).trim(); // Exit 2 = zero matches — valid empty result, not an error. if (r.exitCode != 0 && r.exitCode != 2) { throw PqlException( 'pql ${args.first} failed', exitCode: r.exitCode, stderr: stderr, ); } final stdout = (r.stdout as String).trim(); if (stdout.isEmpty) return null; return jsonDecode(stdout); } }