Files
msd-core/src/task-command-router.cts
Tom Boucher a5f213e73e refactor(#1281): T2 — migrate 12 single-leaf callers off the core spine (batch 1) (#1282)
Per the T1 design rubber-duck, batch by FILE so each tranche drops
convergence-lint allowlist entries. Migrate 12 files' core imports to the
leaf modules directly (behaviour-identical — leaves are the objects core
re-exports by reference):
- io (output/error/ERROR_REASON): agent-command-router, capability-state,
  capability-writer, frontmatter, gsd2-import, learnings, loop-resolver,
  task-command-router
- roadmap-command-router -> config-loader; workstream-inventory -> core-utils
- milestone, verify -> their full leaf sets (both were multi-leaf, not
  single-leaf as first scoped; migrated completely)

All 12 files now import zero core symbols and are removed from the
allowlist (30 -> 18). core.cts re-exports untouched (still serve the
remaining 18 files); teardown is T-final. Stale core.cjs docstrings in the
migrated files corrected to reference io.cjs. No behaviour change.

Closes #1281

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:18:54 -04:00

112 lines
3.9 KiB
TypeScript

/**
* Task command router — is-behavior-adding subcommand handler.
*
* ADR-457 build-at-publish: the hand-written bin/lib/task-command-router.cjs
* collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
* from the prior hand-written .cjs; only types are added.
*/
import fs from 'node:fs';
import path from 'node:path';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import ioMod = require('./io.cjs');
const { output, error, ERROR_REASON } = ioMod;
// ─── Types ────────────────────────────────────────────────────────────────────
interface BehaviorAddingChecks {
tdd_true: boolean;
has_behavior_block: boolean;
has_source_files: boolean;
}
interface BehaviorAddingResult {
is_behavior_adding: boolean;
checks: BehaviorAddingChecks;
reason: string | null;
}
interface RouteTaskCommandOptions {
args: string[];
cwd: string;
raw: boolean;
}
// ─── Implementation ───────────────────────────────────────────────────────────
function isBehaviorAddingTaskContent(content: string): BehaviorAddingResult {
const tddTrue = /\btdd\s*=\s*["']true["']/i.test(content);
const behaviorMatch = content.match(/<behavior>([\s\S]*?)<\/behavior>/i);
const hasBehaviorBlock = Boolean(behaviorMatch && behaviorMatch[1].trim().length > 0);
const filesMatch = content.match(/<files>([\s\S]*?)<\/files>/i);
let hasSourceFiles = false;
if (filesMatch) {
const fileLines = filesMatch[1]
.split(/[\n,]/)
.map((line) => line.trim().replace(/^[-*]\s*/, ''))
.filter(Boolean);
hasSourceFiles = fileLines.some((file) =>
!/\.md$/i.test(file) &&
!/\.json$/i.test(file) &&
!/\.test\.[^.]+$/i.test(file) &&
!/\.spec\.[^.]+$/i.test(file) &&
!/(^|[\\/])tests?[\\/]/i.test(file) &&
!/\.(yml|yaml|toml|ini|cfg|conf|properties)$/i.test(file) &&
!/(^|[\\/])\.env(\..+)?$/i.test(file)
);
}
const isBehaviorAdding = tddTrue && hasBehaviorBlock && hasSourceFiles;
const missing: string[] = [];
if (!tddTrue) missing.push('tdd="true" frontmatter absent');
if (!hasBehaviorBlock) missing.push('<behavior> block missing or empty');
if (!hasSourceFiles) missing.push('<files> has no non-test source file');
return {
is_behavior_adding: isBehaviorAdding,
checks: {
tdd_true: tddTrue,
has_behavior_block: hasBehaviorBlock,
has_source_files: hasSourceFiles,
},
reason: isBehaviorAdding ? null : `Not behavior-adding: ${missing.join('; ')}`,
};
}
function routeTaskCommand({ args, cwd, raw }: RouteTaskCommandOptions): void {
const subcommand = args[1];
if (subcommand !== 'is-behavior-adding') {
error('Unknown task subcommand. Available: is-behavior-adding', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
let content: string | null = null;
if (args[2] === '--task-content') {
content = args[3] || null;
} else if (args[2]) {
const projectRoot = path.resolve(cwd || process.cwd());
const requestedPath = args[2];
const resolvedTaskPath = path.resolve(projectRoot, requestedPath);
const rel = path.relative(projectRoot, resolvedTaskPath);
if (rel === '..' || rel.startsWith(`..${path.sep}`)) {
error(`Task file is outside project scope: ${requestedPath}`, ERROR_REASON.USAGE);
}
if (!fs.existsSync(resolvedTaskPath)) {
error(`Task file not found: ${requestedPath}`, ERROR_REASON.USAGE);
}
content = fs.readFileSync(resolvedTaskPath, 'utf-8');
}
if (!content) {
error('Usage: task.is-behavior-adding <plan-file-path> | --task-content "<xml>"', ERROR_REASON.USAGE);
}
output(isBehaviorAddingTaskContent(content as string), raw, undefined);
}
export = {
isBehaviorAddingTaskContent,
routeTaskCommand,
};